Module:Article history

-------------------------------------------------------------------------------

-- 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'

local DEBUG_MODE = false -- If true, errors are not caught.

-- Load required modules.

require('strict')

local Category = require('Module:Article history/Category')

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

local function maybeCallFunc(val, ...)

-- Checks whether val is a function, and if so calls it with the specified

-- arguments. Otherwise val is returned as-is.

if type(val) == 'function' then

return val(...)

else

return val

end

end

local function renderImage(image, caption, size)

if caption then

caption = '|' .. caption

else

caption = ''

end

return string.format('%s%s', image, size, caption)

end

local function addMixin(class, mixin)

-- Add a mixin to a class. The functions will be shared across classes, so

-- don't use it for functions that keep state.

for name, method in pairs(mixin) do

class[name] = method

end

end

-------------------------------------------------------------------------------

-- Message mixin

-- This mixin is used by all classes to add message-related methods.

-------------------------------------------------------------------------------

local Message = {}

function Message:message(key, ...)

-- This fetches the message from the config with the specified key, and

-- substitutes parameters $1, $2 etc. with the subsequent values it is

-- passed.

local msg = self.cfg.msg[key]

if select('#', ...) > 0 then

return substituteParams(msg, ...)

else

return msg

end

end

function Message:raiseError(msg, help)

-- Raises an error with the specified message and help link. Execution

-- stops unless the error is caught. This is used for errors where

-- subsequent processing becomes impossible.

local errorText

if help then

errorText = self:message('error-message-help', msg, help)

else

errorText = self:message('error-message-nohelp', msg)

end

error(errorText, 0)

end

function Message:addWarning(msg, help)

-- Adds a warning to the object's warnings table. Execution continues as

-- normal. This is used for errors that should be fixed but that do not

-- prevent the module from outputting something useful.

self.warnings = self.warnings or {}

local warningText

if help then

warningText = self:message('warning-help', msg, help)

else

warningText = self:message('warning-nohelp', msg)

end

table.insert(self.warnings, warningText)

end

function Message:getWarnings()

return self.warnings or {}

end

-------------------------------------------------------------------------------

-- Row class

-- This class represents one row in the template.

-------------------------------------------------------------------------------

local Row = {}

Row.__index = Row

addMixin(Row, Message)

function Row.new(data)

local obj = setmetatable({}, Row)

obj.cfg = data.cfg

obj.currentTitle = data.currentTitle

obj.makeData = data.makeData -- used by Row:getData

return obj

end

function Row:_cachedTry(cacheKey, errorCacheKey, func)

-- This method is for use in Row object methods that are called more than

-- once. The results of such methods should be cached to avoid unnecessary

-- processing. We also cache any errors found and abort if an error was

-- raised previously, otherwise error messages could be displayed multiple

-- times.

--

-- We use false as a key to cache nil results, so func cannot return false.

--

-- @param cacheKey The key to cache successful results with

-- @param errorCacheKey The key to cache errors with

-- @param func an anonymous function that returns the method result

if self[errorCacheKey] then

return nil

end

local ret = self[cacheKey]

if ret then

return ret

elseif ret == false then

return nil

end

local success

if DEBUG_MODE then

success = true

ret = func()

else

success, ret = pcall(func)

end

if success then

if ret then

self[cacheKey] = ret

return ret

else

self[cacheKey] = false

return nil

end

else

self[errorCacheKey] = true

-- We have already formatted the error message, so no need to format it

-- again.

error(ret, 0)

end

end

function Row:getData(articleHistoryObj)

return self:_cachedTry('_dataCache', '_isDataError', function ()

return self.makeData(articleHistoryObj)

end)

end

function Row:setIconValues(icon, caption, size)

self.icon = icon

self.iconCaption = caption

self.iconSize = size

end

function Row:getIcon(articleHistoryObj)

return maybeCallFunc(self.icon, articleHistoryObj, self)

end

function Row:getIconCaption(articleHistoryObj)

return maybeCallFunc(self.iconCaption, articleHistoryObj, self)

end

function Row:getIconSize()

return self.iconSize or self.cfg.defaultIconSize or '30px'

end

function Row:renderIcon(articleHistoryObj)

local icon = self:getIcon(articleHistoryObj)

if not icon then

return nil

end

return renderImage(

icon,

self:getIconCaption(articleHistoryObj),

self:getIconSize()

)

end

function Row:setNoticeBarIconValues(icon, caption, size)

self.noticeBarIcon = icon

self.noticeBarIconCaption = caption

self.noticeBarIconSize = size

end

function Row:getNoticeBarIcon(articleHistoryObj)

local icon = maybeCallFunc(self.noticeBarIcon, articleHistoryObj, self)

if icon == true then

icon = self:getIcon(articleHistoryObj)

if not icon then

self:raiseError(

self:message('row-error-missing-icon'),

self:message('row-error-missing-icon-help')

)

end

end

return icon

end

function Row:getNoticeBarIconCaption(articleHistoryObj)

local caption = maybeCallFunc(

self.noticeBarIconCaption,

articleHistoryObj,

self

)

if not caption then

caption = self:getIconCaption(articleHistoryObj)

end

return caption

end

function Row:getNoticeBarIconSize()

return self.noticeBarIconSize or self.cfg.defaultNoticeBarIconSize or '15px'

end

function Row:exportNoticeBarIcon(articleHistoryObj)

local icon = self:getNoticeBarIcon(articleHistoryObj)

if not icon then

return nil

end

return renderImage(

icon,

self:getNoticeBarIconCaption(articleHistoryObj),

self:getNoticeBarIconSize()

)

end

function Row:setText(text)

self.text = text

end

function Row:getText(articleHistoryObj)

return maybeCallFunc(self.text, articleHistoryObj, self)

end

function Row:exportHtml(articleHistoryObj)

if self._html then

return self._html

end

local text = self:getText(articleHistoryObj)

if not text then

return nil

end

local html = mw.html.create('tr')

html

:tag('td')

:addClass('mbox-image')

:wikitext(self:renderIcon(articleHistoryObj))

:done()

:tag('td')

:addClass('mbox-text')

:wikitext(text)

self._html = html

return html

end

function Row:setCategories(val)

-- Set the categories from the object's config. val can be either an array

-- of strings or a function returning an array of category objects.

self.categories = val

end

function Row:getCategories(articleHistoryObj)

local ret = {}

if type(self.categories) == 'table' then

for _, cat in ipairs(self.categories) do

ret[#ret + 1] = Category.new(cat)

end

elseif type(self.categories) == 'function' then

local t = self.categories(articleHistoryObj, self) or {}

for _, categoryObj in ipairs(t) do

ret[#ret + 1] = categoryObj

end

end

return ret

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

obj.statusCfg = obj.cfg.statuses[obj.id]

obj.name = obj.statusCfg.name

obj:setIconValues(

obj.statusCfg.icon,

obj.statusCfg.iconCaption or obj.name,

data.iconSize

)

obj:setNoticeBarIconValues(

obj.statusCfg.noticeBarIcon,

obj.statusCfg.noticeBarIconCaption or obj.name,

obj.statusCfg.noticeBarIconSize

)

obj:setText(obj.statusCfg.text)

obj:setCategories(obj.statusCfg.categories)

return obj

end

function Status:getIconSize()

return self.iconSize

or self.statusCfg.iconSize

or self.cfg.defaultStatusIconSize

or '50px'

end

function Status:getText(articleHistoryObj)

local text = Row.getText(self, articleHistoryObj)

if text then

return substituteParams(

text,

self.currentTitle.subjectPageTitle.prefixedText,

self.currentTitle.text

)

end

end

-------------------------------------------------------------------------------

-- MultiStatus class

-- For when an article can have multiple distinct statuses, e.g. former

-- featured article status and good article status.

-------------------------------------------------------------------------------

local MultiStatus = setmetatable({}, Row)

MultiStatus.__index = MultiStatus

function MultiStatus.new(data)

local obj = Row.new(data)

setmetatable(obj, MultiStatus)

obj.id = data.id

obj.statusCfg = obj.cfg.statuses[data.id]

obj.name = obj.statusCfg.name

-- Set child status objects

local function getChildStatusData(data, id, iconSize)

local ret = {}

for k, v in pairs(data) do

ret[k] = v

end

ret.id = id

ret.iconSize = iconSize

return ret

end

obj.statuses = {}

local defaultIconSize = obj.cfg.defaultMultiStatusIconSize or '30px'

for _, id in ipairs(obj.statusCfg.statuses) do

table.insert(obj.statuses, Status.new(getChildStatusData(

data,

id,

obj.cfg.statuses[id].iconMultiSize or defaultIconSize

)))

end

return obj

end

function MultiStatus:exportHtml(articleHistoryObj)

local ret = mw.html.create()

for _, obj in ipairs(self.statuses) do

ret:node(obj:exportHtml(articleHistoryObj))

end

return ret

end

function MultiStatus:getCategories(articleHistoryObj)

local ret = {}

for _, obj in ipairs(self.statuses) do

for _, categoryObj in ipairs(obj:getCategories(articleHistoryObj)) do

ret[#ret + 1] = categoryObj

end

end

return ret

end

function MultiStatus:exportNoticeBarIcon()

local ret = {}

for _, obj in ipairs(self.statuses) do

ret[#ret + 1] = obj:exportNoticeBarIcon()

end

return table.concat(ret)

end

function MultiStatus:getWarnings()

local ret = {}

for _, obj in ipairs(self.statuses) do

for _, msg in ipairs(obj:getWarnings()) do

ret[#ret + 1] = msg

end

end

return ret

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)

obj:setIconValues(

data.icon,

data.iconCaption,

data.iconSize

)

obj:setNoticeBarIconValues(

data.noticeBarIcon,

data.noticeBarIconCaption,

data.noticeBarIconSize

)

obj:setText(data.text)

obj:setCategories(data.categories)

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

function Action.new(data)

local obj = Row.new(data)

setmetatable(obj, Action)

obj.paramNum = data.paramNum

-- Set the ID

do

if not data.code then

obj:raiseError(

obj:message('action-error-no-code', obj:getParameter('code')),

obj:message('action-error-no-code-help')

)

end

local code = mw.ustring.upper(data.code)

obj.id = obj.cfg.actions[code] and obj.cfg.actions[code].id

if not obj.id then

obj:raiseError(

obj:message(

'action-error-invalid-code',

data.code,

obj:getParameter('code')

),

obj:message('action-error-invalid-code-help')

)

end

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

do

local resultCode = data.resultCode

and mw.ustring.lower(data.resultCode)

or '_BLANK'

if obj.actionCfg.results[resultCode] then

obj.resultId = obj.actionCfg.results[resultCode].id

elseif resultCode == '_BLANK' then

obj:raiseError(

obj:message(

'action-error-blank-result',

obj.id,

obj:getParameter('resultCode')

),

obj:message('action-error-blank-result-help')

)

else

obj:raiseError(

obj:message(

'action-error-invalid-result',

data.resultCode,

obj.id,

obj:getParameter('resultCode')

),

obj:message('action-error-invalid-result-help')

)

end

end

-- Set the date

if data.date then

local success, date = pcall(

lang.formatDate,

lang,

obj:message('action-date-format'),

data.date

)

if success and date then

obj.date = date

else

obj:addWarning(

obj:message(

'action-warning-invalid-date',

data.date,

obj:getParameter('date')

),

obj:message('action-warning-invalid-date-help')

)

end

else

obj:addWarning(

obj:message(

'action-warning-no-date',

obj.paramNum,

obj:getParameter('date'),

obj:getParameter('code')

),

obj:message('action-warning-no-date-help')

)

end

obj.date = obj.date or obj:message('action-date-missing')

-- Set the oldid

obj.oldid = tonumber(data.oldid)

if data.oldid and (not obj.oldid or not isPositiveInteger(obj.oldid)) then

obj.oldid = nil

obj:addWarning(

obj:message(

'action-warning-invalid-oldid',

data.oldid,

obj:getParameter('oldid')

),

obj:message('action-warning-invalid-oldid-help')

)

end

-- Set the notice bar icon values

obj:setNoticeBarIconValues(

data.noticeBarIcon,

data.noticeBarIconCaption,

data.noticeBarIconSize

)

-- Set the categories

obj:setCategories(obj.actionCfg.categories)

return obj

end

function Action:getParameter(key)

-- Finds the original parameter name for the given key that was passed to

-- Action.new.

local prefix = self.cfg.actionParamPrefix

local suffix

for k, v in pairs(self.cfg.actionParamSuffixes) do

if v == key then

suffix = k

break

end

end

if not suffix then

error('invalid key "' .. tostring(key) .. '" passed to Action:getParameter', 2)

end

return prefix .. tostring(self.paramNum) .. suffix

end

function Action:getName(articleHistoryObj)

return maybeCallFunc(self.actionCfg.name, articleHistoryObj, self)

end

function Action:getResult(articleHistoryObj)

return maybeCallFunc(

self.actionCfg.results[self.resultId].text,

articleHistoryObj,

self

)

end

function Action:exportHtml(articleHistoryObj)

if self._html then

return self._html

end

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",

self.link,

self:getName(articleHistoryObj)

))

-- Result cell

row

:tag('td')

:wikitext(self:getResult(articleHistoryObj))

self._html = row

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)

obj:setIconValues(

data.icon,

data.iconCaption,

data.iconSize

)

obj:setNoticeBarIconValues(

data.noticeBarIcon,

data.noticeBarIconCaption,

data.noticeBarIconSize

)

obj:setText(data.text)

obj:setCollapsibleText(data.collapsibleText)

obj:setCategories(data.categories)

return obj

end

function CollapsibleNotice:setCollapsibleText(s)

self.collapsibleText = s

end

function CollapsibleNotice:getCollapsibleText(articleHistoryObj)

return maybeCallFunc(self.collapsibleText, articleHistoryObj, self)

end

function CollapsibleNotice:getIconSize()

return self.iconSize

or self.cfg.defaultCollapsibleNoticeIconSize

or '20px'

end

function CollapsibleNotice:exportHtml(articleHistoryObj, isInCollapsibleTable)

local cacheKey = isInCollapsibleTable

and '_htmlCacheCollapsible'

or '_htmlCacheDefault'

return self:_cachedTry(cacheKey, '_isHtmlError', function ()

local text = self:getText(articleHistoryObj)

if not text then

return nil

end

local function maybeMakeCollapsibleTable(cell, text, collapsibleText)

-- If collapsible text is specified, makes a collapsible table

-- inside the cell with two rows, a header row with one cell and a

-- collapsed row with one cell. These are filled with text and

-- collapsedText, respectively. If no collapsible text is

-- specified, the text is added to the cell as-is.

if collapsibleText then

cell

:tag('div')

:addClass('mw-collapsible mw-collapsed')

:tag('div')

:wikitext(text)

:done()

:tag('div')

:addClass('mw-collapsible-content')

:css('border', '1px silver solid')

:wikitext(collapsibleText)

else

cell:wikitext(text)

end

end

local html = mw.html.create('tr')

local icon = self:renderIcon(articleHistoryObj)

local collapsibleText = self:getCollapsibleText(articleHistoryObj)

if isInCollapsibleTable then

local textCell = html:tag('td')

:attr('colspan', 3)

:css('width', '100%')

local rowText

if icon then

rowText = icon .. ' ' .. text

else

rowText = text

end

maybeMakeCollapsibleTable(textCell, rowText, collapsibleText)

else

local textCell = html

:tag('td')

:addClass('mbox-image')

:wikitext(icon)

:done()

:tag('td')

:addClass('mbox-text')

maybeMakeCollapsibleTable(textCell, text, collapsibleText)

end

return html

end)

end

-------------------------------------------------------------------------------

-- ArticleHistory class

-- This class represents the whole template.

-------------------------------------------------------------------------------

local ArticleHistory = {}

ArticleHistory.__index = ArticleHistory

addMixin(ArticleHistory, Message)

function ArticleHistory.new(args, cfg, currentTitle)

local obj = setmetatable({}, ArticleHistory)

-- Set input

obj.args = args or {}

obj.currentTitle = currentTitle or mw.title.getCurrentTitle()

-- Define object structure.

obj._errors = {}

obj._allObjectsCache = {}

-- 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))

--[[

-- Get a table of the arguments sorted by prefix and number. Non-string

-- keys and keys that don't contain a number are ignored. (This means that

-- positional parameters are ignored, as they are numbers, not strings.)

-- The parameter numbers are stored in the first positional parameter of

-- the subtables, and any gaps are removed so that the tables can be

-- iterated over with ipairs.

--

-- For example, these arguments:

-- {a1x = 'eggs', a1y = 'spam', a2x = 'chips', b1z = 'beans', b3x = 'bacon'}

-- would translate into this prefixArgs table.

-- {

-- a = {

-- {1, x = 'eggs', y = 'spam'},

-- {2, x = 'chips'}

-- },

-- b = {

-- {1, z = 'beans'},

-- {3, x = 'bacon'}

-- }

-- }

--]]

do

local prefixArgs = {}

for k, v in pairs(obj.args) do

if type(k) == 'string' then

local prefix, num, suffix = k:match('^(.-)([1-9][0-9]*)(.*)$')

if prefix then

num = tonumber(num)

prefixArgs[prefix] = prefixArgs[prefix] or {}

prefixArgs[prefix][num] = prefixArgs[prefix][num] or {}

prefixArgs[prefix][num][suffix] = v

prefixArgs[prefix][num][1] = num

end

end

end

-- Remove the gaps

local prefixArrays = {}

for prefix, prefixTable in pairs(prefixArgs) do

prefixArrays[prefix] = {}

local numKeys = {}

for num in pairs(prefixTable) do

numKeys[#numKeys + 1] = num

end

table.sort(numKeys)

for _, num in ipairs(numKeys) do

table.insert(prefixArrays[prefix], prefixTable[num])

end

end

obj.prefixArgs = prefixArrays

end

return obj

end

function ArticleHistory:try(func, ...)

if DEBUG_MODE then

local val = func(...)

return val

else

local success, val = pcall(func, ...)

if success then

return val

else

table.insert(self._errors, val)

return nil

end

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 then

return self.actions

end

-- Get the action args, and exit if they don't exist.

local actionArgs = self.prefixArgs[self.cfg.actionParamPrefix]

if not actionArgs then

self.actions = {}

return self.actions

end

-- Make the objects.

local actions = {}

local suffixes = self.cfg.actionParamSuffixes

for _, t in ipairs(actionArgs) do

local objArgs = {}

for k, v in pairs(t) do

local newK = suffixes[k]

if newK then

objArgs[newK] = v

end

end

objArgs.paramNum = t[1]

objArgs.cfg = self.cfg

objArgs.currentTitle = self.currentTitle

local actionObj = self:try(Action.new, objArgs)

table.insert(actions, actionObj)

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

local codeUpper = mw.ustring.upper(code)

if statuses[codeUpper] then

return statuses[codeUpper].id

else

self:addWarning(

self:message('articlehistory-warning-invalid-status', code),

self:message('articlehistory-warning-invalid-status-help')

)

return nil

end

end

function ArticleHistory:getStatusObj()

-- Get the status object for the current status.

if self.statusObj == false then

return nil

elseif self.statusObj ~= nil then

return self.statusObj

end

local statusId

if self.cfg.getStatusIdFunction then

statusId = self:try(self.cfg.getStatusIdFunction, self)

else

statusId = self:try(

self.getStatusIdForCode, self,

self.args[self.cfg.currentStatusParam]

)

end

if not statusId then

self.statusObj = false

return nil

end

-- Check that some actions were specified, and if not add a warning.

local actions = self:getActionObjects()

if #actions < 1 then

self:addWarning(

self:message('articlehistory-warning-status-no-actions'),

self:message('articlehistory-warning-status-no-actions-help')

)

end

-- Make a new status object.

local statusObjData = {

id = statusId,

currentTitle = self.currentTitle,

cfg = self.cfg

}

local isMulti = self.cfg.statuses[statusId].isMulti

local initFunc = isMulti and MultiStatus.new or Status.new

local statusObj = self:try(initFunc, statusObjData)

self.statusObj = statusObj or false

return self.statusObj or nil

end

function ArticleHistory:getStatusId()

local statusObj = self:getStatusObj()

return statusObj and statusObj.id

end

function ArticleHistory:_noticeFactory(memoizeKey, configKey, class)

-- This holds the logic for fetching tables of Notice and CollapsibleNotice

-- objects.

if self[memoizeKey] then

return self[memoizeKey]

end

local ret = {}

for _, t in ipairs(self.cfg[configKey] or {}) do

if t.isActive(self) then

local data = {}

for k, v in pairs(t) do

if k ~= 'isActive' then

data[k] = v

end

end

data.cfg = self.cfg

data.currentTitle = self.currentTitle

ret[#ret + 1] = class.new(data)

end

end

self[memoizeKey] = ret

return ret

end

function ArticleHistory:getNoticeObjects()

return self:_noticeFactory('notices', 'notices', Notice)

end

function ArticleHistory:getCollapsibleNoticeObjects()

return self:_noticeFactory(

'collapsibleNotices',

'collapsibleNotices',

CollapsibleNotice

)

end

function ArticleHistory:getAllObjects(addSelf)

local cacheKey = addSelf and 'addSelf' or 'default'

local ret = self._allObjectsCache[cacheKey]

if not ret then

ret = {}

local statusObj = self:getStatusObj()

if statusObj then

ret[#ret + 1] = statusObj

end

local objTables = {

self:getNoticeObjects(),

self:getActionObjects(),

self:getCollapsibleNoticeObjects()

}

for _, t in ipairs(objTables) do

for _, obj in ipairs(t) do

ret[#ret + 1] = obj

end

end

if addSelf then

ret[#ret + 1] = self

end

self._allObjectsCache[cacheKey] = ret

end

return ret

end

function ArticleHistory:getNoticeBarIcons()

local ret = {}

-- Icons that aren't part of a row.

if self.cfg.noticeBarIcons then

for _, data in ipairs(self.cfg.noticeBarIcons) do

if data.isActive(self) then

ret[#ret + 1] = renderImage(

data.icon,

nil,

data.size or self.cfg.defaultNoticeBarIconSize

)

end

end

end

-- Icons in row objects.

for _, obj in ipairs(self:getAllObjects()) do

ret[#ret + 1] = obj:exportNoticeBarIcon(self)

end

return ret

end

function ArticleHistory:getErrorMessages()

-- Returns an array of error/warning strings. Error strings come first.

local ret = {}

for _, msg in ipairs(self._errors) do

ret[#ret + 1] = msg

end

for _, obj in ipairs(self:getAllObjects(true)) do

for _, msg in ipairs(obj:getWarnings()) do

ret[#ret + 1] = msg

end

end

return ret

end

function ArticleHistory:categoriesAreActive()

-- Returns a boolean indicating whether categories should be output or not.

local title = self.currentTitle

local ns = title.namespace

return title.isTalkPage

and ns ~= 3 -- not user talk

and ns ~= 119 -- not draft talk

end

function ArticleHistory:renderCategories()

local ret = {}

if self:categoriesAreActive() then

-- Child object categories

for _, obj in ipairs(self:getAllObjects()) do

local categories = self:try(obj.getCategories, obj, self)

for _, categoryObj in ipairs(categories or {}) do

ret[#ret + 1] = tostring(categoryObj)

end

end

-- Extra categories

for _, func in ipairs(self.cfg.extraCategories or {}) do

local cats = func(self) or {}

for _, categoryObj in ipairs(cats) do

ret[#ret + 1] = tostring(categoryObj)

end

end

end

return table.concat(ret)

end

function ArticleHistory:__tostring()

local root = mw.html.create()

-- Table root

local tableRoot = root:tag('table')

tableRoot:addClass('article-history tmbox tmbox-notice')

-- Status

local statusObj = self:getStatusObj()

if statusObj then

tableRoot:node(self:try(statusObj.exportHtml, statusObj, self))

end

-- Notices

local notices = self:getNoticeObjects()

for _, noticeObj in ipairs(notices) do

tableRoot:node(self:try(noticeObj.exportHtml, noticeObj, self))

end

-- Get action objects and the collapsible notice objects, and generate the

-- HTML objects for the action objects. We need the action HTML objects so

-- that we can accurately calculate the number of collapsible rows, as some

-- action objects may generate errors when the HTML is generated.

local actions = self:getActionObjects() or {}

local collapsibleNotices = self:getCollapsibleNoticeObjects() or {}

local collapsibleNoticeHtmlObjects, actionHtmlObjects = {}, {}

for _, obj in ipairs(actions) do

table.insert(

actionHtmlObjects,

self:try(obj.exportHtml, obj, self)

)

end

for _, obj in ipairs(collapsibleNotices) do

table.insert(

collapsibleNoticeHtmlObjects,

self:try(obj.exportHtml, obj, self, true) -- Render the collapsed version

)

end

local nActionRows = #actionHtmlObjects

local nCollapsibleRows = nActionRows + #collapsibleNoticeHtmlObjects

-- Find out if we are collapsed or not.

local isCollapsed = yesno(self.args.collapse)

if isCollapsed == nil then

if self.cfg.uncollapsedRows == 'all' then

isCollapsed = false

elseif nCollapsibleRows == 1 then

isCollapsed = false

else

isCollapsed = nCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3)

end

end

-- If we are not collapsed, re-render the collapsible notices in the

-- non-collapsed version.

if not isCollapsed then

collapsibleNoticeHtmlObjects = {}

for _, obj in ipairs(collapsibleNotices) do

table.insert(

collapsibleNoticeHtmlObjects,

self:try(obj.exportHtml, obj, self, false)

)

end

end

-- Collapsible table for actions and collapsible notices. Collapsible

-- notices are only included in the table if it is collapsed. Action rows

-- are always included.

local collapsibleTable

if isCollapsed or nActionRows > 0 then

-- Collapsible table base

collapsibleTable = tableRoot

:tag('tr')

:tag('td')

:attr('colspan', 2)

:css('width', '100%')

:tag('table')

:addClass('article-history-milestones')

:addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil)

:css('width', '100%')

:css('font-size', '90%')

-- Header row

local ctHeader = collapsibleTable

:tag('tr')

:tag('th')

:attr('colspan', 3)

:css('font-size', '110%')

-- Notice bar

if isCollapsed then

local noticeBarIcons = self:getNoticeBarIcons()

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

end

-- Header text

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

if nActionRows > 0 then

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'))

end

-- Actions

for _, htmlObj in ipairs(actionHtmlObjects) do

collapsibleTable:node(htmlObj)

end

end

-- Collapsible notices and current status

-- These are only included in the collapsible table if it is collapsed.

-- Otherwise, they are added afterwards, so that they align with the

-- notices.

do

local tableNode, statusColspan

if isCollapsed then

tableNode = collapsibleTable

statusColspan = 3

else

tableNode = tableRoot

statusColspan = 2

end

-- Collapsible notices

for _, obj in ipairs(collapsibleNotices) do

tableNode:node(self:try(obj.exportHtml, obj, self, isCollapsed))

end

-- Current status

if statusObj and nActionRows > 1 then

tableNode

:tag('tr')

:tag('td')

:attr('colspan', statusColspan)

:wikitext(self:message('status-blurb', statusObj.name))

end

end

-- Get the categories. We have to do this before the error row, so that

-- category errors display.

local categories = self:renderCategories()

-- Error row and error category

local errors = self:getErrorMessages()

local errorCategory

if #errors > 0 then

local errorList = tableRoot

:tag('tr')

:tag('td')

:attr('colspan', 2)

:addClass('mbox-text')

:tag('ul')

:addClass('error')

:css('font-weight', 'bold')

for _, msg in ipairs(errors) do

errorList:tag('li'):wikitext(msg)

end

if self:categoriesAreActive() then

errorCategory = tostring(Category.new(self:message(

'error-category'

)))

end

-- If there are no errors and no active objects, then exit. We can't make

-- this check earlier as we don't know where the errors may be until we

-- have finished rendering the banner.

elseif #self:getAllObjects() < 1 then

return ''

end

-- Add the categories

root:wikitext(categories)

root:wikitext(errorCategory)

local frame = mw.getCurrentFrame()

return frame:extensionTag{

name = 'templatestyles', args = { src = 'Module:Message box/tmbox.css' }

} .. frame:extensionTag{

name = 'templatestyles', args = { src = 'Module:Article history/styles.css' }

} .. tostring(root)

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

})

if frame:getTitle():find('sandbox', 1, true) then

CONFIG_PAGE = CONFIG_PAGE .. '/sandbox'

end

return p._main(args)

end

function p._exportClasses()

return {

Message = Message,

Row = Row,

Status = Status,

MultiStatus = MultiStatus,

Notice = Notice,

Action = Action,

CollapsibleNotice = CollapsibleNotice,

ArticleHistory = ArticleHistory

}

end

return p