local p = local getArgs = require('Module:Arguments').getArgslocal yesno = require('Module:Yesno')local rawData = mw.loadData('Module:Sanctions/data/sandbox')local data = rawData.sanctionslocal commData = rawData.commlocal arbcomData = rawData.arbcom
local messageBox = require('Module:Message box')
-- Functions
local function tableContainsValue(needle, haystack) for _, v in pairs(haystack) do if v
local function _getTopicData(topicAlias) if data[topicAlias] then return data[topicAlias] else return false endend
-- Returns an invalid topic error, with a table of acceptable topicslocal function syntaxHelp(frame) return '
not specified. Available options:' .. p.table(frame) .. '
'end
-- Topic class
local Topic = Topic.__index = Topic
function Topic.new(topicCode, args) local obj = obj._args = args obj._topicData = _getTopicData(topicCode) return setmetatable(obj, Topic)end
function Topic:get(arg) return self._topicData[arg]end
function Topic:exists return self._topicData and trueend
function Topic:hasGlobalRestriction(type) return self._topicData.restrictions[type]end
function Topic:hasLocalRestriction(type) return yesno(self._args[type]) or falseend
function Topic:hasRestriction(type) return self:hasGlobalRestriction(type) or self:hasLocalRestriction(type)end
function Topic:hasRestrictions(arr) for _, v in ipairs(arr) do if self:hasRestriction(v) then return true end end return falseend
function Topic:hasAnyRevertRestrictions return self:hasRestrictionsend
function Topic:hasAnyRestrictions return self:hasAnyRevertRestrictions or self:hasRestrictionsend
function Topic:getType return (self:get('type')
"both" and 'Arbitration Committee and community' or 'community'))end
function Topic:getTypeUnlinked return (self:get('type')
"both" and 'arbitration and community' or 'community'))end
function Topic:getCustomRestrictions local customRestrictions = local ri = 1 local checkArr = self._topicData.restrictions local breakNext = false while true do if checkArr['restriction'..ri] then table.insert(customRestrictions, checkArr['restriction'..ri]) ri = ri + 1 elseif breakNext then break else ri = 1 checkArr = self._args breakNext = true end end return customRestrictionsend
-- End classes
local function formatRestrictions(topic, you, article) local restrictionList = mw.html.create('ul') if (you) then if topic:hasRestriction('1rr') then restrictionList:tag('li'):wikitext('You are not allowed to make more than 1 revert within 24 hours on a page within this topic, subject to the usual exemptions.') end if topic:hasRestriction('0rr') then restrictionList :tag('li') :wikitext("You are not allowed to make any reverts within this topic, subject to the usual exemptions.") end if topic:hasRestriction('protection') then local protectionRestriction = local shortCode = topic:get('restrictions')['protection'] if shortCode
"semi" then protectionRestriction = "logged in, have made 10 edits, and an account age of 4 days" elseif shortCode
"ecp" then protectionRestriction = "Extended confirmed requirement: Editors must be logged in, have made 500 edits, and an account age of 30 days" elseif shortCode
"user" then protectionRestriction = "Logged in requirement: Editors must be logged in" end restrictionList :tag('li'):wikitext(protectionRestriction .. " in order to make edits " .. (article and " to this article" or " within this topic area") .. ".") end -- Text for boilerplate/predefined restrictions if topic:hasRestriction('consensusrequired') then restrictionList :tag('li') :wikitext("Consensus required: All editors must obtain consensus on the talk page of " .. (article and "this article" or "articles within this topic") .. " before reinstating any edits that have been challenged (via reversion). This includes making edits similar to the ones that have been challenged. If in doubt, do not make the edit.") end if topic:hasRestriction('brd') then restrictionList :tag('li') :wikitext("24-hr BRD cycle: If a change you make to " .. (article and "this article" or "an article within this topic") .." is reverted, you may not reinstate that change unless you discuss the issue on the talk page and wait 24 hours (from the time of the original edit). Partial reverts/reinstatements that reasonably address objections of other editors are preferable to wholesale reverts.") end local customRestrictions = topic:getCustomRestrictions for _, v in ipairs(customRestrictions) do restrictionList :tag('li') :wikitext(v) end end return restrictionListend-- This function builds a talk notice-- TODO: split this up---- @param frame-- @param topic topic class instance-- @param args arguments passed to wrapper template-- @returns String representation of noticelocal function buildTalkNotice(frame, topic, args) local type = args['type'] or args[2] or 'mini' local out = mw.html.create() local hasRestrictions = topic:hasAnyRestrictions local hasRevertRestrictions = topic:hasAnyRevertRestrictions if hasRestrictions then type = 'long' -- force long displaywhere custom restrictions are applicable
out :tag('span') :css('font-size', '120%') :wikitext("WARNING: ACTIVE " .. string.upper(topic:getTypeUnlinked) .. " SANCTIONS") end
if hasRestrictions then out :tag('p') :wikitext("The article, along with other pages relating to "..topic:get('scope')..", is designated by the " .. topic:getType .. " as a contentious topic. The current restrictions are:") else out :tag('p') :wikitext("The use of the contentious topics procedure has been authorized by the " .. topic:getType .. " for pages related to ".. topic:get('scope') ..", including this page." .. (type
if type ~= 'mini' then local restrictionList = formatRestrictions(topic, false, true) if hasRestrictions then out:node(restrictionList) end
out :tag('p') :wikitext("Editors who repeatedly or seriously fail to adhere to the purpose of Wikipedia, any expected standards of behaviour, or any normal editorial process may be sanctioned.") -- Further info box if hasRestrictions then local furtherInfo = mw.html.create()
-- Enforcement procedures furtherInfo :tag('p') :wikitext('Enforcement procedures:') :done
local enforcementProcedures = mw.html.create('ul')
if hasRestrictions then enforcementProcedures :tag('li') :wikitext("Violations of any restrictions " .. (hasRevertRestrictions and "(excluding 1RR/reverting violations) " or "") .. "and other conduct issues should be reported to the " .. (topic:get('type')
"arbcom" and "arbitration enforcement noticeboard" or "administrators' incidents noticeboard") .. ".") :done end
enforcementProcedures :tag('li') :wikitext("An editor must be aware before they can be sanctioned.") :allDone furtherInfo:node(enforcementProcedures)
if hasRevertRestrictions then furtherInfo :tag('p') :wikitext("With respect to any reverting restrictions:") :done :tag('ul') :tag('li') :wikitext("Edits made solely to enforce any clearly established consensus are exempt from all edit-warring restrictions. In order to be considered \"clearly established\" the consensus must be proven by prior talk-page discussion.") :done :tag('li') :wikitext("Edits made which remove or otherwise change any material placed by clearly established consensus, without first obtaining consensus to do so, may be treated in the same manner as clear vandalism.") :done :tag('li') :wikitext("Clear vandalism of any origin may be reverted without restriction.") :done :tag('li') :wikitext("Reverts of edits made by anonymous (IP) editors that are not vandalism are exempt from the 1RR but are subject to the usual rules on edit warring. If you are in doubt, contact an administrator for assistance.") :allDone :tag('p') :wikitext("If you are unsure if your edit is appropriate, discuss it here on this talk page first. Remember: When in doubt, don't revert!") end
local collapsed = frame:expandTemplate out :newline :node(collapsed) end -- End further info box end
local box = messageBox.main('tmbox',)
return boxend
-- Builds an intro alert notice---- @param frame-- @param topic topic class instance-- @returns String representation of noticelocal function buildFirstAlert(frame, topic, sig) local out = mw.html.create('table') :addClass('gs-alert') :cssText("border: 1px solid #AAA; background-color: #E5F8FF; padding: 0.5em; width: 100%; margin-bottom: 1em")
local insideNode = out :tag('tr') :tag('td') :cssText("vertical-align:middle; padding-left:1px; padding-right:0.5em;") :wikitext("") :done :tag('td') insideNode :tag('p'):wikitext("You have recently edited a page related to " .. topic:get('scope') .. ", a topic designated as contentious by the " .. topic:getType .. "."):done :tag('p'):wikitext("A special set of rules applies to certain topic areas, which are referred to as contentious topics. These are specially designated topics that have been identified by the community or the Arbitration Committee to attract more persistent disruptive editing than the rest of the project. When editing a contentious topic, Wikipedia’s norms and policies are more strictly enforced, and Wikipedia administrators have special powers in order to reduce disruption to the project."):done :tag('p'):wikitext("Within contentious topics, editors should edit carefully and constructively, refrain from disrupting the encyclopedia, and:"):done :tag('ul') :tag('li'):wikitext('adhere to the purposes of Wikipedia;'):done :tag('li'):wikitext('comply with all applicable policies and guidelines;'):done :tag('li'):wikitext('follow editorial and behavioural best practice;'):done :tag('li'):wikitext('comply with any page restrictions in force within the area of conflict; and'):done :tag('li'):wikitext('refrain from gaming the system.'):done :done if (topic:hasAnyRestrictions) then insideNode :tag('p'):wikitext("Additionally, you must comply with the following restrictions:") local restrictionList = formatRestrictions(topic, true, false) insideNode:wikitext(tostring(restrictionList)):done end insideNode :tag('p'):wikitext("Editors are advised to err on the side of caution if unsure whether making a particular edit is consistent with these expectations. If you have any questions about contentious topics procedures you may ask them at the arbitration clerks' noticeboard or you may learn more about this contentious topic at . You may also choose to note which contentious topics you know about by using the template."):done return frame:preprocess('
\n' .. tostring(out) .. ' ')end
-- Builds an alert notice---- @param frame-- @param topic topic class instance-- @returns String representation of noticelocal function buildAlert(frame, topic, sig) local out = out = out .. ' You have recently made edits related to ' .. topic:get('scope') .. '. This is a standard message to inform you that ' .. topic:get('scope') .. ' is a' .. (topic:get('type')
"both" and ' and community' or )) .. ' designated contentious topic. This message does not imply that there are any issues with your editing. ' if (topic:hasAnyRestrictions) then out = out .. "You must comply with the following restrictions within this topic area:" local restrictionList = formatRestrictions(topic, true, false) out = out .. tostring(restrictionList) .. '\n\n' end out = out .. 'For more information about the contentious topics system, please see .' return frame:preprocess(tostring(out) .. ' ')end
-- Builds an edit noticelocal function buildEditNotice(frame, topic, args) local enHeader = 'This page relates to '.. topic:get('scope') .. ', which has been designated by the ' .. topic:getType .. ' as a contentious topic.' local enText = mw.html.create() enText:tag('p'):wikitext('While editing this topic, you must adhere to the purpose of Wikipedia, expected standards of behaviour, and Wikipedia\'s policies and norms.'):done if topic:hasAnyRestrictions then local list = formatRestrictions(topic, true, true) enText:tag('p'):wikitext("In addition, while editing, you must follow these restrictions: "):done enText:wikitext(tostring(list)) end
enText :tag('p') :wikitext("Failure to do so may result in a block or other sanctions. Please edit carefully.") :done
local editnotice = frame:expandTemplate
return editnoticeend
local function buildAwarenessNotice(frame, topics, args) local out = 'This user is aware of the designation of the following topics as contentious topics:\n' for k,v in pairs(topics) do out = out .. '* ' .. v:get('scope') .. '\n' end out = out .. "They should not be given alerts for those areas." return messageBox.main('mbox',)end
-- Builds a sanction alert notice---- @param frame-- @param topic topic class instance-- @returns String representation of noticelocal function buildSanctionAlert(frame, type, topic, sanction, rationale, decision, sig) local out = mw.html.create('table') :addClass('gs-alert') :cssText("margin-bottom: 1em; border: 1px solid #AAA; background-color: ivory; padding: 0.5em; display: flex; align-items: center;")
local insideNode = out :tag('tr') :tag('td') :cssText("vertical-align:middle; padding-left:1px; padding-right:0.5em;") :wikitext("") :done :tag('td') insideNode :tag('p'):wikitext("The following sanction has been applied to you: "):done :wikitext(frame:expandTemplate) :tag('p'):wikitext("You have been sanctioned " .. (rationale or "
no reason given")):done :tag('p'):wikitext("This sanction is imposed in my capacity as an uninvolved administrator under the authority of " .. (type"both" and " and the community" or "")) .. " at " .. (topic ~= nil and "" or (decision or "
no decision link provided")) .. (topic ~= nil and ", and the contentious topics procedure." or ".") .. " This sanction has been recorded in the log of sanctions for that topic. If the sanction includes a ban, please read the banning policy to ensure you understand what this means. If you do not comply with this sanction, you may be blocked for an extended period, by way of enforcement of this sanction—and you may also be made subject to further sanctions."):done :tag('p'):wikitext("You may appeal this sanction by " .. (topic ~= nil and "following the instructions here" or "following the instructions here") .. ". Even if you appeal this sanction, you remain bound by it until you are notified by an uninvolved administrator that the appeal has been successful. You are also free to contact me on my talk page if anything of the above is unclear to you."):done return frame:preprocess('\n' .. tostring(out) .. '\n ')end
local function listToText(frame, t) local new = local t = require('Module:TableTools').compressSparseArray(t) for i,v in ipairs(t) do table.insert(new, frame:expandTemplate) end return '\n*'..table.concat(new, '\n*')end
--/////////---- EXPORTS--/////////--
function p.detect(frame) local title local args = getArgs(frame) if args.testTitle then title = mw.title.new(args.testTitle) else title = mw.title.getCurrentTitle end local content = title:getContent or local codes = string.match(content, "}") local shortcutCodes = string.match(content, "}") local dsCodes = string.match(content, "}") if (not codes) and (not dsCodes) and (not shortcutCodes) then return end local text if (codes) then text = listToText(frame, mw.text.split(codes, "|")) elseif (dsCodes) then text = listToText(frame, mw.text.split(dsCodes, "|")) else text = listToText(frame, mw.text.split(shortcutCodes, "|")) end return frame:preprocess("
It is not necessary to notify this user about the following topics being contentious topics:" ..text.. "\n The user has indicated that they are already aware using the template {{Contentious topics/aware}} on their talk page.
" )end
-- Returns awareness noticefunction p.awarenessNotice(frame) local args = getArgs(frame)
local topics = for k,v in ipairs(args) do topics[k] = Topic.new(v, args) if not topics[k]:exists then return frame:preprocess('Invalid topic specified at argument ' .. k .. '. \n' .. p.table(frame)) end end return buildAwarenessNotice(frame, topics, args)end
-- Returns a talk notice-- For documentation, see function p.talknotice(frame) local args = getArgs(frame)
local topic = Topic.new(args['topic'] or args[1], args)
if not topic:exists then return frame:preprocess(syntaxHelp(frame)) end return buildTalkNotice(frame, topic, args)end
-- Returns an alert-- For documentation, see function p.alert(frame) local args = getArgs(frame)
local topic = Topic.new(args['topic'] or args[1], args) if not topic:exists then return frame:preprocess(syntaxHelp(frame)) elseif not topic:hasRestriction('ds') then return frame:preprocess('
This topic area is not designated as a contentious topic. Alert is not required.') end return buildAlert(frame, topic, args['sig'])endfunction p.alertFirst(frame) local args = getArgs(frame)
local topic = Topic.new(args['topic'] or args[1], args) if not topic:exists then return frame:preprocess(syntaxHelp(frame)) elseif not topic:hasRestriction('ds') then return frame:preprocess('
This topic area is not designated as a contentious topic. Alert is not required.') end return buildFirstAlert(frame, topic, args['sig'])endfunction p.sanction(frame) local args = getArgs(frame)
local topic = Topic.new(args['topic'] or args[1], args) return buildSanctionAlert(frame, (topic:exists and topic:get('type') or args['type']), (topic:exists and topic or nil), args['sanction'], args['rationale'], args['decision'], args['sig'])end
-- Returns an edit notice-- For documentation, see function p.editnotice(frame) local args = getArgs(frame)
local topic = Topic.new(args['topic'] or args[1], args) if not topic:exists then return frame:preprocess(syntaxHelp(frame)) end
return buildEditNotice(frame, topic, args)end
function p.generatePage(frame) local args = getArgs(frame) local topic = Topic.new(args['topic'], args) local out = mw.html.create() out:wikitext(messageBox.main('ombox',)):done if (yesno(args['rescinded']) or false) then out:tag('p'):wikitext('This topic previously had enacted remedies that applied to all editors who made edits to this topic area ("the Contentious Topic"). They have since been removed as a contentious topic.'):allDone return tostring(out) else if not topic:exists then error("Topic does not exist, use rescinded parameter for former contentious topics") end out:tag('p'):wikitext('The ' .. topic:getType .. ' has enacted remedies that apply to all editors who make edits related to ' .. topic:get("scope") .. ' ("the Contentious Topic"). The contentious topic procedure applies to all pages and edits related to this topic.'):done end out:tag('h2'):wikitext("Decisions"):done out:wikitext((args['decisions'] or )):done out:tag('h2'):wikitext("Guidance for administrators"):allDone out:wikitext((args['guidance'] or )):done out:tag('h2'):wikitext("Standard set of restrictions"):allDone out:tag("p"):wikitext("Any uninvolved administrator may impose the following set of restrictions for up to one year: "):allDone out:tag('ul') :tag("li"):wikitext("Individual restrictions") :tag("ul") :tag("li"):wikitext("sitewide and partial blocks"):done :tag("li"):wikitext("topic bans and page bans (from the entire contentious topic, a subtopic, or specified pages within the topic),"):done :tag("li"):wikitext("interaction bans,"):done :tag("li"):wikitext("revert restrictions"):done :done :done :tag("li"):wikitext("Page restrictions") :tag("ul") :tag("li"):wikitext("page protection,"):done :tag("li"):wikitext("revert restrictions,"):done :tag("li"):wikitext('the "consensus required" restriction,'):done :tag("li"):wikitext('the "enforced BRD" restriction'):done :done :done :allDone local restrictionList = formatRestrictions(topic, false, false) local hasRestrictions = topic:hasAnyRestrictions local hasRevertRestrictions = topic:hasAnyRevertRestrictions if hasRestrictions then out:tag("p"):wikitext("The following restrictions apply to the whole topic area: "):done out:node(restrictionList) end out:tag('h2'):wikitext("Templates"):allDone out:tag("p"):wikitext("When alerting an editor who has never received an alert for any contentious topic, the following template must be used to alert them: " .. frame:preprocess("")):done out:tag("p"):wikitext("In addition, the following must be used as an editnotice: " .. frame:preprocess("")):done out:tag("p"):wikitext("And the following must be used as a talk notice: " .. frame:preprocess("")):done return tostring(out)end
function p.table(frame) local args = getArgs(frame) local type = args['type'] local tbl = mw.html.create('table') :addClass('wikitable'):addClass('sortable') :css('font-size', '9pt') :css('background', 'transparent') if (type ~= "comm" and type ~= "arbcom" and type ~= nil) then return '
Invalid sanction type specified.' end local topRow = tbl:tag('tr') -- Headers topRow:tag('th') :wikitext("Topic code") :done topRow:tag('th') :wikitext("Area of conflict") :done if (type ~= "comm" and type ~= "arbcom") then topRow:tag('th') :wikitext("Designated by") :done end topRow:tag('th') :wikitext("Relevant information") :done topRow:tag('th') :wikitext("Relevant decision") :allDone -- sort alphabetically local sortedTable = if type"arbcom" then for n in pairs(arbcomData) do table.insert(sortedTable, n) end else for n in pairs(data) do table.insert(sortedTable, n) end end table.sort(sortedTable)
local added = for _,v in ipairs(sortedTable) do local sanction = data[v] local wt = (function(sanction, v) for l,w in ipairs(sanction.aliases or) do if (w
" .. v .. "
" for l,w in ipairs(sanction.aliases or) do if (not added[w]) then out = out .. ' ' .. "" .. w .. "
" added[w] = true end end return out end)(sanction, v) if wt ~= nil then if (not added[v]) then tbl:tag('tr') :tag('td') :wikitext(wt) :done :tag('td') :wikitext(sanction.scope) :done :tag('td') :wikitext(sanction.type"comm" and "the community" or "the Arbitration Committee" .. (sanction.type
return tostring(tbl)end
function p.topicsHelper(frame) local args = getArgs(frame)
if args['sanctions scope'] and data[args['sanctions scope']] then return _getTopicData(args['sanctions scope']).scope elseif args['sanctions link'] and data[args['sanctions link']] then return mw.title.new(_getTopicData(args['sanctions link']).wikilink).redirectTarget else return "" -- ? endend
-- Returns true if the given topic name is a valid topic areafunction p.checkIfValidTopic(topicName) local topic = Topic.new(topicName, nil) return topic:existsend
-- for debuggingp._Topic = Topic
return p