Module:Monthly Challenge listing

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 =  .. 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( '%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