Module:Signpost poll explained

-- This module implements polls used in articles of the Signpost.

local CONFIG_MODULE = 'Module:Signpost poll/config'

local yesno = require('Module:Yesno')local lang = mw.language.getContentLanguage

--------------------------------------------------------------------------------- Message method-- This method is available in every class, so it is defined separately.-------------------------------------------------------------------------------

local function message(self, key, params, isPreprocessed) local msg = self.cfg.msg[key] if params and #params > 0 then msg = mw.message.newRawMessage(msg, params):plain end if isPreprocessed then msg = self.frame:preprocess(msg) end return msgend

--------------------------------------------------------------------------------- Option class-------------------------------------------------------------------------------

local Option = Option.__index = OptionOption.message = message

function Option.new(t) local self = setmetatable(Option) self.cfg = t.cfg self.frame = t.frame self.nOption = t.nOption self.votePage = t.votePage self.preload = t.preload self.text = t.text self.voteText = t.voteText self.color = t.color return selfend

function Option:getCount if self.count then return self.count else self.count = mw.getCurrentFrame:expandTemplate return self.count endend

function Option:setVoteTotal(n) self.total = nend

function Option:getVoteTotal return self.total or error('total number of votes has not been set')end

function Option:getPercentage if self.percentage then return self.percentage else self.percentage = self:getCount / self:getVoteTotal * 100 return self.percentage endend

function Option:getColor -- Get the default color for option n if self.color then return self.color end local colors = self.cfg.colors local color = colors[self.nOption] if color then self.color = color else -- Loop to find the length of colors. We can't use the # operator as -- a metatable is set by mw.loadData. This is bad for polls with -- more options than there are colors in the config, as we would loop -- for every single option object. This will likely never be a problem -- in practice, however. local nColors = 0 for i in ipairs(colors) do nColors = i end -- colors[nColors] is necessary as Lua arrays are indexed starting at -- 1, and n % self.nColors might sometimes equal 0. self.color = colors[self.nOption % nColors] or colors[nColors] end return self.colorend

function Option:getVoteText self.voteText = self.voteText or self:message('vote-default', , true ) return self.voteTextend

function Option:makeVoteURL local url = mw.uri.fullUrl(self.votePage, ) return tostring(url)end

function Option:renderButton local button = mw.html.create('span') :addClass('mw-ui-button mw-ui-progressive') :attr('role', 'button') :attr('aria-disabled', 'false') :wikitext(self.text) local wrapper = mw.html.create('span') :addClass('plainlinks') :css('margin', '0 4px') :wikitext(string.format('[%s %s]', self:makeVoteURL, tostring(button) )) return wrapperend

function Option:renderLegendRow local legend = mw.html.create('div') legend :css('margin', '4px') :tag('span') :css('display', 'inline-block') :css('width', '1.5em') :css('height', '1.5em') :css('margin', '1px 0') :css('border', '1px solid black') :css('background-color', self:getColor) :css('text-align', 'center') :wikitext(' ') :done :wikitext(' ') :wikitext(self:message('legend-option-text',, true)) return legendend

--------------------------------------------------------------------------------- Poll class-------------------------------------------------------------------------------

local Poll = Poll.__index = PollPoll.message = message

function Poll.new(args, cfg, frame) local self = setmetatable(Poll) self.cfg = cfg or mw.loadData(CONFIG_MODULE) self.frame = frame or mw.getCurrentFrame

-- Set required fields self.question = assert(args.question, self:message('no-question-error')) self.votePage = assert(args.votepage, self:message('no-votepage-error'))

-- Set optional fields self.headerText = args.header or self:message('header-text') self.icon = args.icon or self:message('icon-default') self.overlay = args.overlay or self:message('overlay-default') self.minimum = tonumber(args.minimum) or self:message('minimum-default') self.expiry = args.expiry self.lineBreak = args['break']

-- Set options self.options = do local preload = self:message('preload-page') local i = 1 while true do local key = 'option' .. tostring(i) local text = args[key] if not text then break end table.insert(self.options, Option.new) i = i + 1 end if #self.options < 2 then error(self:message('not-enough-options-error')) end end

-- Check for duplicate vote text do local votes = for option in self:iterateOptions do if votes[option:getVoteText] then error(self:message('duplicate-vote-text-error', , true )) else votes[option:getVoteText] = option.nOption end end end

-- Prompt users to create the vote page if it doesn't exist. do local success, votePageContent = pcall(function return mw.title.new(self.votePage):getContent end) if not success or not votePageContent then local createVotePageUrl = mw.uri.fullUrl(self.votePage, ) error(self:message('votepage-nonexistent-error', ), 0) end end

-- Find total number of votes do local total = 0 for option in self:iterateOptions do total = total + option:getCount end for option in self:iterateOptions do option:setVoteTotal(total) end self.voteTotal = total end

return selfend

-- Static methods

function Poll.getUnixDate(date) date = lang:formatDate('U', date) return tonumber(date)end

-- Normal methods

function Poll:iterateOptions local i = 0 local n = #self.options return function i = i + 1 if i <= n then return self.options[i] end endend

function Poll:renderHeader local headerDiv = mw.html.create('div') headerDiv :css('border-top', '1px solid #CCC') :css('font-family', 'Georgia, Palatino, Palatino Linotype, Times, Times New Roman, serif') :css('color', '#333') :css('padding', '5px 0') :css('line-height', '120%') :wikitext(string.format('', self.icon )) :tag('span') :css('text-transform', 'uppercase') :css('color', '#999') :css('font-size', '105%') :css('font-weight', 'bold') :wikitext(self.headerText) return headerDivend

function Poll:renderQuestion local question = mw.html.create('div') :css('margin-top', '10px') :css('margin-bottom', '10px') :css('line-height', '100%') :css('font-size', '95%') :wikitext(self.question) return questionend

function Poll:renderVisualization local overlayWidth = '253px' local vzn = mw.html.create('div') :css('height', '250px') :css('border-spacing', '0') :css('width', overlayWidth) :css('margin-left', 'auto') :css('margin-right', 'auto')

-- Overlay vzn :tag('div') :css('position', 'absolute') :css('z-index', '2') :css('padding', '0') :css('margin', '0') :wikitext(string.format('  ', self.overlay, overlayWidth ))

-- Option colors for option in self:iterateOptions do vzn:tag('div') :css('background', option:getColor) :css('padding', '0') :css('margin', '0') :css('width', '250px') :css('height', string.format('%.3f%%', -- Round to 3 decimal places and add a percent sign option:getPercentage )) :wikitext(' ') end return vznend

function Poll:renderLegend local legend = mw.html.create('div') :css('margin-top', '3px') :css('display', 'flex') :css('justify-content', 'center') local centered = legend:tag('div') for option in self:iterateOptions do centered:node(option:renderLegendRow) end return legendend

function Poll:hasLineBreaks -- Try to auto-detect whether we should have line breaks if self.lineBreak then return yesno(self.lineBreak) or true end local nOptions = #self.options if nOptions > 3 then return true end local wordCount = 0 for option in self:iterateOptions do wordCount = wordCount + mw.ustring.len(option.text) end if nOptions

3 then return wordCount >= 12 else return wordCount >= 15 endend

function Poll:renderButtons local hasBreaks = self:hasLineBreaks local buttons = mw.html.create('div') :css('margin-top', '5px') :css('display', 'flex') :css('justify-content', 'center') local centered = buttons:tag('div') if not hasBreaks then centered:css('text-align', 'center') end for option in self:iterateOptions do local button if hasBreaks then button = centered:tag('div') :css('margin', '4px 0') else button = centered end button:node(option:renderButton) end return buttonsend

function Poll:renderWarning(s) local warning = mw.html.create('div') warning :css('line-height', '90%') :css('width', '100%') :css('margin-top', '5px') :css('text-align', 'center') :css('color', 'red') :css('font-size', '85%') :wikitext(s) return warningend

function Poll:hasMinimumVoteCount return self.voteTotal >= self.minimumend

function Poll:isOpen if self.expiry then return self.getUnixDate < self.getUnixDate(self.expiry) else return true endend

function Poll:__tostring local root = mw.html.create('div') :css('width', '270px') :css('float', 'right') :css('clear', 'right') :css('background', 'none') :css('margin-bottom', '10px') :css('margin-left', '10px') :addClass('signpost-sidebar')

root:node(self:renderHeader) root:node(self:renderQuestion)

-- Visualization and legend if self:hasMinimumVoteCount then root:node(self:renderVisualization) root:node(self:renderLegend) else root:node(self:renderWarning(self:message('not-enough-votes-warning', , true ))) end

-- Buttons if self:isOpen then root:node(self:renderButtons) else root:node(self:renderWarning(self:message('poll-closed-warning'))) end

return tostring(root)end

--------------------------------------------------------------------------------- Exports-------------------------------------------------------------------------------

local p =

function p._main(args, cfg, frame) return tostring(Poll.new(args, cfg, frame))end

function p.main(frame, cfg) cfg = cfg or mw.loadData(CONFIG_MODULE) local args = require('Module:Arguments').getArgs(frame,) return p._main(args, cfg, frame)end

return p