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 08:53, 18 July 2013 (use pageText rather than introText for getting the category, as that pattern match is pretty accurate). 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]*') 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 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