This module generates the vote count in WT:Votes/Active and {{votes}}
.
Use the function status
to get the vote count.
The Lua code below should be completely commented.
local export = {}
local find_templates = require("Module:template parser").find_templates
local insert = table.insert
local sort = table.sort
local split
-- This function returns the full vote page.
local function getFullPage (pageName)
local pageObject = mw.title.new (pageName)
if pageObject.isRedirect then
error(pageName .. " is a redirect.")
end
return pageObject:getContent()
end
local function errorMsg(msg)
return '<span class="maintenance-line" style="color: #FF0000;">' .. msg .. "</span>"
end
-- This function returns the portion of a vote page with the actual votes, without the vote description and the decision.
local function pageExcerpt (pageName)
-- Get the text between "Enter '# {{support}} ~~~~' on next blank line",
-- which appears in the first support section, and the "Decision" section,
-- which appears below the votes.
local page = getFullPage (pageName)
if not page then
error("The page '" .. pageName .. "' does not exist.")
end
return page:match ("Enter '# {{support}} ~~~~' on next blank line(.+)====? *Decision *====?")
or error("Part of " .. pageName .. " with votes not found.")
end
function normalizeNamespace(namespace)
return namespace:lower():gsub("_", " ")
end
function isUsernameIpV4(username)
local parts = { username:match("^(+)%.(+)%.(+)%.(+)$") }
if not parts then return false end
for i = 1, 4 do
local part = parts
--
if tonumber(part) > 255 then return false end
-- no leading zero
if part:match("0") then return false end
end
return true
end
function isUsernameIpV6(username)
if not split then split = require("Module:string utilities").split end
local parts = split(username, ":", true, true)
if #parts < 2 or #parts > 8 then return false end
-- cannot start with a single :
if #parts == 0 and #parts > 0 then return false end
local compressed = false
for i = 1, #parts do
local part = parts
if #part == 0 then -- ::
-- trailing colon only allowed in trailing ::
if compressed == (i < #parts) then
-- can have only one ::
return false
-- do not record an empty section if it is at the beginning, since leading :: is valid
elseif i > 1 then
compressed = true
end
-- check for hex characters, 1-4
elseif not part:match("^+$") or #part > 4 then
return false
end
end
return true
end
function isUsernameIp(username)
-- iPv4
return isUsernameIpV4(username) or isUsernameIpV6(username)
end
-- Processes all links that may have a namespace, stores each username in a link
-- to a "User" or "User talk" page, and returns the last username encountered,
-- or else nil.
-- Removes fragments (the part after #) and link text (the part after | if the
-- link is piped).
-- Consider the text:
-- # {{support}} per ] --] (]) 8:00 pm, 12 September 2015, Saturday (4 years, 5 months, 3 days ago) (UTC−5)
-- It has three "User" or "User talk" links.
-- The username "Example3" from the last link, ],
-- is returned.
-- Sometimes, a person has only the link to the user page or the talk page,
-- this makes sure their votes are counted too.
function getLastUsername(text)
local username
for potentialNamespace, potentialUser in text:gmatch("%+):(#|]+).-%]%]") do
potentialNamespace = normalizeNamespace(potentialNamespace)
if potentialNamespace == "user" or potentialNamespace == "user talk" then
username = mw.text.trim(potentialUser)
end
end
if not username then
-- If {{unsigned}} is present, parse the user name from that, unless it is an IP.
for template in find_templates(text) do
if template:get_name() == "unsigned" then
local args = template:get_arguments()
if args and not isUsernameIp(args) then
username = args
end
end
end
end
return username
end
-- This function returns a variable with a list of all people who voted in the page.
function countVotes (pageName)
local votesText = pageExcerpt (pageName)
-- Initializes new fields to 0.
local votes = setmetatable({}, {
__index = function(self, key)
self = 0
return 0
end
})
local support, oppose, abstain = {}, {}, {}
local stances = {
= support,
= support,
= support,
= oppose,
= oppose,
= oppose,
= abstain
}
-- Iterates over all lines that start with a "#" but not with "##", "#*" or
-- "#:", which excludes discussion and crossed-out votes.
-- Example of accepted line: # {{support}} --] (]) 9:16 pm, 25 November 2015, Wednesday (4 years, 2 months, 21 days ago) (UTC−6)
for numberedLine in votesText:gmatch("%f#(*)") do
local username = getLastUsername(numberedLine)
if username then
votes = votes + 1;
-- Find the first instance of {{support}}, {{oppose}}, or
-- {{abstain}} and add username to the count for that stance.
for template in find_templates(numberedLine) do
local votes = stances
if votes then
insert(votes, username)
break
end
end
end
end
return votes, stances
end
-- This function returns the start date of a vote, in the format of "Dec 9".
function startDate (pageName)
local fullPage = getFullPage(pageName)
local startDateFull = string.match (fullPage, "Vote start.-%(UTC%)")
local startDateTable = mw.text.split (startDateFull, "Vote start.-: ")
local languageObject = mw.language.new ("en")
local ok, startDate = pcall(function() return languageObject:formatDate ("M j", startDateTable) end)
return ok and startDate or errorMsg("Unable to format date '" .. (startDateTable or "nil") .. "'")
end
-- Returns the status of a vote. Either it is a premature vote, or it is the number of votes.
function export.status (frame)
local fullPage = getFullPage(frame.args)
local votes, stances = countVotes (frame.args)
local count = select(2, string.gsub(fullPage, "==== Support", "==== Support"))
local isMultipleVotes = count > 1
local allVotes = 0
local uniqueVoters = 0
local uniqueVoterNames = {}
for username, voteQuantity in pairs(votes) do
allVotes = allVotes + voteQuantity
uniqueVoters = uniqueVoters + 1
if isMultipleVotes then
insert(uniqueVoterNames, username .. " (" .. voteQuantity .. ")")
else
insert(uniqueVoterNames, username)
end
end
sort(uniqueVoterNames)
local status = ""
local p = ""
if uniqueVoters == 1 then
p = "person"
else
p = "people"
end
if isMultipleVotes then
status = allVotes .. " (" .. uniqueVoters .. " " .. p .. ")"
else
status =
"]" .. #stances.support
.. " ]" .. #stances.oppose
.. " ]" .. #stances.abstain
end
-- Checks for {{premature}}, which marks unstarted votes. If the current page is an unstarted vote, add the start date.
if string.find (fullPage, "{{premature}}") then
status = "starts: " .. startDate (frame.args)
end
-- If there are votes, then formats the end result with <span></span> and the title text with the list of voters.
if allVotes > 0 then
status = '<span title="' .. mw.text.listToText(uniqueVoterNames, ", ", ", ") .. '" style="border-bottom: dotted 1px black">' .. status .. "</span>"
end
return status
end
return export