Permanently protected module

Module:Monthly Challenge listing

From Wikisource
Jump to navigation Jump to search

require('strict')

--[=[
Module that provides "smart" updating of a monthly list of works for the
Monthly Challenge.

All it needs is the data table at [[Monthly Challenge/data/YYYY]] and
optionally [[Monthly Challenge/data/YYYY-1]], and it will choose the right
indexes to show and divide them into sections.
]=]

local p = {} --p stands for package
local getArgs = require('Module:Arguments').getArgs
local constructAuthorLink = require('Module:Author link').constructAuthorLink

local dataPrefix = 'Module:Monthly Challenge/data/'
local shortThreshold = 50 -- pages
local ageLimit = 3 -- months
local yearsToLookBack = 1 -- years

local colors = {
    problematic = '#b0b0ff', -- blue
    proofread = '#ffa0a0', -- red
    validate = '#ffe867', --yellow
    done = '#90ff90' --green
}

local function getAge(to, from_y, from_m)

    local my = mw.text.split(to, '-', true)

    if #my ~= 2 then
        error("Invalid first_month: " .. to)
    end

    my[1] = tonumber(my[1])
    my[2] = tonumber(my[2])

    if my[1] == nil or my[2] == nil then
        error("Invalid first_month: " .. to)
    end

    return (from_y * 12 + from_m) - (my[1] * 12 + my[2])
end

local function outputItem(frame, i)
    local subject = table.concat(i.data.subject, ", ")
    local coverNum = tonumber(i.data.cover)
    
    -- avoid line breaks like Volume//1
    local title = i.data.title:gsub("Volume (%d)", "Volume %1")
    
    local author = ''
    for k, v in pairs(i.data.author) do
    	if k > 1 then
    		if k < #i.data.author then
	    		author = author .. ", "
	    	else
	    		author = author .. " and "
    		end
    	end
		author = author .. constructAuthorLink(v)
    end
    
    -- progress bars are very expensive - validated works dont _really_ need them
    -- so turn them off for now to give as a little headroom
    local no_prog_bar
    if i.data.work_status.final == 'validated' then
    	no_prog_bar = 'yes'
    end

    local args = {
        [1] = i.name,
        [2] = coverNum,
        [3] = title,
        [4] = i.data.year,
        [5] = author,
        [6] = subject,
        [7] = i.age,
        [8] = i.data.flag,
        page = i.data.page,
        no_progress_bar = no_prog_bar,
    }

    if coverNum == nil then
        args.cover = i.data.cover
    end
    
    if i.highlight ~= nil then
    	args.highlight = i.highlight
    end

    return frame:expandTemplate{
        title = 'MC-Cover',
        args = args
    }
end

local function outputItems(frame, items)
    local out = ''
    if items ~= nil then
        for k, v in pairs(items) do
            out = out .. outputItem(frame, v)
        end
    end
    return out
end

-- convert a string or array to an array of strings
-- note, this copies the strings, so it doesn't matter if the source is read-only
local function arrayify(stringOrArray)
	local out = {}
    if type(stringOrArray) == 'table' then
        for _, v2 in pairs(stringOrArray) do
            table.insert(out, v2)
        end
    else
        table.insert(out, stringOrArray)
    end
    return out
end

local function getSortTitle(t)
	if not t then
		return nil
	end
	t = t:gsub('^The (.*)$', '%1, The')
	t = t:gsub('^A (.*)$', '%1, A')
	t = t:gsub('^An (.*)$', '%1, An')
	return t
end

local function getIndexesForYearData(data, year, month)
    local indexes = {}
    for age, works in pairs(data) do
    	for index, v in pairs(works) do

            local idx = {
                name = index,
                age = age,
            }

            -- copy what we need from the read-only table
            idx.data = {
                title = v.title,
                sort_title = getSortTitle(v.title),
                cover = v.cover,
                author = arrayify(v.author),
                flag = v.flag,
                year = v.year,
                page = v.page,
                subject = arrayify(v.subject),
                short = v.short,
                work_status = v.status
            }
            table.insert(indexes, idx)
        end
    end
    return indexes
end

local function populateIndexData(indexes)
	
	for k, v in pairs(indexes) do
		
		v.name = v.name:gsub('_', ' ')
		
		local d = v.data
		
        local file = mw.title.makeTitle('File', v.name)
        local index = mw.title.makeTitle('Index', v.name)
        
        -- expensive!
        local imgData = file.file

        if d.short ~= nil then
            v.short = d.short
        elseif imgData ~= nil and imgData.pages then
            v.short = (#imgData.pages < shortThreshold)
        else
            v.short = false
        end

        -- avoid storing content, it eats memory
        -- also check content == nil expensive call over index.exists
        local content = index:getContent()
	end
end

local function construct_header(frame, message, color)
    return frame:expandTemplate{
        title = 'MC-Section/s',
        args = {
            text = message,
            ['background-color'] = colors[color]
        }
    }
end

local function getTimeInQueueMsg(months)

    if months < 0 then
        return "no expiry"
    elseif months == 0 then
        return "new works this month"
    end

    local m = "month"
    if months > 1 then
        m = m .. 's'
    end

    return "works added " .. months .. ' ' .. m .. ' ago'
end

local function constructListingSection(frame, t, colour, msg)
    local out = ''

    if t and #t > 0 then
	    table.sort(t, function (a, b)
	    	return a.data.sort_title < b.data.sort_title	
	    end)
    	
        out = out .. construct_header(frame, msg, colour)
        out = out .. outputItems(frame, t)
        out = out .. frame:expandTemplate{title='MC-Section/e'}
    end
    return out
end

local function constructListing(frame, works)
    local out = ''
    out = out .. constructListingSection(frame, works.tofix, 'problematic', "To fix")
    out = out .. constructListingSection(frame, works.short.p, 'proofread', "Under 50 pages: to proofread")
    out = out .. constructListingSection(frame, works.normal.p[-1], 'proofread', "To proofread (" .. getTimeInQueueMsg(-1) .. ")")

    for i = 0, ageLimit, 1 do
        local msg = getTimeInQueueMsg(i)
        out = out .. constructListingSection(frame, works.normal.p[i], 'proofread', "To proofread (" .. msg .. ")")
    end
    
	out = out .. constructListingSection(frame, works.short.v, 'validate', "Under 50 pages: to validate")
    out = out .. constructListingSection(frame, works.normal.v[-1], 'validate', "To validate (" .. getTimeInQueueMsg(-1) .. ")")

    for i = 0, ageLimit, 1 do
        local msg = getTimeInQueueMsg(i)
        out = out .. constructListingSection(frame, works.normal.v[i], 'validate', "To validate (" .. msg .. ")")
    end

    out = out .. constructListingSection(frame, works.done, 'done', "Completed works")

    return out
end

-- This loads up to yearsToLookBack + 1 data tables
-- Then loads the content for each found index
-- If this is an issue for limits, this could be done in two passes,
local function getRelevantIndexes(year, month)
	local indexes = {}

    local success, monthData = pcall(mw.loadData, dataPrefix .. string.format('%d-%02d', year, month))
    if success and monthData and monthData.works then
        local monthIndexes = getIndexesForYearData(monthData.works, year, month)
        for _, v in ipairs(monthIndexes) do
            table.insert(indexes, v)
        end
    end
    return indexes
end

local function latestStatus( idx )
	return idx.data.work_status.final or idx.data.work_status.initial
end

local function indexIsValidated( idx )
	return latestStatus( idx ) == 'validated'	
end

local function indexIsProofread( idx )
	return latestStatus( idx ) == 'proofread'
end

local function findSprintWorks(indexes, sprint)
	local sprintIndexes = {}
	
	local excludeValidated = true
	
	if sprint ~= nil then
		local s = mw.ustring.lower(sprint)
		
		for _, v in pairs(indexes) do
			for _, vs in pairs(v.data.subject) do
				if s == mw.ustring.lower(vs) and
						not (excludeValidated and indexIsValidated(v) ) then
					table.insert(sprintIndexes, v)
					break
				end
			end
		end
	end
	
	return sprintIndexes
end

local function getChallengeWorks(year, month, sprint)
    local indexes = getRelevantIndexes(year, month)
    populateIndexData(indexes)

    local works = {
        short = {
            v = {},
            p = {},
        },
        normal = {
            v = {},
            p = {},
        },
        done= {},
        tofix= {},
        sprint = findSprintWorks(indexes, sprint)
    }
    
    -- highlight sprint works
    for _, v in pairs(works.sprint) do
		v.highlight = sprint
    end

    -- distribute into bins for display
    for k, v in pairs(indexes) do
    	
    	if sprint and v.highlight then
    		table.insert(works.sprint, v)
    	end

        if indexIsValidated(v) then
            table.insert(works.done, v)
        elseif latestStatus( v ) == 'tofix' then
            table.insert(works.tofix, v)
        elseif v.short then
        	-- short works are all together
            if indexIsProofread(v) then
                table.insert(works.short.v, v)
            else
                table.insert(works.short.p, v)
            end
        else
            -- this is a normal work, arrange by age
            local t;
            if indexIsProofread(v) then
                t = works.normal.v
            else
                t = works.normal.p
            end

            if t[v.age] == nil then
                t[v.age] = {}
            end
            table.insert(t[v.age], v)
        end
    end

    return works
end

local function ymwFromArgs(argy, argm, argw)

    local year = tonumber(argy)
    local month = tonumber(argm)

    if year == nil or month == nil then
        error("Both month and year must be given")
    end
    
	local thisWeek = argw
	if thisWeek == nil then
		local dateNow = os.date("!*t")
		thisWeek = math.floor((dateNow.day - 1) / 7) + 1
	end

    return year, month, thisWeek
end
	

--[=[
Function docs
]=]
function p.listing(frame)
    local args = getArgs(frame)

	local year, month = ymwFromArgs(args[1], args[2])

	local works = getChallengeWorks(year, month, args.sprint)

    --mw.logObject(works)

    return constructListing(frame, works)
end

local function metatableLength(t)
	local l = 0
	for _,_ in pairs(t) do
		l = l + 1
	end
	return l
end

local function getSprint(year, month, week)

	local ok, thisYearData = pcall(mw.loadData, dataPrefix .. string.format('%d-%02d', year, month))
	
	if not ok then
		return nil
	end
	
	local monthSprints = thisYearData.sprints
	
	local sprint = nil
	if monthSprints ~= nil then
		local maxWeek = metatableLength(monthSprints)
		if week > maxWeek then
			week = maxWeek
		end
		sprint = monthSprints[week]
	end
	return sprint
end

--[=[
Return the relvant sprint for the given challenge
]=]
function p.sprint(frame)
	local args = getArgs(frame)
	local year, month, week = ymwFromArgs(args.year, args.month, args.week)
	local sprint = getSprint(year, month, week)
	return sprint
end

--[=[
Construct an HTML list of the works in the relevant challenge's sprint
]=]
function p.sprintWorks(frame)
	local args = getArgs(frame)
	local year, month, week = ymwFromArgs(args.year, args.month, args.week)
	local sprint = getSprint(year, month, week)
	
	local links = {}
	if sprint == nil then
		table.insert(links, 'No sprint found')
	else
		-- load the works for the given challenge
		local indexes = getRelevantIndexes(year, month)
	    populateIndexData(indexes)
	
		local sprintWorks = findSprintWorks(indexes, sprint)
		
		for k, v in pairs(sprintWorks) do
			local link = '[[Index:' .. v.name .. '|' .. v.data.title .. ']]'
			table.insert(links, link)
		end
	end

	local ul = mw.html.create("ul")

	for k, v in pairs(links) do
		ul:tag('li')
			:wikitext(v)
	end

	return ul
end

local function make_list( lines )
	local out = ''
	for i, v in pairs( lines ) do
		out = out .. '*' .. v .. '\n'
	end
	return out	
end

--[=[
Completed works in this month
]=]
local function get_completed_works( year, month, stage )
	local indexes = getRelevantIndexes(year, month)
	
	local completed_work_list = {}
	
	for _, v in pairs( indexes ) do
		local status = v.data.work_status
		if status then
			if stage == 'validated' then
				if status.initial ~= 'validated' and status.final == 'validated' then
					table.insert( completed_work_list, v )	
				end
			elseif stage == 'proofread' then
				-- any work that stated out not proofread and ended validated
				if not ( status.initial == 'proofread' or status.initial == 'validated' )
						and ( status.final == 'proofread' or status.final == 'validated' ) then
					table.insert( completed_work_list, v )		
				end
			end
		end
	end
	
	return completed_work_list
end

function p.completed_works( frame )
    local args = getArgs(frame)
	local year, month = ymwFromArgs(args[1], args[2])
	
	local completed_work_list = get_completed_works(year, month, args.stage)
	
	local lines = {}
	for k, v in pairs(completed_work_list) do
		table.insert( lines, string.format( '[[Index:%s|%s]]', v.name, v.data.title ) )	
	end
	
	return make_list( lines )
end

function p.have_completed_works( frame )
    local args = getArgs(frame)
	local year, month = ymwFromArgs(args[1], args[2], args.stage)
	local completed_work_list = get_completed_works(year, month, args.stage)

	return (#completed_work_list  > 0) and 'yes' or 'no'
end

return p