Jump to content

Module:Rfx

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Mr. Stradivarius (talk | contribs) at 02:08, 31 January 2014 (convert whitespace to tabs). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

----------------------------------------------------------------------
--                          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][^\n]*)') do
		if mw.ustring.find(vote, '%S') then
			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, '^%s*(.-)$' ) or vote -- Trim initial newline, hash and whitespace.
				table.insert( t, mw.ustring.format( "'''Error parsing signature''': ''%s''", vote ) )
			end
		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 data.type == 'rfa' then
			if mw.ustring.match(
				pageText,
				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[sS]uccessful requests for adminship(.-)[%s_]*%]%]'
			) then
				return 'successful'
			elseif mw.ustring.match(
				pageText,
				'%[%[[%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(
				pageText,
				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[sS]uccessful requests for bureaucratship(.-)[%s_]*%]%]'
			) then
				return 'successful'
			elseif mw.ustring.match(
				pageText,
				'%[%[[%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