Module:Protection banner
From Timelines
Revision as of 07:25, 6 June 2014 by Mr. Stradivarius (talk) (give up on the OOP approach, lay out the main module structure in more detail, and move the config to Module:Protection banner/config)
Documentation for this module may be created at Module:Protection banner/doc
-- This module implements {{pp-meta}} and its daughter templates such as -- {{pp-dispute}}, {{pp-vandalism}} and {{pp-sock}}. -- Initialise necessary modules. local mArguments = require('Module:Arguments') local mProtectionLevel = require('Module:Effective protection level')._main local mFileLink = require('Module:File link') local yesno = require('Module:Yesno') local mMessageBox -- only needs to be loaded if we are outputting a banner, so lazily initialise -- Load config local cfg = mw.loadData('Module:Protection banner/config') local p = {} -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function validateField(tName, t, field, expectType, nilOk) local val = t[field] local valType = type(val) if not (valType == expectType or nilOk and valType == 'nil') then error(string.format( "type error in '%s' field '%s' (%s%s expected, got %s)", tName, tostring(field), expectType, nilOk and ' or nil' or '', valType ), 2) end end local function toTableEnd(t, pos) -- Sends the value at position pos to the end of array t, and shifts the -- other items down accordingly. return table.insert(t, table.remove(t, pos)) end -------------------------------------------------------------------------------- -- Main functions -------------------------------------------------------------------------------- function p.main(frame) local args = mArguments.getArgs(frame) return p._main(args) end function p._main(args) local protectionLevel = p.getProtectionLevel(args) local isSmall = yesno(args.small) or false local bannerConfig = p.getBannerConfig(protectionLevel, args) local ret = '' if isSmall then ret = ret .. p.makePadlock(protectionLevel, args, bannerConfig) else ret = ret .. p.makeBanner(protectionLevel, args, bannerConfig) end ret = ret .. p.getProtectionCategory(protectionLevel, args) ret = ret .. p.getTrackingCategories(protectionLevel, args) return ret end -------------------------------------------------------------------------------- -- Protection functions -------------------------------------------------------------------------------- function p.getProtectionLevel(args) local title if args.page then title = mw.title.new(args.page) else title = mw.title.getCurrentTitle() end local protectionData = p.getProtectionData(title) protectionLevel = protectionData[args.action or 'edit'] return protectionLevel or '*' end function p.getProtectionData(title) -- Gets a table containing protection data for the given title. The data -- is cached using a metatable, and so can be indexed as needed without -- a performance loss. local protectionData = {} local actions = { create = true, edit = true, move = true, autoreview = true } setmetatable(protectionData, { __index = function (t, key) local level if actions[key] then level = mProtectionLevel(key, title) if level == 'accountcreator' then -- Lump titleblacklisted pages in with template-protected pages, -- since templateeditors can do both. level = 'templateeditor' end end protectionData[key] = level return level end }) return protectionData end -------------------------------------------------------------------------------- -- Banner config functions -------------------------------------------------------------------------------- function p.getBannerConfig(protectionLevel, args) end -------------------------------------------------------------------------------- -- Padlock functions -------------------------------------------------------------------------------- function p.makePadlock(protectionLevel, args, bannerConfig) local data = p.makePadlockData(protectionLevel, args, bannerConfig) return p.renderPadlock(data) end function p.makePadlockData(protectionLevel, args, bannerConfig) end function p.renderPadlock(data) data = data or {} local image = mFileLink.new(data.filename or 'Transparent.gif') :width(20) :link(data.link) :alt(data.alt) :render() local root = mw.html.create('div') root :addClass('metadata topicon nopopups') :attr('id', 'protected-icon') :css{display = 'none', right = data.right or '55px'} :wikitext(image) return tostring(root) end -------------------------------------------------------------------------------- -- Banner functions -------------------------------------------------------------------------------- function p.makeBanner(protectionLevel, args, bannerConfig) local data = p.makeBannerData(protectionLevel, args, bannerConfig) return p.renderBanner(data) end function p.makeBannerData(protectionLevel, args, bannerConfig) end function p.renderBanner(data) data = data or {} local image = mFileLink.new(data.filename) :width(40) :caption(data.mouseover) :render() local mbargs = { page = data.page, type = 'protection', image = image, text = string.format( "'''%s'''%s", data.reasonText, data.explanationText and '<br />' .. data.explanationText or '' ) } return mMessageBox.main('mbox', mbargs) end -------------------------------------------------------------------------------- -- Protection category functions -------------------------------------------------------------------------------- function p.getProtectionCategory(protectionLevel, args) end function p.getPagetype(ns) -- Returns a string with the page's type. Takes a namespace number as input. local pagetype = pagetypeNamespaces[ns] or pagetypeNamespaces.default if not pagetype then error('the page type could not be found; please define a name for the key "default"') end return pagetype end function p.matchNamespace(ns) -- Matches a namespace number to a string that can be passed to the -- namespace parameter of p.getCategoryName. if not ns or type(ns) ~= 'number' then return nil end local nskey = cfg.categoryNamespaces[ns] if not nskey and ns % 2 == 1 then nskey = 'talk' end return nskey end function p.getCategoryName(cats, action, level, namespace, reason, expiry) --[[ -- Gets a category name from the category table, given a combination of -- the protection type, the protection level, the namespace number, the -- reason for protection, and the expiry date. --]] cats = cats or cfg.categories --[[ -- Define the properties table. Each property is a table containing the -- canonical order that the property is tested in, the position the -- property has in the category key strings, and the property value itself. --]] local properties = { expiry = {order = 1, keypos = 5, val = expiry}, namespace = {order = 2, keypos = 3, val = p.matchNamespace(namespace)}, reason = {order = 3, keypos = 4, val = reason}, level = {order = 4, keypos = 2, val = level}, action = {order = 5, keypos = 1, val = action} } --[[ -- Load the category order configuration for the reason specified. -- The configuration is stored in the categoryOrder field of each reason -- subtable of cfg.reasons. If the value is a table, then the order is the -- values specified in the table. If the value is a string, then the -- property corresponding to that string is tested last (i.e. it is the most -- important, because it keeps its specified value the longest) and the -- other properties are tested in the canonical order. If the value is of -- any other type then the canonical order is used. --]] local reasonTable = reason and cfg.reasons[reason] local categoryOrder = reasonTable and reasonTable.categoryOrder local categoryOrderType = type(categoryOrder) local configOrder = {} if categoryOrderType == 'table' then local dupes = {} for i = 1, 5 do local propertiesKey = categoryOrder[i] if not propertiesKey then local msg = 'no entry found for key ' .. i .. ' in the cfg.reasons.' .. reason .. '.categoryOrder table' error(msg) end local property = properties[propertiesKey] if not property then local msg = 'invalid value "' .. propertiesKey .. '" detected in the cfg.reasons.' .. reason .. '.categoryOrder table' error(msg) end if dupes[propertiesKey] then local msg = 'duplicate values "' .. propertiesKey .. '" detected in the cfg.reasons.' .. reason .. '.categoryOrder table' error(msg) else dupes[propertiesKey] = true end configOrder[i] = property end else for propertiesKey, t in pairs(properties) do configOrder[t.order] = t end if categoryOrderType == 'string' then local property = properties[categoryOrder] if not property then local msg = '"' .. categoryOrder .. '" is not a valid value of cfg.reasons.' .. reason .. '.categoryOrder' error(msg) end toTableEnd(configOrder, property.order) end end --[[ -- Define the attempt order. Properties with no value defined are moved -- to the end, where they will later be given the value "all". This is -- to cut down on the number of table lookups in the cats table, which -- grows exponentially with the number of properties with valid values. -- We keep track of the number of active properties with the noActive -- parameter. --]] local active, inactive = {}, {} for i, t in ipairs(configOrder) do if t.val then active[#active + 1] = t else inactive[#inactive + 1] = t end end local noActive = #active local attemptOrder = active for i, t in ipairs(inactive) do attemptOrder[#attemptOrder + 1] = t end --[[ -- Check increasingly generic key combinations until we find a match. -- If a specific category exists for the combination of properties -- we are given, that match will be found first. If not, we keep -- trying different key combinations until we match using the key -- "all-all-all-all-all". -- -- To generate the keys, we index the property subtables using a -- binary matrix with indexes i and j. j is only calculated up to -- the number of active properties. For example, if there were three -- active properties, the matrix would look like this, with 0 -- corresponding to the string "all", and 1 corresponding to the -- val field in the property table: -- -- j 1 2 3 -- i -- 1 1 1 1 -- 2 0 1 1 -- 3 1 0 1 -- 4 0 0 1 -- 5 1 1 0 -- 6 0 1 0 -- 7 1 0 0 -- 8 0 0 0 -- -- Values of j higher than the number of active properties are set -- to the string "all". -- -- A key for the category table is constructed for each value of i. -- The correct position of the value in the key is determined by the -- pos field in the property table. --]] for i = 1, 2^noActive do local key = {} for j, t in ipairs(attemptOrder) do if j > noActive then key[t.keypos] = 'all' else local quotient = i / 2 ^ (j - 1) quotient = math.ceil(quotient) if quotient % 2 == 1 then key[t.keypos] = t.val else key[t.keypos] = 'all' end end end key = table.concat(key, '-') local attempt = cats[key] if attempt then return attempt end end error( 'No category match found;' .. ' please define the category for key "all-all-all-all-all"' ) end -------------------------------------------------------------------------------- -- Tracking category functions -------------------------------------------------------------------------------- function p.getTrackingCategories(protectionLevel, args) end --[[ When to add "Category:Wikipedia pages with incorrect protection templates": * If it uses a type which is incompatible with the actual protection level of a page, or * If an expiry date is set and is in the past When to add "Category:Wikipedia protected pages without expiry": * No expiry is set, and * Expiry can be set, and * The protection type is not "move" (could change this?), and * The reason doesn't have "indefinitely protected" categories (pp-blp, pp-semi-indef and pp-move-indef have these), and * The reason is not the generic pp-protected (could change this?) (This leaves reasons "dispute", "vandalism", "usertalk", and "sock") ]] return p