Zweck Bearbeiten

Das Modul NewsFeed generiert einen dynamischen, JSON-basierten Nachrichtenblock für Wikonia. Es dient zur Anzeige von Artikel-News, internen Mitteilungen und Funfacts – vollständig ohne manuelle Pflege auf der Hauptseite.

Funktionsweise Bearbeiten

Das Modul liest drei JSON-Dateien aus:

Jede Datei enthält eine Liste von Einträgen mit Metadaten (Datum, Priorität, Turnus etc.). Aus diesen Daten erzeugt das Modul eine sortierte, ratio-basierte Auswahl für die Anzeige.

Datenfelder Bearbeiten

Feld Typ Beschreibung
title Text / Wikitext Titel oder Haupttext (Wikitext erlaubt, z. B. Artikel)
text Text (optional) Kurzbeschreibung oder Unterzeile
from Datum (YYYY-MM-DD) Startdatum des Zeitraums
to Datum (YYYY-MM-DD) Enddatum des Zeitraums
priority Zahl Gewichtung (höhere Werte werden bevorzugt)
persistent Bool Bleibt sichtbar, auch wenn abgelaufen
turnus Text/Zahl Wiederkehrintervall („day“, „week“, „month“, „year“ oder Zahl in Tagen)
highlight Bool Markiert wichtige Einträge (CSS-Klasse .highlight)

Anzeigeverhalten Bearbeiten

  • Artikel- und Intern-News benötigen ein gültiges Datum oder Turnus/Persistent.
  • Funfacts ohne Datum gelten automatisch als aktiv und werden zufällig gemischt.
  • Alte oder abgelaufene News werden ausgeblendet, sofern nicht persistent.
  • Verhältnis der angezeigten Mengen wird über den Parameter ratio gesteuert.

Zufallssteuerung und Cache Bearbeiten

Sofern mehr als die in der Ratio vorgesehene Anzahl an Fun-Facts vorhanden sind werden aktive Fakten, ohne Datumsangabe zufällig auslost. Dabei wird, um das generelle Caching der Startseite nicht zu gefährden ein Seed erstellt, der alle 24 Stunden erneuert wird. sobald der Seed gesetzt wurde, bleiben die Fakten für alle Aufrufe (für jeden Benutzer) für diesen Zeitraum gleich.

Ausnahmen:

  • Es werden andere Inhalte hinzugefügt, oder die JSON-Daten aktualisiert, dies löst Rekursiv eine neu Hauptseitenversion aus.
  • Der Cache der Startseite wird manuell geleert über die Weboberfläche, oder durch einen Server-Admin.

Aufruf Bearbeiten

{{#invoke:NewsFeed|render|total=11|ratio=5:3:3}}

Parameter Bearbeiten

Parameter Beschreibung Standard
total Gesamtzahl der Einträge 10
ratio Verhältnis Artikel:Intern:Funfacts 5:3:3

Beispiel-JSON Bearbeiten

[
  {
    "title": "Heute erscheint das neue [[Pixel 10]]",
    "text": "KI-Chip, neue Kamera, stärkerer Fokus auf Nachhaltigkeit.",
    "from": "2025-11-01",
    "to": "2025-11-15",
    "priority": 2,
    "persistent": false,
    "turnus": "year",
    "highlight": true
  }
]

Ausgabe Bearbeiten

Erzeugt eine HTML-Liste:

<ul class="newslist">
  <li><b>1.–15. November 2025:</b> Heute erscheint das neue <a href="/wiki/Pixel_10">Pixel 10</a> – KI-Chip, neue Kamera …</li>
</ul>

Hinweise Bearbeiten

  • Das Modul nutzt serverseitige Zeitzone aus $wgLocaltimezone.
  • Alle JSON-Dateien liegen im Modul:-Namensraum, um sie vor normaler Bearbeitung zu schützen.
  • Fehlerhafte JSON-Syntax führt zu einem klaren Lua-Fehler, der in der Ausgabe angezeigt wird.

-- ==============================================================
-- Modul:NewsFeed  (fix: no mw.list.contains; robust fill; total cap)
-- ==============================================================

local p = {}
local lang = mw.language.getContentLanguage()

-----------------------------------------------------------------
-- Helpers
-----------------------------------------------------------------

local function readJSON(suffix)
    local ok, data = pcall(function()
        local t = mw.title.new('Modul:NewsDaten.' .. suffix .. '.json')
        local content = t and t:getContent() or nil
        if not content or content == '' then return {} end
        return mw.text.jsonDecode(content)
    end)
    if ok and type(data) == 'table' then return data end
    return {}
end

local function parseDate(s)
    if not s or s == '' then return nil end
    local y, m, d = s:match("^(%d+)%-(%d+)%-(%d+)$")
    if not y then return nil end
    return os.time{ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = 12 }
end

-- Lokaler Timestamp über MediaWiki ermitteln
local function now()
    return tonumber(lang:formatDate('U'))
end

local function relativeDate(dateStr)
    local today = now()
    local d = parseDate(dateStr)
    if not d then return "" end

    -- Tagesanfänge normalisieren
    local todayDate = os.date("!*t", today)
    todayDate.hour, todayDate.min, todayDate.sec = 12, 0, 0
    local normalizedToday = os.time(todayDate)

    local diff = math.floor((normalizedToday - d) / 86400)

    if diff >= -1 and diff <= 1 then
        if diff < 0 then return "Morgen"
        elseif diff > 0 then return "Gestern"
        else return "Heute" end
    elseif math.abs(diff) < 7 then
        return (diff < 0) and ("in " .. (-diff) .. " Tagen") or (diff .. " Tage her")
    else
        return lang:formatDate("j. F Y", dateStr)
    end
end


local function isActive(item)
    local today = os.time()
    local from, to = parseDate(item.from), parseDate(item.to)

    -- Standardlaufzeit
    if from and to and today >= from and today <= to then
        return true
    end

    -- Persistent
    if item.persistent then
        return true
    end

    -- Turnus (wiederkehrend)
    if item.turnus then
        local ref = from or today
        local nowt = os.date("*t", today)
        local reft = os.date("*t", ref)

        if item.turnus == "year" then
            return (nowt.month == reft.month and nowt.day == reft.day)
        elseif item.turnus == "month" then
            return (nowt.day == reft.day)
        elseif item.turnus == "week" then
            local diffDays = math.floor((today - ref) / 86400)
            return diffDays % 7 == 0
        else
            local n = tonumber(item.turnus)
            if n and n > 0 then
                local diffDays = math.floor((today - ref) / 86400)
                return diffDays % n == 0
            end
        end
    end

    return false
end

local function compare(a, b)
    local pa, pb = a.priority or 0, b.priority or 0
    if pa ~= pb then return pa > pb end
    -- Neuere "from" nach vorn
    return (a.from or "") > (b.from or "")
end

local function take(list, quota)
    local out = {}
    local n = math.min(#list, quota)
    for i = 1, n do out[#out+1] = list[i] end
    return out, (quota - n)
end

local function merge(a, b)
    local r = {}
    for i=1,#a do r[#r+1] = a[i] end
    for i=1,#b do r[#r+1] = b[i] end
    return r
end

local function contains(tbl, needle)
    for i=1,#tbl do
        if tbl[i] == needle then return true end -- Referenzgleichheit reicht
    end
    return false
end

-----------------------------------------------------------------
-- Rendering
-----------------------------------------------------------------

function p.render(frame)
    local args = frame:getParent() and frame:getParent().args or frame.args
    local total = tonumber(args.total) or 10
    local ratio = mw.text.split(args.ratio or "5:3:3", ":")

    local qA = tonumber(ratio[1]) or 5
    local qI = tonumber(ratio[2]) or 3
    local qF = tonumber(ratio[3]) or 2

-----------------------------------------------------------------
-- Daten einlesen
-----------------------------------------------------------------

local A = readJSON("Artikel")
local I = readJSON("Intern")
local F = readJSON("Funfacts")

-----------------------------------------------------------------
-- Filtern & Sonderlogik Funfacts
-----------------------------------------------------------------

local aAct, iAct, fAct = {}, {}, {}

-- Artikel & Intern nur, wenn sie isActive() erfüllen
for _, v in ipairs(A) do
    if isActive(v) then table.insert(aAct, v) end
end
for _, v in ipairs(I) do
    if isActive(v) then table.insert(iAct, v) end
end

-- Funfacts: aktiv, wenn sie isActive() erfüllen ODER kein Datum haben
for _, v in ipairs(F) do
    if isActive(v) or (not v.from and not v.to) then
        table.insert(fAct, v)
    end
end

-- Zufällige Reihenfolge bei datumsfreien Funfacts
if #fAct > 1 then
    math.randomseed(tonumber(os.date("%Y%m%d")))
    for i = #fAct, 2, -1 do
        local j = math.random(i)
        fAct[i], fAct[j] = fAct[j], fAct[i]
    end
end

-- Funfacts ohne Datum: zufällige Auswahl
local funNoDate = {}
for _, v in ipairs(fAct) do
    if not v.from and not v.to then
        table.insert(funNoDate, v)
    end
end

-- Wenn mehrere ohne Datum → zufällig mischen
if #funNoDate > 1 then
    math.randomseed(os.time())
    for i = #funNoDate, 2, -1 do
        local j = math.random(i)
        funNoDate[i], funNoDate[j] = funNoDate[j], funNoDate[i]
    end
end

-- Die gemischten Funfacts wieder vorn anhängen
fAct = funNoDate


    table.sort(aAct, compare)
    table.sort(iAct, compare)
    table.sort(fAct, compare)

    -- Quoten ziehen
    local pickA, remA = take(aAct, qA)
    local pickI, remI = take(iAct, qI)
    local pickF, remF = take(fAct, qF)

    local result = {}
    for _,v in ipairs(pickA) do result[#result+1] = v end
    for _,v in ipairs(pickI) do result[#result+1] = v end
    for _,v in ipairs(pickF) do result[#result+1] = v end

    -- Auffüllen, wenn Quoten nicht erreicht
    local deficit = remA + remI + remF
    if deficit > 0 then
        local pool = merge(merge(aAct, iAct), fAct)
        table.sort(pool, compare)
        for _, v in ipairs(pool) do
            if not contains(result, v) then
                result[#result+1] = v
                deficit = deficit - 1
                if deficit <= 0 then break end
            end
        end
    end

    -- Total-Limit durchsetzen
    if #result > total then
        local trimmed = {}
        for i=1,total do trimmed[i] = result[i] end
        result = trimmed
    end

-- Render
local out = {}
out[#out+1] = '<ul class="newslist">'
for _, item in ipairs(result) do
    local cls = item.highlight and ' class="highlight"' or ''

    -- prüfen, ob es überhaupt ein Datum gibt
    local hasDate = (item.from and item.from ~= '') or (item.to and item.to ~= '')
    local dateDisplay
    if hasDate then
        if item.from and item.to and item.from ~= item.to then
            dateDisplay = relativeDate(item.from) .. " – " .. relativeDate(item.to)
        else
            dateDisplay = relativeDate(item.from or item.to)
        end
    end

    local prefix = dateDisplay and ('<b>' .. dateDisplay .. ':</b> ') or ''
    local title  = frame:preprocess(item.title or "")
    local text   = item.text and (' – ' .. mw.text.nowiki(item.text)) or ''

    out[#out+1] = string.format('<li%s>%s%s%s</li>', cls, prefix, title, text)
end
out[#out+1] = '</ul>'

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

return p