Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

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.
Warning: You are not logged in. Your IP address will be publicly visible if you make any edits. If you log in or create an account, your edits will be attributed to your username, along with other benefits.
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 server-side portal widgets for BattlestarWiki.
-- Provides dynamic portal widgets for BattlestarWiki:
--  stats bar, newest page, random image, colonial calendar
--
--
-- Functions:
-- Usage (from Template:Portal/Layout):
--  colonialCalendar  — "On this day in BSG" sidebar widget
--  {{#invoke:Portal|stats|category=Characters (RDM)|episodes=73|characters=62|seasons=4|year=2003}}
--  relatedPortals    — related-portals tile grid
--  {{#invoke:Portal|newestPage|category=Characters (RDM)}}
--
--   {{#invoke:Portal|randomImage|category=Images (RDM)}}
-- NOTE: stats, newest article, and image carousel are handled
--   {{#invoke:Portal|colonialCalendar|category=RDM}}
-- entirely by the portal JS in MediaWiki:Common.js, not Lua.
-- The JS reads data-category / data-listpage attributes set by
-- Template:Portal/Layout and makes live API calls.


local p = {}
local p = {}
local mwHtml   = mw.html
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 BSG broadcast dates.
-- Covers real-world broadcast dates and in-universe events.
-- To extend: add entries here, or extract to Module:Portal/Calendar
-- Editors can extend this table or replace it with a data module.
-- (see DEPLOYMENT.md) so non-Lua editors can add dates directly.
------------------------------------------------------------------------
------------------------------------------------------------------------


local CALENDAR = {
local CALENDAR = {
     ['1-14'] = {
     ['1-14'] = {
         { year = '2005', text = '[[33 (episode)|33]] premieres on Sci Fi Channel — first regular RDM episode' },
         { 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 = "[[Kobol's Last Gleaming, Part II]] airs — Season 1 finale" },
         { 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 Galactica dive sequence' },
         { 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 frame = mw.getCurrentFrame()
     local m = tonumber(mw.getCurrentFrame():callParserFunction('#time', 'n'))
    local m = tonumber( frame:callParserFunction( '#time', 'n' ) )
     local d = tonumber(mw.getCurrentFrame():callParserFunction('#time', 'j'))
     local d = tonumber( frame:callParserFunction( '#time', 'j' ) )
     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 root = mwHtml.create( 'div' ):addClass( 'portal-calendar' )
     local sub = root:tag('div'):addClass('portal-calendar-sub')
    root:tag( 'div' ):addClass( 'portal-calendar-date' ):wikitext( dateDisplay )
         :wikitext('Real-world BSG broadcast history')
    root:tag( 'div' ):addClass( 'portal-calendar-sub' )
         :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( ' \xe2\x80\x94 ' .. ev.text )
                 :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


------------------------------------------------------------------------
------------------------------------------------------------------------
-- Related Portals
-- Public: relatedPortals
-- Args:
-- Args: type = 'series' | 'topic'
--  type     "series" (default) or "topic"
-- Renders the related-portals tile grid appropriate for the portal type.
--   exclude  Full page title to hide (this portal itself)
------------------------------------------------------------------------
------------------------------------------------------------------------


local SERIES_PORTALS = {
local SERIES_PORTALS = {
     { title = 'Portal:Battlestar Galactica (TOS)', label = 'Original Series',    sub = 'TOS &middot; 1978' },
     { 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 &middot; 2003&ndash;2009' },
     { title = 'Portal:Battlestar Galactica (RDM)', label = 'Re-imagined Series', sub = 'RDM · 2003–2009' },
     { title = 'Portal:Caprica',                    label = 'Caprica',            sub = 'Prequel &middot; 2010&ndash;2011' },
     { title = 'Portal:Caprica',                    label = 'Caprica',            sub = 'Prequel · 2010–2011' },
     { title = 'Portal:Blood and Chrome',          label = 'Blood &amp; Chrome', sub = 'Prequel &middot; 2012' },
     { title = 'Portal:Blood and Chrome',          label = 'Blood &amp; Chrome', sub = 'Prequel · 2012' },
}
}


local TOPIC_PORTALS = {
local TOPIC_PORTALS = {
     { title = 'Portal:Characters', label = 'Characters', sub = 'All series' },
     { title = 'Portal:Characters', label = 'Characters', sub = 'All series' },
     { title = 'Portal:Ships',       label = 'Ships',       sub = 'All series' },
     { title = 'Portal:Ships',     label = 'Ships',     sub = 'All series' },
     { title = 'Portal:Cylons',     label = 'Cylons',     sub = 'All series' },
     { title = 'Portal:Cylons',     label = 'Cylons',     sub = 'All series' },
     { title = 'Portal:Episodes',   label = 'Episodes',   sub = 'All series' },
     { 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  ) do combined[#combined + 1] = v end
     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 frame:preprocess( tostring( root ) )
     return tostring(root)
end
end


return p
return p

To protect the wiki against automated edit spam, please solve the following captcha:

Cancel Editing help (opens in new window)

  [] · [[]] · [[|]] · {{}} · · “” ‘’ «» ‹› „“ ‚‘ · ~ | ° &nbsp; · ± × ÷ ² ³ ½ · §
     [[Category:]] · [[:File:]] · [[Special:MyLanguage/]] · <code></code> · <nowiki></nowiki> <code><nowiki></nowiki></code> · <syntaxhighlight></syntaxhighlight> · <includeonly></includeonly> · <noinclude></noinclude> · #REDIRECT[[]] · <translate></translate> · <languages/> · {{#translation:}} · <tvar|></> · {{DEFAULTSORT:}} · <categorytree></categorytree> · <div style="clear:both;"></div> <s></s>


Your changes will be visible immediately.
  • For testing, please use the sandbox instead.
  • On talk pages, please sign your comment by typing four tildes (~~~~).

Page included on this page: