Difference between revisions of "Module:Article history/sandbox"
Jump to navigation
Jump to search
blackwiki>Mr. Stradivarius (make a start at turning the Row class into an ArticleProperty class and separating the icon code) |
m (37 revisions imported) |
||
(15 intermediate revisions by 4 users not shown) | |||
Line 8: | Line 8: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
− | local CONFIG_PAGE = 'Module:Article history/config | + | local CONFIG_PAGE = 'Module:Article history/config' |
local WRAPPER_TEMPLATE = 'Template:Article history' | local WRAPPER_TEMPLATE = 'Template:Article history' | ||
local DEBUG_MODE = false -- If true, errors are not caught. | local DEBUG_MODE = false -- If true, errors are not caught. | ||
Line 115: | Line 115: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
− | -- | + | -- Row class |
+ | -- This class represents one row in the template. | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
− | local | + | local Row = {} |
− | + | Row.__index = Row | |
+ | addMixin(Row, Message) | ||
− | function | + | function Row.new(data) |
− | + | local obj = setmetatable({}, Row) | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | local obj = setmetatable({}, | ||
obj.cfg = data.cfg | obj.cfg = data.cfg | ||
obj.currentTitle = data.currentTitle | obj.currentTitle = data.currentTitle | ||
obj.isSmall = data.isSmall | obj.isSmall = data.isSmall | ||
− | obj.makeData = data.makeData -- used by | + | obj.makeData = data.makeData -- used by Row:getData |
return obj | return obj | ||
end | end | ||
− | function | + | function Row:_cachedTry(cacheKey, errorCacheKey, func) |
− | -- This method is for use in | + | -- 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 | -- 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 | -- processing. We also cache any errors found and abort if an error was | ||
Line 225: | Line 176: | ||
end | end | ||
− | function | + | function Row:getData(articleHistoryObj) |
return self:_cachedTry('_dataCache', '_isDataError', function () | return self:_cachedTry('_dataCache', '_isDataError', function () | ||
return self.makeData(articleHistoryObj) | return self.makeData(articleHistoryObj) | ||
Line 231: | Line 182: | ||
end | end | ||
− | function | + | function Row:setIconValues(icon, caption, size, smallSize) |
− | return self. | + | self.icon = icon |
+ | self.iconCaption = caption | ||
+ | self.iconSize = size | ||
+ | self.iconSmallSize = smallSize | ||
+ | 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() | ||
+ | if self.isSmall then | ||
+ | return self.iconSmallSize or self.cfg.defaultSmallIconSize or '15px' | ||
+ | else | ||
+ | return self.iconSize or self.cfg.defaultIconSize or '30px' | ||
+ | end | ||
+ | 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 | end | ||
− | function | + | function Row:exportNoticeBarIcon(articleHistoryObj) |
− | self | + | local icon = self:getNoticeBarIcon(articleHistoryObj) |
+ | if not icon then | ||
+ | return nil | ||
+ | end | ||
+ | return renderImage( | ||
+ | icon, | ||
+ | self:getNoticeBarIconCaption(articleHistoryObj), | ||
+ | self:getNoticeBarIconSize() | ||
+ | ) | ||
end | end | ||
− | function | + | function Row:setText(text) |
self.text = text | self.text = text | ||
end | end | ||
− | function | + | function Row:getText(articleHistoryObj) |
return maybeCallFunc(self.text, articleHistoryObj, self) | return maybeCallFunc(self.text, articleHistoryObj, self) | ||
end | end | ||
− | function | + | 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 | -- 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. | -- of strings or a function returning an array of category objects. | ||
Line 253: | Line 300: | ||
end | end | ||
− | function | + | function Row:getCategories(articleHistoryObj) |
local ret = {} | local ret = {} | ||
if type(self.categories) == 'table' then | if type(self.categories) == 'table' then | ||
Line 263: | Line 310: | ||
for _, categoryObj in ipairs(t) do | for _, categoryObj in ipairs(t) do | ||
ret[#ret + 1] = categoryObj | 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() | ||
+ | if self.isSmall then | ||
+ | return self.statusCfg.smallIconSize | ||
+ | or self.cfg.defaultSmallStatusIconSize | ||
+ | or '30px' | ||
+ | else | ||
+ | return self.iconSize | ||
+ | or self.statusCfg.iconSize | ||
+ | or self.cfg.defaultStatusIconSize | ||
+ | or '50px' | ||
+ | end | ||
+ | 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.defaultSmallStatusIconSize 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 | ||
end | end | ||
Line 274: | Line 452: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
− | local Notice = setmetatable({}, | + | local Notice = setmetatable({}, Row) |
Notice.__index = Notice | Notice.__index = Notice | ||
function Notice.new(data) | function Notice.new(data) | ||
− | local | + | local obj = Row.new(data) |
− | setmetatable( | + | setmetatable(obj, Notice) |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | obj:setIconValues( | |
− | + | data.icon, | |
− | + | data.iconCaption, | |
+ | data.iconSize, | ||
+ | data.iconSmallSize | ||
+ | ) | ||
+ | obj:setNoticeBarIconValues( | ||
+ | data.noticeBarIcon, | ||
+ | data.noticeBarIconCaption, | ||
+ | data.noticeBarIconSize | ||
+ | ) | ||
+ | obj:setText(data.text) | ||
+ | obj:setCategories(data.categories) | ||
− | + | return obj | |
− | |||
end | end | ||
Line 304: | Line 486: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
− | local Action = setmetatable({}, | + | local Action = setmetatable({}, Row) |
Action.__index = Action | Action.__index = Action | ||
function Action.new(data) | function Action.new(data) | ||
− | local | + | local obj = Row.new(data) |
− | setmetatable( | + | setmetatable(obj, Action) |
− | + | obj.paramNum = data.paramNum | |
-- Set the ID | -- Set the ID | ||
do | do | ||
if not data.code then | if not data.code then | ||
− | + | obj:raiseError( | |
− | + | obj:message('action-error-no-code', obj:getParameter('code')), | |
− | + | obj:message('action-error-no-code-help') | |
) | ) | ||
end | end | ||
local code = mw.ustring.upper(data.code) | local code = mw.ustring.upper(data.code) | ||
− | + | obj.id = obj.cfg.actions[code] and obj.cfg.actions[code].id | |
− | if not | + | if not obj.id then |
− | + | obj:raiseError( | |
− | + | obj:message( | |
'action-error-invalid-code', | 'action-error-invalid-code', | ||
data.code, | data.code, | ||
− | + | obj:getParameter('code') | |
), | ), | ||
− | + | obj:message('action-error-invalid-code-help') | |
) | ) | ||
end | end | ||
Line 336: | Line 518: | ||
-- Add a shortcut for this action's config. | -- Add a shortcut for this action's config. | ||
− | + | obj.actionCfg = obj.cfg.actions[obj.id] | |
-- Set the link | -- Set the link | ||
− | + | obj.link = data.link or obj.currentTitle.talkPageTitle.prefixedText | |
-- Set the result ID | -- Set the result ID | ||
Line 346: | Line 528: | ||
and mw.ustring.lower(data.resultCode) | and mw.ustring.lower(data.resultCode) | ||
or '_BLANK' | or '_BLANK' | ||
− | if | + | if obj.actionCfg.results[resultCode] then |
− | + | obj.resultId = obj.actionCfg.results[resultCode].id | |
elseif resultCode == '_BLANK' then | elseif resultCode == '_BLANK' then | ||
− | + | obj:raiseError( | |
− | + | obj:message( | |
'action-error-blank-result', | 'action-error-blank-result', | ||
− | + | obj.id, | |
− | + | obj:getParameter('resultCode') | |
), | ), | ||
− | + | obj:message('action-error-blank-result-help') | |
) | ) | ||
else | else | ||
− | + | obj:raiseError( | |
− | + | obj:message( | |
'action-error-invalid-result', | 'action-error-invalid-result', | ||
data.resultCode, | data.resultCode, | ||
− | + | obj.id, | |
− | + | obj:getParameter('resultCode') | |
), | ), | ||
− | + | obj:message('action-error-invalid-result-help') | |
) | ) | ||
end | end | ||
Line 375: | Line 557: | ||
lang.formatDate, | lang.formatDate, | ||
lang, | lang, | ||
− | + | obj:message('action-date-format'), | |
data.date | data.date | ||
) | ) | ||
if success and date then | if success and date then | ||
− | + | obj.date = date | |
else | else | ||
− | + | obj:addWarning( | |
− | + | obj:message( | |
'action-warning-invalid-date', | 'action-warning-invalid-date', | ||
data.date, | data.date, | ||
− | + | obj:getParameter('date') | |
), | ), | ||
− | + | obj:message('action-warning-invalid-date-help') | |
) | ) | ||
end | end | ||
else | else | ||
− | + | obj:addWarning( | |
− | + | obj:message( | |
'action-warning-no-date', | 'action-warning-no-date', | ||
− | + | obj.paramNum, | |
− | + | obj:getParameter('date'), | |
− | + | obj:getParameter('code') | |
), | ), | ||
− | + | obj:message('action-warning-no-date-help') | |
) | ) | ||
end | end | ||
− | + | obj.date = obj.date or obj:message('action-date-missing') | |
-- Set the oldid | -- Set the oldid | ||
− | + | obj.oldid = tonumber(data.oldid) | |
− | if data.oldid and (not | + | if data.oldid and (not obj.oldid or not isPositiveInteger(obj.oldid)) then |
− | + | obj.oldid = nil | |
− | + | obj:addWarning( | |
− | + | obj:message( | |
'action-warning-invalid-oldid', | 'action-warning-invalid-oldid', | ||
data.oldid, | data.oldid, | ||
− | + | obj:getParameter('oldid') | |
), | ), | ||
− | + | obj:message('action-warning-invalid-oldid-help') | |
) | ) | ||
end | end | ||
+ | |||
+ | -- Set the notice bar icon values | ||
+ | obj:setNoticeBarIconValues( | ||
+ | data.noticeBarIcon, | ||
+ | data.noticeBarIconCaption, | ||
+ | data.noticeBarIconSize | ||
+ | ) | ||
-- Set the categories | -- Set the categories | ||
− | + | obj:setCategories(obj.actionCfg.categories) | |
− | return | + | return obj |
end | end | ||
Line 450: | Line 639: | ||
self | 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 | end | ||
Line 458: | Line 686: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
− | local CollapsibleNotice = setmetatable({}, | + | local CollapsibleNotice = setmetatable({}, Row) |
CollapsibleNotice.__index = CollapsibleNotice | CollapsibleNotice.__index = CollapsibleNotice | ||
function CollapsibleNotice.new(data) | function CollapsibleNotice.new(data) | ||
− | local obj = | + | local obj = Row.new(data) |
setmetatable(obj, CollapsibleNotice) | setmetatable(obj, CollapsibleNotice) | ||
Line 501: | Line 729: | ||
or '20px' | or '20px' | ||
end | end | ||
+ | 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 | end | ||
Line 513: | Line 802: | ||
function ArticleHistory.new(args, cfg, currentTitle) | function ArticleHistory.new(args, cfg, currentTitle) | ||
− | local | + | local obj = setmetatable({}, ArticleHistory) |
-- Set input | -- Set input | ||
− | + | obj.args = args or {} | |
− | + | obj.currentTitle = currentTitle or mw.title.getCurrentTitle() | |
-- Set isSmall | -- Set isSmall | ||
− | + | obj.isSmall = yesno(obj.args.small) or false | |
-- Define object structure. | -- Define object structure. | ||
− | + | obj._errors = {} | |
− | + | obj._allObjectsCache = {} | |
− | + | ||
-- Format the config | -- Format the config | ||
local function substituteAliases(t, ret) | local function substituteAliases(t, ret) | ||
Line 552: | Line 841: | ||
return ret | return ret | ||
end | end | ||
− | + | obj.cfg = substituteAliases(cfg or require(CONFIG_PAGE)) | |
--[[ | --[[ | ||
Line 578: | Line 867: | ||
do | do | ||
local prefixArgs = {} | local prefixArgs = {} | ||
− | for k, v in pairs( | + | for k, v in pairs(obj.args) do |
if type(k) == 'string' then | if type(k) == 'string' then | ||
local prefix, num, suffix = k:match('^(.-)([1-9][0-9]*)(.*)$') | local prefix, num, suffix = k:match('^(.-)([1-9][0-9]*)(.*)$') | ||
Line 603: | Line 892: | ||
end | end | ||
end | end | ||
− | + | obj.prefixArgs = prefixArrays | |
end | end | ||
− | + | return obj | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | return | ||
end | end | ||
Line 697: | Line 931: | ||
local actions = {} | local actions = {} | ||
local suffixes = self.cfg.actionParamSuffixes | local suffixes = self.cfg.actionParamSuffixes | ||
− | for | + | for _, t in ipairs(actionArgs) do |
local objArgs = {} | local objArgs = {} | ||
for k, v in pairs(t) do | for k, v in pairs(t) do | ||
Line 734: | Line 968: | ||
end | end | ||
− | function ArticleHistory: | + | function ArticleHistory:getStatusObj() |
− | return self.statusId | + | -- 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, | ||
+ | isSmall = self.isSmall | ||
+ | } | ||
+ | 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 | end | ||
− | function ArticleHistory: | + | function ArticleHistory:getStatusId() |
− | return | + | local statusObj = self:getStatusObj() |
+ | return statusObj and statusObj.id | ||
end | end | ||
Line 749: | Line 1,024: | ||
end | end | ||
local ret = {} | local ret = {} | ||
− | for | + | for _, t in ipairs(self.cfg[configKey] or {}) do |
if t.isActive(self) then | if t.isActive(self) then | ||
local data = {} | local data = {} | ||
Line 784: | Line 1,059: | ||
if not ret then | if not ret then | ||
ret = {} | ret = {} | ||
+ | local statusObj = self:getStatusObj() | ||
+ | if statusObj then | ||
+ | ret[#ret + 1] = statusObj | ||
+ | end | ||
local objTables = { | local objTables = { | ||
− | |||
self:getNoticeObjects(), | self:getNoticeObjects(), | ||
self:getActionObjects(), | self:getActionObjects(), | ||
self:getCollapsibleNoticeObjects() | self:getCollapsibleNoticeObjects() | ||
} | } | ||
− | for | + | for _, t in ipairs(objTables) do |
− | for | + | for _, obj in ipairs(t) do |
ret[#ret + 1] = obj | ret[#ret + 1] = obj | ||
end | end | ||
Line 830: | Line 1,108: | ||
ret[#ret + 1] = msg | ret[#ret + 1] = msg | ||
end | end | ||
− | for | + | for _, obj in ipairs(self:getAllObjects(true)) do |
− | for | + | for _, msg in ipairs(obj:getWarnings()) do |
ret[#ret + 1] = msg | ret[#ret + 1] = msg | ||
end | end | ||
Line 852: | Line 1,130: | ||
if self:categoriesAreActive() then | if self:categoriesAreActive() then | ||
-- Child object categories | -- Child object categories | ||
− | for | + | for _, obj in ipairs(self:getAllObjects()) do |
local categories = self:try(obj.getCategories, obj, self) | local categories = self:try(obj.getCategories, obj, self) | ||
− | for | + | for _, categoryObj in ipairs(categories or {}) do |
ret[#ret + 1] = tostring(categoryObj) | ret[#ret + 1] = tostring(categoryObj) | ||
end | end | ||
end | end | ||
− | + | ||
-- Extra categories | -- Extra categories | ||
− | for | + | for _, func in ipairs(self.cfg.extraCategories or {}) do |
local cats = func(self) or {} | local cats = func(self) or {} | ||
− | for | + | for _, categoryObj in ipairs(cats) do |
ret[#ret + 1] = tostring(categoryObj) | ret[#ret + 1] = tostring(categoryObj) | ||
end | end | ||
Line 869: | Line 1,147: | ||
return table.concat(ret) | return table.concat(ret) | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
Line 1,011: | Line 1,157: | ||
if self.isSmall then | if self.isSmall then | ||
tableRoot:addClass('mbox-small') | tableRoot:addClass('mbox-small') | ||
− | |||
− | |||
end | end | ||
− | + | ||
− | -- Status | + | -- Status |
− | + | local statusObj = self:getStatusObj() | |
− | tableRoot:node(self:try( | + | if statusObj then |
+ | tableRoot:node(self:try(statusObj.exportHtml, statusObj, self)) | ||
end | end | ||
Line 1,023: | Line 1,168: | ||
local notices = self:getNoticeObjects() | local notices = self:getNoticeObjects() | ||
for _, noticeObj in ipairs(notices) do | for _, noticeObj in ipairs(notices) do | ||
− | tableRoot:node(self:try( | + | tableRoot:node(self:try(noticeObj.exportHtml, noticeObj, self)) |
end | end | ||
Line 1,033: | Line 1,178: | ||
local collapsibleNotices = self:getCollapsibleNoticeObjects() or {} | local collapsibleNotices = self:getCollapsibleNoticeObjects() or {} | ||
local collapsibleNoticeHtmlObjects, actionHtmlObjects = {}, {} | local collapsibleNoticeHtmlObjects, actionHtmlObjects = {}, {} | ||
− | for _, | + | for _, obj in ipairs(actions) do |
table.insert( | table.insert( | ||
actionHtmlObjects, | actionHtmlObjects, | ||
− | self:try( | + | self:try(obj.exportHtml, obj, self) |
) | ) | ||
end | end | ||
Line 1,042: | Line 1,187: | ||
table.insert( | table.insert( | ||
collapsibleNoticeHtmlObjects, | collapsibleNoticeHtmlObjects, | ||
− | self:try( | + | self:try(obj.exportHtml, obj, self, true) -- Render the collapsed version |
) | ) | ||
end | end | ||
local nActionRows = #actionHtmlObjects | local nActionRows = #actionHtmlObjects | ||
local nCollapsibleRows = nActionRows + #collapsibleNoticeHtmlObjects | local nCollapsibleRows = nActionRows + #collapsibleNoticeHtmlObjects | ||
− | + | ||
-- Find out if we are collapsed or not. | -- Find out if we are collapsed or not. | ||
local isCollapsed | local isCollapsed | ||
Line 1,065: | Line 1,210: | ||
table.insert( | table.insert( | ||
collapsibleNoticeHtmlObjects, | collapsibleNoticeHtmlObjects, | ||
− | self:try( | + | self:try(obj.exportHtml, obj, self, false) |
) | ) | ||
end | end | ||
Line 1,083: | Line 1,228: | ||
:tag('table') | :tag('table') | ||
:addClass('AH-milestones') | :addClass('AH-milestones') | ||
− | :addClass(isCollapsed and 'collapsible collapsed' or nil) | + | :addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil) |
:css('width', '100%') | :css('width', '100%') | ||
:css('background', 'transparent') | :css('background', 'transparent') | ||
Line 1,117: | Line 1,262: | ||
)) | )) | ||
end | end | ||
− | + | ||
-- Subheadings | -- Subheadings | ||
if nActionRows > 0 then | if nActionRows > 0 then | ||
Line 1,139: | Line 1,284: | ||
end | end | ||
end | end | ||
− | + | ||
-- Collapsible notices and current status | -- Collapsible notices and current status | ||
-- These are only included in the collapsible table if it is collapsed. | -- These are only included in the collapsible table if it is collapsed. | ||
Line 1,153: | Line 1,298: | ||
statusColspan = 2 | statusColspan = 2 | ||
end | end | ||
− | + | ||
-- Collapsible notices | -- Collapsible notices | ||
for _, obj in ipairs(collapsibleNotices) do | for _, obj in ipairs(collapsibleNotices) do | ||
− | tableNode:node(self:try( | + | tableNode:node(self:try(obj.exportHtml, obj, self, isCollapsed)) |
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | + | ||
-- Current status | -- Current status | ||
− | if | + | if statusObj and nActionRows > 1 then |
tableNode | tableNode | ||
:tag('tr') | :tag('tr') | ||
:tag('td') | :tag('td') | ||
:attr('colspan', statusColspan) | :attr('colspan', statusColspan) | ||
− | :wikitext(self:message('status-blurb', | + | :wikitext(self:message('status-blurb', statusObj.name)) |
end | end | ||
end | end | ||
Line 1,229: | Line 1,369: | ||
wrappers = WRAPPER_TEMPLATE | wrappers = WRAPPER_TEMPLATE | ||
}) | }) | ||
+ | if frame:getTitle():find('sandbox', 1, true) then | ||
+ | CONFIG_PAGE = CONFIG_PAGE .. '/sandbox' | ||
+ | end | ||
return p._main(args) | return p._main(args) | ||
end | end | ||
Line 1,235: | Line 1,378: | ||
return { | return { | ||
Message = Message, | Message = Message, | ||
− | + | Row = Row, | |
Status = Status, | Status = Status, | ||
MultiStatus = MultiStatus, | MultiStatus = MultiStatus, |
Latest revision as of 13:12, 26 September 2020
Documentation for this module may be created at Module:Article history/sandbox/doc
-------------------------------------------------------------------------------
-- 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('Module:No globals')
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('[[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
-------------------------------------------------------------------------------
-- 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.isSmall = data.isSmall
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, smallSize)
self.icon = icon
self.iconCaption = caption
self.iconSize = size
self.iconSmallSize = smallSize
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()
if self.isSmall then
return self.iconSmallSize or self.cfg.defaultSmallIconSize or '15px'
else
return self.iconSize or self.cfg.defaultIconSize or '30px'
end
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()
if self.isSmall then
return self.statusCfg.smallIconSize
or self.cfg.defaultSmallStatusIconSize
or '30px'
else
return self.iconSize
or self.statusCfg.iconSize
or self.cfg.defaultStatusIconSize
or '50px'
end
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.defaultSmallStatusIconSize 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,
data.iconSmallSize
)
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,
data.iconSmallSize
)
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()
if self.isSmall then
return self.iconSmallSize
or self.cfg.defaultSmallCollapsibleNoticeIconSize
or '15px'
else
return self.iconSize
or self.cfg.defaultCollapsibleNoticeIconSize
or '20px'
end
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()
-- Set isSmall
obj.isSmall = yesno(obj.args.small) or false
-- 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,
isSmall = self.isSmall
}
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
data.isSmall = self.isSmall
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('tmbox tmbox-notice')
if self.isSmall then
tableRoot:addClass('mbox-small')
end
-- 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
if self.cfg.uncollapsedRows == 'all' then
isCollapsed = false
elseif nCollapsibleRows == 1 then
isCollapsed = false
else
isCollapsed = nCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3)
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('AH-milestones')
:addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil)
:css('width', '100%')
:css('background', 'transparent')
:css('font-size', '90%')
if nCollapsibleRows > 1 then
-- 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
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)
return 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