Module:Rfx
![]() | This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
![]() | This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
This is a library for getting information about individual requests for adminship (RfA) and requests for bureaucratship (RfB) pages on the English Wikipedia. It is not meant to be used directly from wiki pages, but rather to be used by other Lua modules.
Creating new objects
First of all, the library must be loaded, like this:
local rfx = require( 'Module:Rfx' )
Once the library is loaded, you can make a new rfx object using rfx.new()
. Caution - this function is expensive (see below).
rfx.new()
is used like this:
local myRfx = rfx.new( pagename )
The pagename
variable should be the name of a valid RfA or RfB page, for example:
local exampleRfa = rfx.new( 'Wikipedia:Requests for adminship/Example' )
If pagename
is not specified, or the page is not a subpage of Wikipedia:Requests for adminship or Wikipedia:Requests for bureaucratship, then rfx.new
will return nil
.
Methods and properties
Once you have created a new rfx
object, there are a number of methods and properties that you can use. They are all read-only.
- Properties
type
: the type of the rfx. This is either "rfa
" or "rfb
".supports
: the number of supports in the RfX.nil
if the supports could not be processed.opposes
: the number of opposes in the RfX.nil
if the opposes could not be processed.neutrals
: the number of neutrals in the RfX.nil
if the neutrals could not be processed.percent
: the support percentage. Calculated by and rounded to the nearest integer.nil
if it could not be processed.endTime
: the end time of the RfX. This is a string value taken from the RfX page.nil
if it could not be found.user
: the username of the RfX candidate.nil
if it could not be found.
- Methods
Methods must be called with the colon syntax:
local titleObject = exampleRfa:getTitleObject()
getTitleObject()
: gets the title object for the RfX page. See the reference manual for details on how to use title objects.getSupportUsers()
: gets an array containing the usernames that supported the RfX. If any usernames could not be processed, the text "Error parsing signature" is used instead, along with the text of the comment in question. N.b. this technique relies on the text of comment text being unique - if it is not unique thendupesExist()
will treat the identical comments as duplicate votes. If the page content could not be parsed at all, this method returnsnil
.getOpposeUsers()
: gets an array containing the usernames that opposed the RfX. Functions similarly togetSupportUsers()
.getNeutralUsers()
: gets an array containing the usernames that were neutral at the RfX. Functions similarly togetSupportUsers()
.dupesExist()
: returns a boolean indicating whether there were any duplicate votes at the RfX. Returnsnil
if the vote tables couldn't be processed.getSecondsLeft()
: returns the number of seconds left before the RfX is due to close. Once it is due to close, shows zero. If the ending time cannot be found, returnsnil
.getTimeLeft()
: returns a string showing the time left before the RfX is due to close. The string is in the format "x days, y hours
".getReport()
: returns a URI object for X!'s RfA Analysis tool at Wikimedia Labs, preloaded with the RfX page.getStatus()
: returns a string showing the current status of the RfX. This can be "successful", "unsuccessful", "open", or "pending closure". Returnsnil
if the status could not be determined.
You can compare rfx
objects with the ==
operator. This will return true only if the two objects point to the same page. tostring( rfx )
will return prefixedTitle
from the RfX page's title object (see the reference manual).
Expensive functions
This module makes use of the title:getContent method to fetch RfX page sources. This method will be called for each RfX page being looked up, so each use of rfx.new
will count as an expensive function call. Please be aware that the library may fail for scripts which create many different RfX objects. (The current limit for the English Wikipedia is 500 expensive function calls per page.) Also, each RfX page that is looked up will count as a transclusion in Special:WhatLinksHere.
----------------------------------------------------------------------
-- Module:Rfx --
-- This is a library for retrieving information about requests --
-- for adminship and requests for bureaucratship on the English --
-- Wikipedia. Please see the module documentation for instructions. --
----------------------------------------------------------------------
local libraryUtil = require( 'libraryUtil' )
--------------------------------------
-- Helper functions --
--------------------------------------
-- Get a title object, passing the function through pcall
-- in case we are over the expensive function count limit.
local function getTitleObject( title )
if not title then
return nil
end
local noError, titleObject = pcall( mw.title.new, title )
if not noError or ( noError and not titleObject ) then
return nil
end
return titleObject
end
-- Returns an array of usernames that voted in a particular section.
-- If a username cannot be processed, an error message is displayed
-- in the table field, together with a random number to avoid it
-- being mistaken for a duplicate vote.
local function getVoteTable( section )
if type( section ) ~= 'string' then
return nil
end
section = mw.ustring.match(section, '^(.-\n#.-)\n[^#]') or section -- Discard subsequent numbered lists.
local t = {}
for vote in mw.ustring.gmatch(section, '\n#[^#*;:][^\n]*') do
local username = false
for link in mw.ustring.gmatch(vote, '%[%[([^%[%]]-)%]%]') do
-- If there is a pipe, get the text before it.
if mw.ustring.match(link, '|') then
link = mw.ustring.match(link, '^(.-)|')
end
-- If there is a slash, get the text before that.
if mw.ustring.match(link, '/') then
link = mw.ustring.match(link, '^(.-)/')
end
-- Decode html entities and percent encodings.
link = mw.text.decode(link, true)
link = mw.uri.decode(link, 'WIKI')
-- If there is a hash, get the text before that.
if mw.ustring.match(link, '#') then
link = mw.ustring.match(link, '^(.-)#')
end
local userLink = mw.ustring.match(link, '^[%s_]*[uU][sS][eE][rR][%s_]*:[%s_]*(.-)[%s_]*$')
local userTalkLink = mw.ustring.match(link, '^[%s_]*[uU][sS][eE][rR][%s_]*[tT][aA][lL][kK][%s_]*:[%s_]*(.-)[%s_]*$')
if userLink then
username = userLink
elseif userTalkLink then
username = userTalkLink
end
end
if username then
table.insert( t, username )
else
vote = mw.ustring.match( vote, '^\n#%s*(.-)$' ) or vote -- Trim initial newline, hash and whitespace.
table.insert( t, mw.ustring.format( "'''Error parsing signature''': ''%s''", vote ) )
end
end
return t
end
local function checkDups( ... )
local t = {}
local tables = { ... }
for i, v in ipairs( tables ) do
for j, name in ipairs( v ) do
if t[ name ] then
return true
else
t[ name ] = true
end
end
end
return false
end
------------------------------------------
-- Define the constructor function --
------------------------------------------
local rfx = {}
function rfx.new( title )
local obj = {}
local data = {}
local checkSelf = libraryUtil.makeCheckSelfFunction( 'Module:Rfx', 'rfx', obj, 'rfx object' )
-- Get the title object and check to see whether we are a subpage of WP:RFA or WP:RFB.
if type( title ) ~= 'string' then
return nil
end
title = getTitleObject( title )
if not title then
return nil
end
function data:getTitleObject()
checkSelf( self, 'getTitleObject' )
return title
end
local rfaTitle = getTitleObject( 'Wikipedia:Requests for adminship' )
local rfbTitle = getTitleObject( 'Wikipedia:Requests for bureaucratship' )
if rfaTitle and title:isSubpageOf( rfaTitle ) then
data.type = 'rfa'
elseif rfbTitle and title:isSubpageOf( rfbTitle ) then
data.type = 'rfb'
else
return nil
end
-- Get the page content and process it for votes, end times, etc.
local pageText = title:getContent()
if type( pageText ) ~= 'string' then
return nil
end
local introText, supportText, opposeText, neutralText = mw.ustring.match(
pageText,
'^(.-)\n====[^=\n][^\n]-====.-'
.. '\n=====%s*[sS]upport%s*=====(.-)'
.. '\n=====%s*[oO]ppose%s*=====(.-)'
.. '\n=====%s*[nN]eutral%s*=====(.-)$'
)
if not ( supportText or opposeText or neutralText ) then
introText, supportText, opposeText, neutralText = mw.ustring.match(
pageText,
"^(.-\n'''[^\n]-%(%d+/%d+/%d+%)[^\n]-''')\n.-"
.. "\n'''Support'''(.-)\n'''Oppose'''(.-)\n'''Neutral'''(.-)"
)
end
-- Get the user lists as methods, as making them subtables
-- of the data table causes metatable headaches.
local supportUsers, opposeUsers, neutralUsers
if supportText and opposeText and neutralText then
supportUsers = getVoteTable( supportText )
opposeUsers = getVoteTable( opposeText )
neutralUsers = getVoteTable( neutralText )
end
function data:getSupportUsers()
checkSelf( self, 'getSupportUsers' )
if not supportText then
return nil
end
return getVoteTable( supportText )
end
function data:getOpposeUsers()
checkSelf( self, 'getOpposeUsers' )
if not opposeText then
return nil
end
return getVoteTable( opposeText )
end
function data:getNeutralUsers()
checkSelf( self, 'getNeutralUsers' )
if not neutralText then
return nil
end
return getVoteTable( neutralText )
end
if supportUsers and opposeUsers and neutralUsers then
data.supports = #supportUsers
data.opposes = #opposeUsers
data.neutrals = #neutralUsers
end
if data.supports and data.opposes then
data.percent = math.floor( (data.supports / (data.supports + data.opposes) * 100) + 0.5 )
end
if introText then
data.endTime = mw.ustring.match( introText, '(%d%d:%d%d, %d+ %w+ %d+) %(UTC%)')
data.user = mw.ustring.match( introText, '===%s*%[%[%s*[wW]ikipedia%s*:%s*[rR]equests for %w+/.-|%s*(.-)%s*%]%]%s*===')
end
-- Checks for duplicate votes
function data:dupesExist()
checkSelf( self, 'dupesExist' )
if not ( supportUsers and opposeUsers and neutralUsers ) then
return nil
end
return checkDups( supportUsers, opposeUsers, neutralUsers )
end
-- Functions to get time left.
function data:getSecondsLeft()
checkSelf( self, 'getSecondsLeft' )
if not self.endTime then
return nil
end
local lang = mw.getContentLanguage()
local now = tonumber( lang:formatDate("U") )
local noError, endTime = pcall( lang.formatDate, lang, 'U', self.endTime )
if not noError or ( noError and type( endTime ) ~= 'string' ) then
return nil
end
local endTime = tonumber( endTime )
if type( endTime ) ~= 'number' then
return nil
end
local secondsLeft = endTime - now
if secondsLeft <= 0 then
return 0
else
return secondsLeft
end
end
function data:getTimeLeft()
checkSelf( self, 'getTimeLeft' )
local lang = mw.getContentLanguage()
local secondsLeft = self:getSecondsLeft()
if not secondsLeft then
return nil
end
return mw.ustring.gsub( lang:formatDuration( secondsLeft, {'days', 'hours'}), ' and', ',' )
end
-- Gets the URI object for X!'s RfA Analysis tool
function data:getReport()
checkSelf( self, 'getReport' )
return mw.uri.new( '//tools.wmflabs.org/xtools/rfa/?p=' .. mw.uri.encode( title.prefixedText ) )
end
-- Gets the current status of the RfX. Returns either "successful", "unsuccessful",
-- "open", or "pending closure". Returns nil if the status could not be found.
function data:getStatus()
checkSelf( self, 'getStatus' )
if not introText then
return nil
end
if data.type == 'rfa' then
if mw.ustring.match(
introText,
'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[sS]uccessful requests for adminship(.-)[%s_]*%]%]'
) then
return 'successful'
elseif mw.ustring.match(
introText,
'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[uU]nsuccessful requests for adminship(.-)[%s_]*%]%]'
) then
return 'unsuccessful'
end
elseif data.type == 'rfb' then
if mw.ustring.match(
introText,
'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[sS]uccessful requests for bureaucratship(.-)[%s_]*%]%]'
) then
return 'successful'
elseif mw.ustring.match(
introText,
'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[uU]nsuccessful requests for bureaucratship(.-)[%s_]*%]%]'
) then
return 'unsuccessful'
end
end
local secondsLeft = self:getSecondsLeft()
if secondsLeft and secondsLeft > 0 then
return 'open'
elseif secondsLeft and secondsLeft <= 0 then
return 'pending closure'
else
return nil
end
end
-- Specify which fields are read-only, and prepare the metatable.
local readOnlyFields = {
getTitleObject = true,
['type'] = true,
getSupportUsers = true,
getOpposeUsers = true,
getNeutralUsers = true,
supports = true,
opposes = true,
neutrals = true,
endTime = true,
percent = true,
user = true,
dupesExist = true,
getSecondsLeft = true,
getTimeLeft = true,
getReport = true,
getStatus = true
}
local function pairsfunc( t, k )
local v
repeat
k = next( readOnlyFields, k )
if k == nil then
return nil
end
v = t[k]
until v ~= nil
return k, v
end
return setmetatable( obj, {
__pairs = function ( t )
return pairsfunc, t, nil
end,
__index = data,
__newindex = function( t, key, value )
if readOnlyFields[ key ] then
error( 'index "' .. key .. '" is read-only', 2 )
else
rawset( t, key, value )
end
end,
__tostring = function( t )
return t:getTitleObject().prefixedText
end
} )
end
return rfx