Editing Module:Portal
From the only original and legitimate Battlestar Wiki: the free-as-in-beer, non-corporate, open-content encyclopedia, analytical reference, and episode guide on all things Battlestar Galactica. Accept neither subpar substitutes nor subpar clones.
More actions
The edit can be undone.
Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
| Latest revision | Your text | ||
| Line 1: | Line 1: | ||
-- Module:Portal | -- Module:Portal | ||
-- Provides | -- Provides dynamic portal widgets for BattlestarWiki: | ||
-- stats bar, newest page, random image, colonial calendar | |||
-- | -- | ||
-- | -- Usage (from Template:Portal/Layout): | ||
-- | -- {{#invoke:Portal|stats|category=Characters (RDM)|episodes=73|characters=62|seasons=4|year=2003}} | ||
-- | -- {{#invoke:Portal|newestPage|category=Characters (RDM)}} | ||
-- {{#invoke:Portal|randomImage|category=Images (RDM)}} | |||
-- {{#invoke:Portal|colonialCalendar|category=RDM}} | |||
-- | |||
-- | |||
local p = {} | local p = {} | ||
local mwHtml | local mwTitle = mw.title | ||
local mwHtml = mw.html | |||
local mwSite = mw.site | |||
local mwUstring = mw.ustring | local mwUstring = mw.ustring | ||
| Line 19: | Line 19: | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
local function trim( s ) | --- Trim whitespace from both ends of a string. | ||
return s and mwUstring.match( s, '^%s*(.-)%s*$' ) or '' | local function trim(s) | ||
return s and mwUstring.match(s, '^%s*(.-)%s*$') or '' | |||
end | end | ||
local function arg( args, key, default ) | --- Return the value of an arg, trimmed, or a default. | ||
local function arg(args, key, default) | |||
local v = args[key] | local v = args[key] | ||
return ( v and trim( v ) ~= '' ) and trim( v ) or default | return (v and trim(v) ~= '') and trim(v) or default | ||
end | |||
--- Build a CSS class string, filtering nils. | |||
local function classes(...) | |||
local t = {} | |||
for _, c in ipairs({...}) do | |||
if c then t[#t+1] = c end | |||
end | |||
return table.concat(t, ' ') | |||
end | |||
--- Get the count of members in a MediaWiki category via mw.title magic. | |||
--- Returns nil if category doesn't exist. | |||
local function getCategoryCount(catName) | |||
local t = mwTitle.new('Category:' .. catName) | |||
if not t then return nil end | |||
-- pagesInCategory returns a table {all=N, files=N, subcats=N, pages=N} | |||
-- Needs to be called through the expensive-function path; we use the | |||
-- parser function as a safer fallback. | |||
local ok, count = pcall(function() | |||
return t.pagesInCategory | |||
end) | |||
if ok and count then return count end | |||
return nil | |||
end | |||
--- Fetch the most recently created page in a category. | |||
--- Returns {title, excerpt, timestamp} or nil. | |||
--- NOTE: mw.title API doesn't support category member queries directly; | |||
--- we use mw.ext.cargo if available, otherwise return nil so the template | |||
--- can show a static fallback. In production this should be backed by a | |||
--- Cargo table or a Lua-side API call cached in frame:callParserFunction. | |||
local function getNewestInCategory(catName) | |||
-- Cargo query (requires Cargo extension + populated tables) | |||
if mw.ext and mw.ext.cargo then | |||
local ok, rows = pcall(function() | |||
return mw.ext.cargo.query( | |||
'Pages', | |||
'_pageName,_pageTitle,_creationDate', | |||
{ | |||
where = '_category = "' .. catName .. '"', | |||
orderBy = '_creationDate DESC', | |||
limit = 1 | |||
} | |||
) | |||
end) | |||
if ok and rows and rows[1] then | |||
local r = rows[1] | |||
return { | |||
title = r._pageTitle or r._pageName, | |||
pageName = r._pageName, | |||
timestamp = r._creationDate | |||
} | |||
end | |||
end | |||
-- Fallback: nil — template renders a static placeholder message | |||
return nil | |||
end | |||
--- Return a random file from a category. | |||
--- In a live wiki this uses mw.title combined with randomness seeded by | |||
--- the current date so the image changes daily rather than on every purge. | |||
--- Returns a file title string or nil. | |||
local function getDailyRandomImage(catName) | |||
-- Seed by date so the same image shows all day (stable across purges) | |||
local dateStr = mw.getCurrentFrame():callParserFunction('#time', 'Ymd') | |||
local seed = tonumber(dateStr) or 20240101 | |||
math.randomseed(seed) | |||
-- We can't enumerate category members purely in Lua/Scribunto, so we | |||
-- depend on either: | |||
-- (a) A companion sub-page Portal:XXX/ImageList (one filename per line) | |||
-- (b) The RandomInCategory extension | |||
-- (c) A Cargo table of files tagged with the category | |||
-- | |||
-- Strategy: try sub-page first, then RandomInCategory magic word, else nil. | |||
-- (a) Sub-page list e.g. "Portal:Battlestar Galactica (RDM)/ImageList" | |||
-- Each line: File:Foo.jpg | optional caption | |||
local listPage = mwTitle.getCurrentTitle().prefixedText .. '/ImageList' | |||
local listTitle = mwTitle.new(listPage) | |||
if listTitle and listTitle.exists then | |||
local content = listTitle:getContent() | |||
if content then | |||
local files = {} | |||
for line in content:gmatch('[^\n]+') do | |||
local fname = trim(line:match('^([^|]+)')) | |||
if fname ~= '' then | |||
files[#files+1] = fname | |||
end | |||
end | |||
if #files > 0 then | |||
return files[math.random(#files)] | |||
end | |||
end | |||
end | |||
-- (b) Nothing found — return nil; template uses a placeholder | |||
return nil | |||
end | end | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
-- Colonial Calendar | -- Colonial Calendar data | ||
-- Keyed "M-D" (no zero-padding) → list of {year, text} | -- Keyed as "M-D" (no zero-padding) → list of {year, text, link} | ||
-- Covers real-world | -- Covers real-world broadcast dates and in-universe events. | ||
-- | -- Editors can extend this table or replace it with a data module. | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
local CALENDAR = { | local CALENDAR = { | ||
['1-14'] = { | ['1-14'] = { | ||
{ year = '2005', text = '[[33 (episode)|33]] premieres on Sci Fi Channel — first regular | { year='2005', text='[[33 (episode)|33]] premieres on Sci Fi Channel — the first regular episode of the Re-imagined Series', link='33 (episode)' }, | ||
}, | }, | ||
['1-21'] = { | ['1-21'] = { | ||
{ year = '2005', text = '[[Water (episode)|Water]] airs on Sci Fi Channel' }, | { year='2005', text='[[Water (episode)|Water]] airs on Sci Fi Channel', link='Water (episode)' }, | ||
}, | }, | ||
['3-4'] | ['3-4'] = { | ||
{ year = '2005', text = '[[Tigh Me Up, Tigh Me Down]] airs on Sky One (UK)' }, | { year='2005', text='[[Tigh Me Up, Tigh Me Down]] airs on Sky One (UK)', link='Tigh Me Up, Tigh Me Down' }, | ||
}, | }, | ||
['3-18'] = { | ['3-18'] = { | ||
{ year = '2005', text = '[[Colonial Day]] airs on Sky One' }, | { year='2005', text='[[Colonial Day]] airs on Sky One', link='Colonial Day' }, | ||
}, | }, | ||
['3-20'] = { | ['3-20'] = { | ||
{ year = '2009', text = '[[Daybreak, Part II|Daybreak]] finale airs — end of the Re-imagined Series' }, | { year='2009', text='[[Daybreak, Part II|Daybreak]] finale airs — end of the Re-imagined Series', link='Daybreak, Part II' }, | ||
}, | }, | ||
['4-1'] | ['4-1'] = { | ||
{ year = '2005', text = | { year='2005', text='[[Kobol\'s Last Gleaming, Part II]] airs — Season 1 finale', link="Kobol's Last Gleaming, Part II" }, | ||
}, | }, | ||
['4-12'] = { | ['4-12'] = { | ||
{ year = '2007', text = '[[The Woman King]] airs on Sci Fi Channel' }, | { year='2007', text='[[The Woman King]] airs on Sci Fi Channel', link='The Woman King' }, | ||
{ year = '2009', text = '[[Islanded in a Stream of Stars]] airs — three episodes remain' }, | { year='2009', text='[[Islanded in a Stream of Stars]] airs — three episodes remain', link='Islanded in a Stream of Stars' }, | ||
}, | }, | ||
['6-3'] | ['6-3'] = { | ||
{ year = '2005', text = '[[Scattered]] airs — Season 2 premiere' }, | { year='2005', text='[[Scattered]] airs — Season 2 premiere', link='Scattered' }, | ||
}, | }, | ||
['9-22'] = { | ['9-22'] = { | ||
{ year = '2006', text = '[[Occupation]]/[[Precipice]] two-hour Season 3 premiere airs' }, | { year='2006', text='[[Occupation]]/[[Precipice]] two-hour Season 3 premiere airs', link='Occupation' }, | ||
}, | }, | ||
['10-6'] = { | ['10-6'] = { | ||
{ year = '2006', text = '[[Exodus, Part II]] airs — fan-favourite | { year='2006', text='[[Exodus, Part II]] airs — fan-favourite battle sequence', link='Exodus, Part II' }, | ||
}, | }, | ||
['12-8'] = { | ['12-8'] = { | ||
{ year = '2003', text = '[[Miniseries, Night 1|BSG Miniseries]] premieres on Sci Fi Channel' }, | { year='2003', text='[[Miniseries, Night 1|BSG Miniseries]] premieres on Sci Fi Channel', link='Miniseries, Night 1' }, | ||
}, | }, | ||
} | } | ||
-- Build lookup normalised to "M-D" (strips leading zeros) | |||
local function todayKey() | local function todayKey() | ||
local | local m = tonumber(mw.getCurrentFrame():callParserFunction('#time', 'n')) | ||
local d = tonumber(mw.getCurrentFrame():callParserFunction('#time', 'j')) | |||
local d = tonumber( | |||
return m .. '-' .. d | return m .. '-' .. d | ||
end | end | ||
function p.colonialCalendar( frame ) | ------------------------------------------------------------------------ | ||
-- Public: stats | |||
-- Args: category, episodes, characters, seasons, year | |||
-- Any missing numeric arg is omitted from the bar. | |||
------------------------------------------------------------------------ | |||
function p.stats(frame) | |||
local args = frame:getParent().args | |||
local cat = arg(args, 'category', '') | |||
-- Collect stat items: {num, label} | |||
local items = {} | |||
-- Article count from category | |||
if cat ~= '' then | |||
local n = getCategoryCount(cat) | |||
if n then | |||
items[#items+1] = { num = tostring(n), label = 'articles' } | |||
end | |||
end | |||
-- Manual counts passed as template args | |||
local function addStat(key, label) | |||
local v = arg(args, key, nil) | |||
if v then items[#items+1] = { num = v, label = label } end | |||
end | |||
addStat('episodes', 'episodes') | |||
addStat('characters', 'characters') | |||
addStat('seasons', 'seasons') | |||
addStat('year', 'first aired') | |||
if #items == 0 then return '' end | |||
local root = mwHtml.create('div'):addClass('portal-stats-bar') | |||
for _, item in ipairs(items) do | |||
root:tag('div'):addClass('portal-stat') | |||
:tag('div'):addClass('portal-stat-num'):wikitext(item.num):done() | |||
:tag('div'):addClass('portal-stat-label'):wikitext(item.label):done() | |||
end | |||
return tostring(root) | |||
end | |||
------------------------------------------------------------------------ | |||
-- Public: newestPage | |||
-- Args: category | |||
-- Returns an HTML block for the newest article widget body. | |||
------------------------------------------------------------------------ | |||
function p.newestPage(frame) | |||
local args = frame:getParent().args | |||
local cat = arg(args, 'category', '') | |||
local fallback = arg(args, 'fallback', '') | |||
local newest = (cat ~= '') and getNewestInCategory(cat) or nil | |||
local root = mwHtml.create('div'):addClass('portal-newest') | |||
if newest then | |||
root:tag('div'):addClass('portal-newest-label') | |||
:wikitext('Newest · Category:' .. cat) | |||
root:tag('div'):addClass('portal-newest-title') | |||
:wikitext('[[' .. newest.pageName .. '|' .. newest.title .. ']]') | |||
if newest.timestamp then | |||
root:tag('div'):addClass('portal-newest-meta') | |||
:wikitext('Created ' .. newest.timestamp) | |||
end | |||
elseif fallback ~= '' then | |||
-- Static fallback provided by the template editor | |||
root:wikitext(fallback) | |||
else | |||
root:tag('div'):addClass('portal-newest-placeholder') | |||
:wikitext('<small>[[Special:NewPages|Browse new articles]]' | |||
.. (cat ~= '' and ' · filtered to [[:Category:' .. cat .. ']]' or '') | |||
.. '</small>') | |||
end | |||
return frame:preprocess(tostring(root)) | |||
end | |||
------------------------------------------------------------------------ | |||
-- Public: randomImage | |||
-- Args: category (used to find the ImageList sub-page) | |||
-- Returns wikitext for a [[File:...]] transclusion, or placeholder. | |||
------------------------------------------------------------------------ | |||
function p.randomImage(frame) | |||
local args = frame:getParent().args | |||
local cat = arg(args, 'category', '') | |||
local size = arg(args, 'size', '400px') | |||
local file = getDailyRandomImage(cat) | |||
if file then | |||
-- Normalise: strip leading "File:" if present | |||
file = file:gsub('^[Ff]ile:', '') | |||
return '[[File:' .. file .. '|' .. size .. '|portal random image]]' | |||
end | |||
-- Placeholder div so the layout doesn't collapse | |||
local ph = mwHtml.create('div'):addClass('portal-image-placeholder') | |||
:wikitext('No image list found. Create [[' .. | |||
mwTitle.getCurrentTitle().prefixedText .. '/ImageList]] to enable the random image slider.') | |||
return tostring(ph) | |||
end | |||
------------------------------------------------------------------------ | |||
-- Public: colonialCalendar | |||
-- Returns an HTML fragment listing today's BSG events. | |||
-- Args: category (used to filter if needed; currently shows all entries) | |||
------------------------------------------------------------------------ | |||
function p.colonialCalendar(frame) | |||
local key = todayKey() | local key = todayKey() | ||
local events = CALENDAR[key] | local events = CALENDAR[key] | ||
local dateDisplay = mw.getCurrentFrame():callParserFunction( '#time', 'F j' ) | local root = mwHtml.create('div'):addClass('portal-calendar') | ||
-- Date display | |||
local dateDisplay = mw.getCurrentFrame():callParserFunction('#time', 'F j') | |||
root:tag('div'):addClass('portal-calendar-date'):wikitext(dateDisplay) | |||
local | local sub = root:tag('div'):addClass('portal-calendar-sub') | ||
:wikitext('Real-world BSG broadcast history') | |||
:wikitext( 'Real-world BSG broadcast history' ) | |||
if events and #events > 0 then | if events and #events > 0 then | ||
local list = root:tag( 'ul' ):addClass( 'portal-calendar-events' ) | local list = root:tag('ul'):addClass('portal-calendar-events') | ||
for _, ev in ipairs( events ) do | for _, ev in ipairs(events) do | ||
list:tag( 'li' ):addClass( 'portal-calendar-event' ) | list:tag('li'):addClass('portal-calendar-event') | ||
:tag( 'span' ):addClass( 'portal-calendar-year' ):wikitext( ev.year ):done() | :tag('span'):addClass('portal-calendar-year'):wikitext(ev.year):done() | ||
:wikitext( ' | :wikitext(' — ') | ||
:tag('span'):wikitext(ev.text) | |||
end | end | ||
else | else | ||
root:tag( 'div' ):addClass( 'portal-calendar-none' ) | root:tag('div'):addClass('portal-calendar-none') | ||
:wikitext( '<small>No recorded BSG events on this date.</small>' ) | :wikitext('<small>No recorded BSG events on this date.</small>') | ||
end | end | ||
return frame:preprocess( tostring( root ) ) | return frame:preprocess(tostring(root)) | ||
end | end | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
-- | -- Public: relatedPortals | ||
-- Args: | -- Args: type = 'series' | 'topic' | ||
-- Renders the related-portals tile grid appropriate for the portal type. | |||
-- | |||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
local SERIES_PORTALS = { | local SERIES_PORTALS = { | ||
{ title = 'Portal:Battlestar Galactica (TOS)', label = 'Original Series', sub = 'TOS | { title = 'Portal:Battlestar Galactica (TOS)', label = 'Original Series', sub = 'TOS · 1978' }, | ||
{ title = 'Portal:Galactica 1980', label = 'Galactica 1980', sub = '1980' }, | { title = 'Portal:Galactica 1980', label = 'Galactica 1980', sub = '1980' }, | ||
{ title = 'Portal:Battlestar Galactica (RDM)', label = 'Re-imagined Series', sub = 'RDM | { title = 'Portal:Battlestar Galactica (RDM)', label = 'Re-imagined Series', sub = 'RDM · 2003–2009' }, | ||
{ title = 'Portal:Caprica', label = 'Caprica', sub = 'Prequel | { title = 'Portal:Caprica', label = 'Caprica', sub = 'Prequel · 2010–2011' }, | ||
{ title = 'Portal:Blood and Chrome', label = 'Blood & Chrome', sub = 'Prequel | { title = 'Portal:Blood and Chrome', label = 'Blood & Chrome', sub = 'Prequel · 2012' }, | ||
} | } | ||
local TOPIC_PORTALS = { | local TOPIC_PORTALS = { | ||
{ title = 'Portal:Characters', | { title = 'Portal:Characters', label = 'Characters', sub = 'All series' }, | ||
{ title = 'Portal:Ships', | { title = 'Portal:Ships', label = 'Ships', sub = 'All series' }, | ||
{ title = 'Portal:Cylons', | { title = 'Portal:Cylons', label = 'Cylons', sub = 'All series' }, | ||
{ title = 'Portal:Episodes', | { title = 'Portal:Episodes', label = 'Episodes', sub = 'All series' }, | ||
{ title = 'Portal:Merchandise', label = 'Merchandise', sub = 'All series' }, | { title = 'Portal:Merchandise',label = 'Merchandise',sub = 'All series' }, | ||
} | } | ||
function p.relatedPortals( frame ) | function p.relatedPortals(frame) | ||
local args = frame:getParent().args | local args = frame:getParent().args | ||
local exclude = arg( args, 'exclude', '' ) | local pType = arg(args, 'type', 'series') -- 'series' or 'topic' | ||
local exclude = arg(args, 'exclude', '') -- current portal title to skip | |||
local list = (pType == 'topic') and TOPIC_PORTALS or (SERIES_PORTALS) | |||
-- Merge both lists for cross-linking | |||
local combined = {} | local combined = {} | ||
for _, v in ipairs( SERIES_PORTALS ) do combined[#combined + 1] = v end | for _, v in ipairs(SERIES_PORTALS) do combined[#combined+1] = v end | ||
for _, v in ipairs( TOPIC_PORTALS | for _, v in ipairs(TOPIC_PORTALS) do combined[#combined+1] = v end | ||
local root = mwHtml.create( 'div' ):addClass( 'portal-related-grid' ) | local root = mwHtml.create('div'):addClass('portal-related-grid') | ||
for _, portal in ipairs( combined ) do | for _, portal in ipairs(combined) do | ||
if portal.title ~= exclude then | if portal.title ~= exclude then | ||
root:tag( 'div' ):addClass( 'portal-related-tile' ) | root:tag('div'):addClass('portal-related-tile') | ||
:tag( 'div' ):addClass( 'portal-related-title' ) | :tag('div'):addClass('portal-related-title') | ||
:wikitext( '[[' .. portal.title .. '|' .. portal.label .. ']]' ):done() | :wikitext('[[' .. portal.title .. '|' .. portal.label .. ']]'):done() | ||
:tag( 'div' ):addClass( 'portal-related-sub' ) | :tag('div'):addClass('portal-related-sub') | ||
:wikitext( portal.sub ) | :wikitext(portal.sub) | ||
end | end | ||
end | end | ||
return | return tostring(root) | ||
end | end | ||
return p | return p | ||