Jump to content

Module:UserProfile

From PAI

Documentation for this module may be created at Module:UserProfile/doc

-- Module:UserProfile
-- Usage on User:WikiSysop:  {{#invoke:UserProfile|founder}}
-- Data lives in Module:UserProfile/config.

local p = {}
local cfg  = mw.loadData('Module:UserProfile/config')
local lang = mw.language.getContentLanguage()

-- ---------- helpers ----------

-- mw.loadData tables are read-only proxies: use ipairs only, never next(),
-- # or table.concat on them. Everything below copies into local tables first.

local function each(t)
    return ipairs(t or {})
end

local function isEmpty(t)
    return t == nil or t[1] == nil
end

-- True if a value (or anything nested in it) still holds an unfilled « » placeholder
local function hasPlaceholder(v)
    if type(v) == 'string' then
        return v:find('«', 1, true) ~= nil
    elseif type(v) == 'table' then
        for _, x in ipairs(v) do
            if hasPlaceholder(x) then return true end
        end
    end
    return false
end

-- Returns a plain local array without placeholder entries
local function clean(items)
    local out = {}
    for _, it in each(items) do
        if not hasPlaceholder(it) then out[#out + 1] = it end
    end
    return out
end

local function section(title, body)
    if body == nil or body == '' then return nil end
    return '== ' .. title .. ' ==\n' .. body
end

local function subsection(title, body)
    if body == nil or body == '' then return nil end
    return '=== ' .. title .. ' ===\n' .. body
end

local function list(items, ordered)
    items = clean(items)
    if isEmpty(items) then return '' end
    local mark = ordered and '# ' or '* '
    local out = {}
    for _, item in ipairs(items) do
        out[#out + 1] = mark .. tostring(item)
    end
    return table.concat(out, '\n')
end

-- Two-column table, first column as row headers
local function kvTable(rows, caption, collapsed)
    rows = clean(rows)
    if isEmpty(rows) then return '' end
    local t = mw.html.create('table'):addClass('wikitable')
    if collapsed then
        t:addClass('mw-collapsible'):addClass('mw-collapsed')
    end
    if caption then
        t:tag('tr'):tag('th'):attr('colspan', 2):wikitext(caption)
    end
    for _, r in ipairs(rows) do
        local tr = t:tag('tr')
        tr:tag('th'):attr('scope', 'row'):wikitext(tostring(r[1] or ''))
        tr:tag('td'):wikitext(tostring(r[2] or ''))
    end
    return tostring(t)
end

-- Table with a header row
local function gridTable(headers, rows)
    rows = clean(rows)
    if isEmpty(rows) then return '' end
    local t = mw.html.create('table'):addClass('wikitable')
    local head = t:tag('tr')
    for _, h in each(headers) do
        head:tag('th'):attr('scope', 'col'):wikitext(h)
    end
    for _, r in ipairs(rows) do
        local tr = t:tag('tr')
        for _, c in each(r) do
            tr:tag('td'):wikitext(tostring(c))
        end
    end
    return tostring(t)
end

local function link(title, label)
    return string.format('[[%s|%s]]', title, label)
end

-- Format a MediaWiki timestamp safely; returns nil on failure
local function safeDate(fmt, ts)
    local ok, res = pcall(function() return lang:formatDate(fmt, ts) end)
    if ok and res and res ~= '' then return res end
    return nil
end

local function revisionDate(frame, fmt)
    frame = frame or mw.getCurrentFrame()
    local ts = frame:preprocess('{{REVISIONTIMESTAMP}}')
    if ts and ts ~= '' then
        return safeDate(fmt, ts)
    end
    return nil
end

-- ---------- blocks ----------

local function infobox(frame)
    local t = mw.html.create('table'):addClass('wikitable'):addClass('floatright')
    t:attr('style', 'width:22em;')
    t:tag('tr'):tag('th'):attr('colspan', 2):wikitext(cfg.owner or mw.title.getCurrentTitle().text)
    for _, r in ipairs(clean(cfg.infobox)) do
        local tr = t:tag('tr')
        tr:tag('th'):attr('scope', 'row'):wikitext(tostring(r[1] or ''))
        tr:tag('td'):wikitext(tostring(r[2] or ''))
    end
    local d = revisionDate(frame, 'j F Y')
    if d then
        local tr = t:tag('tr')
        tr:tag('th'):attr('scope', 'row'):wikitext('Last updated')
        tr:tag('td'):wikitext(d)
    end
    return tostring(t)
end

local function stats()
    local s = mw.site.stats
    local data = {
        { 'Articles',         s.articles },
        { 'All pages',        s.pages },
        { 'Files',            s.files },
        { 'Registered users', s.users },
        { 'Active users',     s.activeUsers },
        { 'Administrators',   s.admins },
        { 'Total edits',      s.edits },
    }
    local rows = {}
    for _, d in ipairs(data) do
        rows[#rows + 1] = { d[1], lang:formatNum(d[2] or 0) }
    end
    return gridTable({ 'Metric', 'Value' }, rows)
end

local function contentStatus()
    local rows = {}
    for _, cat in each(cfg.watchCategories) do
        local n = mw.site.stats.pagesInCategory(cat, 'pages') or 0
        rows[#rows + 1] = {
            string.format('[[:Category:%s|%s]]', cat, cat),
            lang:formatNum(n),
        }
    end
    return gridTable({ 'Category', 'Pages' }, rows)
end

-- Always links the page, so a missing one shows as a red link you can click to create
local function policies()
    local rows = {}
    for _, pol in each(cfg.policies) do
        local title = mw.title.new(pol[1])
        local exists = title and title.exists
        rows[#rows + 1] = {
            link(pol[1], pol[2]),
            exists and 'Exists' or "''Missing''",
        }
    end
    return gridTable({ 'Policy page', 'Status' }, rows)
end

local function dashboard()
    local rows = {}
    for _, group in each(cfg.tools) do
        local links = {}
        for _, l in ipairs(clean(group[2])) do
            links[#links + 1] = link(l[1], l[2])
        end
        if #links > 0 then
            rows[#rows + 1] = { group[1], table.concat(links, ' · ') }
        end
    end
    return kvTable(rows, 'Maintenance and moderation tools', true)
end

local function faq()
    return kvTable(cfg.faq, 'Questions about the wiki and my role', true)
end

local function categories()
    local out = {}
    for _, c in each(cfg.categories) do
        out[#out + 1] = '[[Category:' .. c .. ']]'
    end
    return table.concat(out, '\n')
end

-- ---------- main entry ----------

function p.founder(frame)
    frame = frame or mw.getCurrentFrame()
    local out = {}
    local function add(s)
        if s and s ~= '' then out[#out + 1] = s end
    end

    add(infobox(frame))
    add(cfg.lead)
    add('<br clear="all" />')

    add(section("Founder's statement", cfg.statement))
    add(section('Mission and vision', kvTable(cfg.mission)))

    local bg = {}
    local function addBg(s) if s then bg[#bg + 1] = s end end
    if cfg.story and not hasPlaceholder(cfg.story) then
        addBg(subsection('Founding story', cfg.story))
    end
    addBg(subsection('Expertise', list(cfg.expertise)))
    addBg(subsection('Timeline', gridTable({ 'Date', 'Milestone' }, cfg.timeline)))
    add(section('Background', table.concat(bg, '\n\n')))

    add(section('Responsibilities', gridTable({ 'Area', 'What I do' }, cfg.responsibilities)))
    add(section('Editorial principles', list(cfg.principles, true)))
    add(section('Governance', list(cfg.governance)))
    add(section('Technical stewardship', kvTable(cfg.tech)))
    add(section('Get involved', list(cfg.involved, true)))
    add(section('Wiki at a glance', stats()))
    add(section('Content status', contentStatus()))
    add(section('Admin dashboard', dashboard()))
    add(section('Policy pages', policies()))
    add(section('Frequently asked questions', faq()))
    add(section('Contact', list(cfg.contact)))

    local d = revisionDate(frame, 'F Y')
    add("----\n''Founder profile of the wiki." .. (d and (' Last updated ' .. d .. '.') or '') .. "''")
    add(categories())

    return table.concat(out, '\n\n')
end

-- Reusable sections for other pages: {{#invoke:UserProfile|stats}} etc.
function p.stats()         return stats() end
function p.contentStatus() return contentStatus() end
function p.dashboard()     return dashboard() end
function p.policies()      return policies() end
p.main = p.founder

return p