Module:Article history: Difference between revisions

Content deleted Content added
update after the current title and current status code was moved to the article history objects in the config
per edit request on talk page - remove line as unnecessary
 
(122 intermediate revisions by 6 users not shown)
Line 4:
-- 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 or statuses, as well as other
-- 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()
 
-------------------------------------------------------------------------------
Line 20 ⟶ 27:
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('[[File:%s|%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
 
-------------------------------------------------------------------------------
-- ActionMessage classmixin
-- This mixin is used by all classes to add message-related methods.
-- Action objects deal with a single action in the history of the article.
-------------------------------------------------------------------------------
 
local ActionMessage = {}
Action.__index = Action
 
function Message:message(key, ...)
-- Properties to implement:
-- This fetches the message from the config with the specified key, and
-- * id
-- substitutes parameters $1, $2 etc. with the subsequent values it is
-- * name
-- * oldidpassed.
local msg = self.cfg.msg[key]
-- * paramNum
if select('#', ...) > 0 then
return substituteParams(msg, ...)
else
return msg
end
end
 
function Action.newMessage:raiseError(datamsg, help)
-- Raises an error with the specified message and help link. Execution
-- Valid data table keys:
-- stops unless the error is caught. This is used for errors where
-- code, result, link, paramNum, actionCfg
-- subsequent processing becomes impossible.
local obj = setmetatable({}, Action)
local errorText
return obj
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 ActionMessage:getResultaddWarning(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 ActionMessage:getTextgetWarnings()
return self.warnings or {}
end
 
-------------------------------------------------------------------------------
-- HeaderRow class
-- HeaderThis objectsclass arerepresents displayedone atrow the top ofin the template, above the.
-- collapsed section. They usually have an image and a blurb.
-------------------------------------------------------------------------------
 
local HeaderRow = {}
HeaderRow.__index = HeaderRow
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
 
-------------------------------------------------------------------------------
Line 63 ⟶ 314:
-------------------------------------------------------------------------------
 
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
 
-------------------------------------------------------------------------------
-- DoubleStatusMultiStatus class
-- For when an article can have twomultiple distinct statuses, e.g. former featured
-- featured article status and good article status.
-------------------------------------------------------------------------------
 
local DoubleStatusMultiStatus = setmetatable({}, Row)
DoubleStatusMultiStatus.__index = DoubleStatusMultiStatus
 
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 time-dependentpart of its
-- and aren't part of its current status, e.g. the topicdate areaan ofarticle awas goodfeatured on the main page.
-- article, or the date the article was featured on DYK.
-------------------------------------------------------------------------------
 
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|%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
 
-------------------------------------------------------------------------------
Line 92 ⟶ 779:
local ArticleHistory = {}
ArticleHistory.__index = ArticleHistory
addMixin(ArticleHistory, Message)
 
function ArticleHistory.new(args, cfg, currentTitle)
Line 98 ⟶ 786:
-- Set input
obj.args = args or {}
obj.cfg = cfg or require(CONFIG_PAGE)
obj.currentTitle = currentTitle or mw.title.getCurrentTitle()
 
-- Define object structure.
obj.actions_errors = {}
obj.categories_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:initializeActionObjectstry(func, ...)
if DEBUG_MODE then
-- Filter the arguments for actions.
local actionParamsval = {}func(...)
return val
local pattern = '^' .. self.cfg.actionParamPrefix .. '([1-9][0-9]*)(.-)$'
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 k_, vt in pairsipairs(argsactionArgs) do
local objArgs = {}
if type(k) == 'string' then
for k, v in pairs(t) do
local num, suffix = key:match(pattern)
iflocal numnewK and suffix and= suffixes[suffixk] then
if newK then
num = tonumber(num)
local t = actionParamsobjArgs[numnewK] or= {}v
t[suffixes[suffix]] = v
t.num = num
actionParams[num] = t
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
-- Sort the action parameters.
end
local actionParamsSorted = {}
 
for num, t in pairs(actionParams) do
function ArticleHistory:getStatusIdForCode(code)
table.insert(actionParamsSorted, t)
-- 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
table.sort(actionParamsSorted, function (t1, t2)
local codeUpper = mw.ustring.upper(code)
return t1.num < t2.num
if statuses[codeUpper] then
end)
return statuses[codeUpper].id
else
-- Create the action objects.
self:addWarning(
for _, t in ipairs(actionParamsSorted) do
self:message('articlehistory-warning-invalid-status', code),
table.insert(self.actions, Action.new(t))
self:message('articlehistory-warning-invalid-status-help')
)
return nil
end
end
 
function ArticleHistory:getCurrentStatusgetStatusObj()
-- 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:renderBoxgetStatusId()
local statusObj = self:getStatusObj()
return statusObj and statusObj.id
end
 
function ArticleHistory:addCategory_noticeFactory(categorymemoizeKey, sortKeyconfigKey, class)
-- This holds the logic for fetching tables of Notice and CollapsibleNotice
if category then
-- objects.
table.insert(self.categories, {category = category, sortKey = sortKey})
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 = {}
 
local categoryNsText = mw.site.namespaces[14].name
if self:categoriesAreActive() then
for _, t in ipairs(self.categories) do
-- Child object categories
local cat
for _, obj in ipairs(self:getAllObjects()) do
if t.sortKey then
local categories = self:try(obj.getCategories, obj, self)
cat = string.format(
for _, categoryObj in ipairs(categories or {}) do
'[[%s:%s|%s]]',
ret[#ret + 1] = tostring(categoryObj)
categoryNsText, t.category, t.sortKey
)end
elseend
 
cat = string.format('[[%s:%s]]', categoryNsText, category)
-- 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
table.insert(ret, cat)
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
 
Line 181 ⟶ 1,336:
local p = {}
 
function p._main(args, cfg, currentTitle)
local articleHistoryObj = ArticleHistory.new(args, cfg, currentTitle)
return tostring(articleHistoryObj)
end
 
Line 188 ⟶ 1,345:
wrappers = WRAPPER_TEMPLATE
})
if frame:getTitle():find('sandbox', 1, true) then
CONFIG_PAGE = CONFIG_PAGE .. '/sandbox'
end
return p._main(args)
end
Line 193 ⟶ 1,353:
function p._exportClasses()
return {
ActionMessage = ActionMessage,
ImageRowRow = ImageRowRow,
Status = Status,
MultiStatus = MultiStatus,
Notice = Notice,
Action = Action,
CollapsibleNotice = CollapsibleNotice,
ArticleHistory = ArticleHistory
}