Jump to content

Module:Signpost poll: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
add editintro for creating the vote page
on second thoughts, it doesn't make any sense to make the vote page preload a parameter
Line 180: Line 180:


-- Set optional fields
-- Set optional fields
self.votePagePreload = args.votepagepreload or self:message('vote-page-preload-default')
self.preload = args.preload or self:message('preload-default')
self.preload = args.preload or self:message('preload-default')
self.headerText = args.header or self:message('header-text')
self.headerText = args.header or self:message('header-text')
Line 242: Line 241:
{
{
action = 'edit',
action = 'edit',
preload = self.votePagePreload,
preload = self:message('vote-page-preload-default'),
['preloadparams[]'] = mw.title.getCurrentTitle().prefixedText,
['preloadparams[]'] = mw.title.getCurrentTitle().prefixedText,
summary = self:message('vote-page-create-summary'),
summary = self:message('vote-page-create-summary'),

Revision as of 03:45, 31 August 2015

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

local CONFIG_MODULE = 'Module:Signpost poll/config'

local yesno = require('Module:Yesno')
local mStringCount = require('Module:String count')
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 = mStringCount._count{
			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.preload = args.preload or self:message('preload-default')
	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 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 = self.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=]] &nbsp;',
				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('&nbsp;')
	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