Module:Monthly Challenge listing
Appearance
Provides logic for {{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],
['color'] = 'black'
}
}
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