Module:Article history

This is an old revision of this page, as edited by Mr. Stradivarius (talk | contribs) at 16:41, 21 October 2014 (get some output from our action objects). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

-------------------------------------------------------------------------------
--                            Article history
--
-- This module allows editors to link to all the significant events in an
-- article's history, such as good article nominations and featured article
-- nominations. It also displays its current status, as well as other
-- information, such as the date it was featured on the main page.
-------------------------------------------------------------------------------

local CONFIG_PAGE = 'Module:Article history/config'
local WRAPPER_TEMPLATE = 'Template:Article history'

-- Load required modules.
local yesno = require('Module:Yesno')
local lang = mw.language.getContentLanguage()

-------------------------------------------------------------------------------
-- Helper functions
-------------------------------------------------------------------------------

local function isPositiveInteger(num)
	return type(num) == 'number'
		and math.floor(num) == num
		and num > 0
		and num < math.huge
end

local function substituteParams(msg, ...)
	return mw.message.newRawMessage(msg, ...):plain()
end

local function makeUrlLink(url, display)
	return string.format('[%s %s]', url, display)
end

-------------------------------------------------------------------------------
-- Row class
-- This class represents one row in the template.
-------------------------------------------------------------------------------

local Row = {}
Row.__index = Row

function Row.new(data)
	local obj = setmetatable({}, Row)
	obj.cfg = data.cfg
	obj.isSmall = data.isSmall
	obj.categories = {}
	return obj
end

function Row.renderImage(image, caption, size)
	if caption then
		caption = '|' .. caption
	else
		caption = ''
	end
	return string.format('[[File:%s|%s%s]]', image, size, caption)
end

function Row:setIcon(icon, caption, size)
	self.icon = icon
	self.caption = caption
	self.iconSize = size
end

function Row:setText(text)
	self.text = text
end

function Row:exportHtml()
	local html = mw.html.create('tr')

	-- Render the icon link.
	local icon
	if self.icon then
		local iconSize
		if self.isSmall then
			iconSize = self.iconSize or self.cfg.defaultSmallIconSize or '15px'
		else
			iconSize = self.iconSize or self.cfg.defaultIconSize or '30px'
		end
		icon = self.renderImage(self.icon, self.caption, iconSize)
	end

	-- Render the HTML
	html
		:tag('td')
			:addClass('mbox-image')
			:wikitext(icon)
			:done()
		:tag('td')
			:addClass('mbox-text')
			:wikitext(self.text)

	return html
end

function Row:addCategory(categoryObj)
	table.insert(self.categories, categoryObj)
end

function Row:exportCategories()
	return self.categories
end

function Row:setNoticeBarIcon(icon, caption, size)
	self.noticeBarIcon = icon
	self.noticeBarCaption = caption
	self.noticeBarIconSize = size
end

function Row:exportNoticeBarIcon()
	if not self.noticeBarIcon then
		return nil
	end
	local size = self.noticeBarIconSize or self.cfg.defaultNoticeBarIconSize or '15px'
	return self.renderImage(self.noticeBarIcon, size, self.noticeBarCaption)
end

-------------------------------------------------------------------------------
-- Status class
-- Status objects deal with possible current statuses of the article.
-------------------------------------------------------------------------------

local Status = setmetatable({}, Row)
Status.__index = Status

function Status.new(data)
	local obj = Row.new(data)
	setmetatable(obj, Status)

	obj.id = data.id
	local statusCfg = obj.cfg.statuses[obj.id]
	
	-- Set the icon
	local iconSize
	if obj.isSmall then
		iconSize = statusCfg.smallIconSize or self.cfg.defaultSmallStatusIconSize or '30px'
	else
		iconSize = statusCfg.iconSize or self.cfg.defaultStatusIconSize or '50px'
	end
	obj:setIcon(statusCfg.icon, statusCfg.caption, iconSize)
	
	-- Set the text
	self:setText(substituteParams(
		statusCfg.text,
		self.currentTitle.subjectPageTitle.prefixedText,
		self.currentTitle.text
	))
	
	-- Add categories
	for i, categoryObj in ipairs(statusCfg.categories or {}) do
		self:addCategory(categoryObj)
	end

	return obj
end

-------------------------------------------------------------------------------
-- DoubleStatus class
-- For when an article can have two distinct statuses, e.g. former featured
-- article status and good article status.
-------------------------------------------------------------------------------

local DoubleStatus = setmetatable({}, Row)
DoubleStatus.__index = DoubleStatus

function DoubleStatus.new(data)
	local obj = Row.new(data)
	setmetatable(obj, DoubleStatus)
	return obj
end

-------------------------------------------------------------------------------
-- Notice class
-- Notice objects contain notices about an article that aren't part of its
-- current status, e.g. the date an article was featured on the main page.
-------------------------------------------------------------------------------

local Notice = setmetatable({}, Row)
Notice.__index = Notice

function Notice.new(data)
	local obj = Row.new(data)
	setmetatable(obj, Notice)
	return obj
end

-------------------------------------------------------------------------------
-- Action class
-- Action objects deal with a single action in the history of the article. We
-- use getter methods rather than properties for the name and result, etc., as
-- their processing needs to be delayed until after the status object has been
-- initialised. The status object needs to parse the action objects when it is
-- initialised, and the value of some names, etc., in the action objects depend
-- on the status object, so this is necessary to avoid errors/infinite loops.
-------------------------------------------------------------------------------

local Action = setmetatable({}, Row)
Action.__index = Action

-- Properties to implement:
-- * id
-- * oldid
-- * paramNum
-- * cfg
-- * actionCfg
-- * currentTitle

function Action.new(data)
	-- Valid data table keys:
	-- code, result, link, paramNum, cfg, currentTitle
	local obj = Row.new(data)
	setmetatable(obj, Action)

	obj.cfg = data.cfg
	obj.currentTitle = data.currentTitle
	obj.paramNum = data.paramNum

	-- Set the ID
	obj.id = obj.cfg.actions[data.code] and obj.cfg.actions[data.code].id
	if not obj.id then
		-- @TODO: Error
	end

	-- Add a shortcut for this action's config.
	obj.actionCfg = obj.cfg.actions[obj.id]

	-- Set the link
	obj.link = data.link or obj.currentTitle.talkPageTitle.prefixedText

	-- Set the result ID
	if obj.actionCfg.results[data.resultCode] then
		obj.resultId = obj.actionCfg.results[data.resultCode].id
	else
		-- @TODO: Error
	end

	-- Set the date
	if data.date then
		local success, date = pcall(
			lang.formatDate,
			lang,
			obj.cfg.msg['action-date-format'],
			data.date
		)
		if success and date then
			obj.date = date
		else
			-- @TODO: Error
		end
	else
		-- @TODO: Error
	end

	-- Set the oldid
	obj.oldid = tonumber(data.oldid)
	if obj.oldid and not isPositiveInteger(obj.oldid) then
		-- @TODO: Error
	end

	return obj
end

function Action:parseConfigVal(val, articleHistoryObj)
	if type(val) == 'function' then
		return val(self, articleHistoryObj)
	else
		return val
	end
end

function Action:getName(articleHistoryObj)
	return self:parseConfigVal(self.actionCfg.name, articleHistoryObj)
end

function Action:getResult(articleHistoryObj)
	return self:parseConfigVal(
		self.actionCfg.results[self.resultId].text,
		articleHistoryObj
	)
end

function Action:exportHtml(articleHistoryObj)
	local row = mw.html.create('tr')

	-- Date cell
	local dateCell = row:tag('td')
	if self.oldid then
		dateCell
			:tag('span')
				:addClass('plainlinks')
				:wikitext(makeUrlLink(
					self.currentTitle.subjectPageTitle:fullUrl{oldid = self.oldid},
					self.date
				))
	else
		dateCell:wikitext(self.date)
	end

	-- Process cell
	row
		:tag('td')
			:wikitext(string.format(
				"'''[[%s|%s]]'''",
				self.link,
				self:getName(articleHistoryObj)
			))
	
	-- Result cell
	row
		:tag('td')
			:wikitext(self:getResult(articleHistoryObj))

	return row
end

-------------------------------------------------------------------------------
-- CollapsibleNotice class
-- This class makes notices that go in the collapsible part of the template,
-- underneath the list of actions.
-------------------------------------------------------------------------------

local CollapsibleNotice = setmetatable({}, Row)
CollapsibleNotice.__index = CollapsibleNotice

function CollapsibleNotice.new(data)
	local obj = Row.new(data)
	setmetatable(obj, CollapsibleNotice)
	return obj
end

-------------------------------------------------------------------------------
-- ArticleHistory class
-- This class represents the whole template.
-------------------------------------------------------------------------------

local ArticleHistory = {}
ArticleHistory.__index = ArticleHistory

function ArticleHistory.new(args, cfg, currentTitle)
	local obj = setmetatable({}, ArticleHistory)

	-- Set input
	obj.args = args or {}
	obj.currentTitle = currentTitle or mw.title.getCurrentTitle()

	-- Set isSmall
	obj.isSmall = yesno(obj.args.small) or false
	
	-- Format the config
	local function substituteAliases(t, ret)
		-- This function substitutes strings found in an "aliases" subtable
		-- as keys in the parent table. It works recursively, so "aliases"
		-- subtables can be placed at any level. It assumes that tables will
		-- not be nested recursively, which should be true in the case of our
		-- config file.
		ret = ret or {}
		for k, v in pairs(t) do
			if k ~= 'aliases' then
				if type(v) == 'table' then
					local newRet = {}
					ret[k] = newRet
					if v.aliases then
						for _, alias in ipairs(v.aliases) do
							ret[alias] = newRet
						end
					end
					substituteAliases(v, newRet)
				else
					ret[k] = v
				end
			end
		end
		return ret
	end
	obj.cfg = substituteAliases(cfg or require(CONFIG_PAGE))
	
	return obj
end

function ArticleHistory:message(key, ...)
	local msg = self.cfg.msg[key]
	if select('#', ...) > 0 then
		return substituteParams(msg, ...)
	else
		return msg
	end
end

function ArticleHistory:getActionObjects()
	-- Gets an array of action objects for the parameters specified by the
	-- user. We memoise this so that the parameters only have to be processed
	-- once.
	if self.actions ~= nil then
		return self.actions
	end

	-- Filter the arguments for actions.
	local actionParams = {}
	local pattern = '^' .. self.cfg.actionParamPrefix .. '([1-9][0-9]*)(.-)$'
	local suffixes = self.cfg.actionParamSuffixes
	for k, v in pairs(self.args) do
		if type(k) == 'string' then
			local num, suffix = k:match(pattern)
			if num and suffix and suffixes[suffix] then
				num = tonumber(num)
				local t = actionParams[num] or {}
				t[suffixes[suffix]] = v
				t.paramNum = num
				actionParams[num] = t
			end
		end
	end
	
	-- Sort the action parameters.
	local actionParamsSorted = {}
	for num, t in pairs(actionParams) do
		table.insert(actionParamsSorted, t)
	end
	table.sort(actionParamsSorted, function (t1, t2)
		return t1.num < t2.num
	end)
	
	-- Create the action objects.
	local actions = {}
	for _, t in ipairs(actionParamsSorted) do
		t.cfg = self.cfg
		t.currentTitle = self.currentTitle
		table.insert(actions, Action.new(t))
	end

	self.actions = actions
	return actions
end

function ArticleHistory:getStatusIdForCode(code)
	-- Gets a status ID given a status code. If no code is specified, returns
	-- nil, and if the code is invalid, raises an error.
	if not code then
		return nil
	end
	local statuses = self.cfg.statuses
	code = mw.ustring.upper(code)
	if statuses[code] then
		return statuses[code].id
	else
		-- @TODO: Error
	end
end

function ArticleHistory:getStatusObj()
	-- Get the status object for the current status.
	if self.statusObj ~= nil then
		return self.statusObj
	end
	local statusId
	if self.cfg.getStatusIdFunction then
		statusId = self.cfg.getStatusIdFunction(self)
	else
		statusId = self:getStatusIdForCode(self.args.currentstatus)
	end
	if not statusId then
		self.statusObj = false
		return false
	end
	-- @TODO: Make new status object.
end

function ArticleHistory:getStatusId()
	local statusObj = self:getStatusObj()
	return statusObj and statusObj.id
end

function ArticleHistory:getNoticeObjects()
	-- Returns an array of notice objects to be output.
	local ret = {}
	return ret
end

function ArticleHistory:getCollapsibleNoticeObjects()
	-- Returns an array of collapsible notice objects to be output.
	local ret = {}
	return ret
end

function ArticleHistory:renderBox()
	local root = mw.html.create('table')
	root
		:addClass('tmbox tmbox-notice')
		:addClass(self.isSmall and 'mbox-small' or nil)
	
	-- Status
	local statusObj = self:getStatusObj()
	if statusObj then
		root:node(statusObj:exportHtml())
	end

	-- Notices
	local notices = self:getNoticeObjects()
	for _, noticeObj in ipairs(notices) do
		root:node(noticeObj:exportHtml())
	end

	-- Get the count of collapsible rows
	local actions = self:getActionObjects()
	local collapsibleNotices = self:getCollapsibleNoticeObjects()
	local noCollapsibleRows = #actions + #collapsibleNotices

	-- Collapsible table for actions and collapsible notices
	if noCollapsibleRows > 0 then
		-- Find out if we are collapsed or not.
		local isCollapsed
		if self.cfg.uncollapsedRows == 'all' then
			isCollapsed = false
		else
			isCollapsed = noCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3)
		end

		-- Collapsible table base
		local collapsibleTable = root
			:tag('tr')
				:tag('td')
					:attr('colspan', 2)
					:css('width', '100%')
					:tag('table')
						:addClass('AH-milestones')
						:addClass(isCollapsed and 'collapsible collapsed' or nil)
						:css('width', '100%')
						:css('background', 'transparent')
						:css('font-size', '90%')

		-- Header row
		local ctHeader = collapsibleTable
			:tag('tr')
				:tag('th')
					:attr('colspan', 3)
					:css('font-size', '110%')
		local noticeBarIcons = {}
		for i, t in ipairs{notices, collapsiblenotices, actions} do
			for j, obj in ipairs(t) do
				table.insert(noticeBarIcons, obj:exportNoticeBarIcon())
			end
		end
		if #noticeBarIcons > 0 then
			local noticeBar = ctHeader:tag('span'):css('float', 'left')
			for _, icon in ipairs(noticeBarIcons) do
				noticeBar:wikitext(icon)
			end
			ctHeader:wikitext(' ')
		end
		if mw.site.namespaces[self.currentTitle.namespace].subject.id == 0 then
			ctHeader:wikitext(self:message('milestones-header'))
		else
			ctHeader:wikitext(self:message(
				'milestones-header-other-ns',
				self.currentTitle.subjectNsText
			))
		end

		-- Subheadings
		collapsibleTable
			:tag('tr')
				:css('text-align', 'left')
				:tag('th')
					:wikitext(self:message('milestones-date-header'))
					:done()
				:tag('th')
					:wikitext(self:message('milestones-process-header'))
					:done()
				:tag('th')
					:wikitext(self:message('milestones-result-header'))

		-- Actions and collapsible notices
		for i, t in ipairs{actions, collapsibleNotices} do
			for j, obj in ipairs(t) do
				collapsibleTable:node(obj:exportHtml())
			end
		end
	end

	return tostring(root)
end

function ArticleHistory:renderCategories()
	local ret = {}
	for i, actionObj in ipairs(self:getActionObjects()) do
		for j, categoryObj in ipairs(actionObj:exportCategories()) do
			ret[#ret + 1] = tostring(categoryObj)
		end
	end
	return table.concat(ret)
end

function ArticleHistory:__tostring()
	return self:renderBox() .. self:renderCategories()
end

-------------------------------------------------------------------------------
-- Exports
-- These functions are called from Lua and from wikitext.
-------------------------------------------------------------------------------

local p = {}

function p._main(args, cfg, currentTitle)
	local articleHistoryObj = ArticleHistory.new(args, cfg, currentTitle)
	return tostring(articleHistoryObj)
end

function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		wrappers = WRAPPER_TEMPLATE
	})
	return p._main(args)
end

function p._exportClasses()
	return {
		Action = Action,
		ImageRow = ImageRow,
		Status = Status,
		Notice = Notice,
		ArticleHistory = ArticleHistory
	}
end

return p