Module:Signpost poll
MyWikiBiz, Author Your Legacy — Saturday December 21, 2024
Jump to navigationJump to searchTemplate:Lua This module implements Wikipedia:Wikipedia Signpost/Templates/Voter.
-- 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 msg end ------------------------------------------------------------------------------- -- Option class ------------------------------------------------------------------------------- local Option = {} Option.__index = Option Option.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 self end function Option:getCount() if self.count then return self.count else self.count = mw.getCurrentFrame():expandTemplate{title="String count",args={ page = self.votePage, search = self:getVoteText(n) }} return self.count end end function Option:setVoteTotal(n) self.total = n end 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 end end 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.color end function Option:getVoteText() self.voteText = self.voteText or self:message( 'vote-default', {self.nOption}, true ) return self.voteText end function Option:makeVoteURL() local url = mw.uri.fullUrl( self.votePage, { action = 'edit', section = 'new', nosummary = 'true', preload = self.preload, ['preloadparams[]'] = self:getVoteText() } ) 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 wrapper end 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', { self.text, self:getCount(), string.format('%.0f', self:getPercentage()) }, true)) return legend end ------------------------------------------------------------------------------- -- Poll class ------------------------------------------------------------------------------- local Poll = {} Poll.__index = Poll Poll.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{ nOption = i, text = text, voteText = args[key .. 'vote'], color = args[key .. 'color'], cfg = self.cfg, frame = self.frame, votePage = self.votePage, preload = preload }) 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', {votes[option:getVoteText()], option.nOption}, 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, { action = 'edit', preload = self:message('vote-page-preload-default'), ['preloadparams[]'] = mw.title.getCurrentTitle().prefixedText, summary = self:message('vote-page-create-summary'), editintro = self:message('vote-page-create-editintro') } ) error(self:message( 'votepage-nonexistent-error', {tostring(createVotePageUrl)} ), 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 self end -- 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 end end 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( '[[File:%s|right|30px|link=]]', self.icon )) :tag('span') :css('text-transform', 'uppercase') :css('color', '#999') :css('font-size', '105%') :css('font-weight', 'bold') :wikitext(self.headerText) return headerDiv end 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 question end 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( '[[File:%s|%s|link=]] ', 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 vzn end 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 legend end 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 end end 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 buttons end 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 warning end function Poll:hasMinimumVoteCount() return self.voteTotal >= self.minimum end function Poll:isOpen() if self.expiry then return self.getUnixDate() < self.getUnixDate(self.expiry) else return true end end 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', {self.minimum - self.voteTotal}, 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, { wrappers = cfg.wrappers }) return p._main(args, cfg, frame) end return p