Modul:ProjectTasks: Unterschied zwischen den Versionen
Zur Navigation springen
Zur Suche springen
Markus (Diskussion | Beiträge) Die Seite wurde neu angelegt: „local p = {} -- ---------- helpers ---------- local function norm(s) if not s then return "" end s = mw.text.trim(tostring(s)) return mw.ustring.lower(s) end local function stripNs(title) return mw.ustring.gsub(title or "", "^[^:]+:", "") end local function mermaidId(title) local base = mw.ustring.gsub(title, "[^%w]", "_") if mw.ustring.match(base, "^%d") then base = "T_" .. base end if #base > 40 then local h = mw.hash.hashValue("md5…“ |
Markus (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| (4 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 2: | Zeile 2: | ||
-- ---------- helpers ---------- | -- ---------- helpers ---------- | ||
local function canonicalTitle(s) | |||
if not s then return nil end | |||
s = mw.text.trim(tostring(s)) | |||
-- remove surrounding [[...]] | |||
s = mw.ustring.gsub(s, "^%[%[", "") | |||
s = mw.ustring.gsub(s, "%]%]$", "") | |||
s = mw.ustring.gsub(s, "%]%].*$", "") -- if extra garbage after ]] | |||
s = mw.ustring.gsub(s, "%[%[", "") | |||
s = mw.ustring.gsub(s, "%]%]", "") | |||
-- if "Title|Label" -> keep only Title | |||
local pipePos = mw.ustring.find(s, "|", 1, true) | |||
if pipePos then | |||
s = mw.ustring.sub(s, 1, pipePos - 1) | |||
end | |||
s = mw.text.trim(s) | |||
if s == "" then return nil end | |||
return s | |||
end | |||
local function norm(s) | local function norm(s) | ||
if not s then return "" end | if not s then return "" end | ||
| Zeile 12: | Zeile 34: | ||
end | end | ||
local function | local function wikiLink(title, label) | ||
title = canonicalTitle(title) | |||
if | if not title or title == "" then return "" end | ||
label = label or stripNs(title) | |||
return string.format("[[%s|%s]]", title, label) | |||
end | end | ||
| Zeile 47: | Zeile 66: | ||
local v = po[key] | local v = po[key] | ||
if not v then return nil end | if not v then return nil end | ||
if type(v) == "table" | if type(v) == "table" and v[1] ~= nil then | ||
return valueToString(v[1]) | |||
end | end | ||
return valueToString(v) | return valueToString(v) | ||
end | end | ||
local function listPages(po, key) | local function listPages(po, key) | ||
| Zeile 59: | Zeile 78: | ||
local v = po[key] | local v = po[key] | ||
for _, it in ipairs(asList(v)) do | for _, it in ipairs(asList(v)) do | ||
local t = canonicalTitle(valueToString(it)) | |||
if t then table.insert(out, t) end | |||
end | end | ||
return out | return out | ||
end | end | ||
local function | -- IMPORTANT: robust title extraction from SMW results | ||
if not | local function rowTitle(row) | ||
local t = row.fulltext or row.title | |||
return | if not t and row[1] then | ||
if type(row[1]) == "string" then t = row[1] | |||
elseif type(row[1]) == "table" and row[1].fulltext then t = row[1].fulltext end | |||
end | |||
return canonicalTitle(t) | |||
end | end | ||
-- ---------- | -- ---------- load all tasks for a project ---------- | ||
local function loadProjectTasks(project) | local function loadProjectTasks(project) | ||
local q = { | local q = { | ||
| Zeile 78: | Zeile 101: | ||
"?Teil von", | "?Teil von", | ||
"?Abhängig von", | "?Abhängig von", | ||
"limit=2000" | "limit=2000" | ||
} | } | ||
local res = mw.smw.ask(q) or {} | local res = mw.smw.ask(q) or {} | ||
local tasks = {} | local tasks = {} | ||
for _, row in ipairs(res) do | for _, row in ipairs(res) do | ||
local title = row | local title = rowTitle(row) | ||
if title then | if title then | ||
local po = getPrintouts(row) | local po = getPrintouts(row) | ||
| Zeile 93: | Zeile 114: | ||
local parent = firstText(po, "Teil von") | local parent = firstText(po, "Teil von") | ||
local deps = listPages(po, "Abhängig von") | local deps = listPages(po, "Abhängig von") | ||
if st == "" then st = "todo" end | if st == "" then st = "todo" end | ||
| Zeile 100: | Zeile 120: | ||
tasks[title].parent = parent | tasks[title].parent = parent | ||
tasks[title].deps = deps | tasks[title].deps = deps | ||
end | end | ||
end | end | ||
| Zeile 114: | Zeile 126: | ||
end | end | ||
local function computeBlockedBy(tasks, title) | local function computeBlockedBy(tasks, title) | ||
local data = tasks[title] | local data = tasks[title] | ||
| Zeile 130: | Zeile 141: | ||
table.sort(blockedBy) | table.sort(blockedBy) | ||
return blockedBy | return blockedBy | ||
end | end | ||
-- ========== PUBLIC: Ready/Blocked lists ========== | -- ========== PUBLIC: Ready/Blocked lists ========== | ||
function p.readyBlocked(frame) | function p.readyBlocked(frame) | ||
local args = frame.args | local args = frame.args | ||
| Zeile 149: | Zeile 149: | ||
local project = args.project or parentArgs.project | local project = args.project or parentArgs.project | ||
if not project or project == "" then | if not project or project == "" then | ||
return "Fehler: project= | return "Fehler: project=... fehlt." | ||
end | end | ||
local tasks = loadProjectTasks(project) | local tasks = loadProjectTasks(project) | ||
local ready = {} | local ready, blocked, onhold, done = {}, {}, {}, {} | ||
for title, data in pairs(tasks) do | for title, data in pairs(tasks) do | ||
local st = norm(data.status) | local st = norm(data.status) | ||
local | local bb = computeBlockedBy(tasks, title) | ||
if st == "done" then | if st == "done" then | ||
| Zeile 168: | Zeile 165: | ||
table.insert(onhold, title) | table.insert(onhold, title) | ||
else | else | ||
if # | if #bb == 0 then table.insert(ready, title) else table.insert(blocked, title) end | ||
end | end | ||
end | end | ||
table.sort(ready) | table.sort(ready); table.sort(blocked); table.sort(onhold); table.sort(done) | ||
local function renderList(list, showBlockedBy) | local function renderList(list, showBlockedBy) | ||
| Zeile 186: | Zeile 176: | ||
table.insert(out, '{| class="wikitable sortable" style="width:100%;"') | table.insert(out, '{| class="wikitable sortable" style="width:100%;"') | ||
table.insert(out, "! Task") | table.insert(out, "! Task") | ||
if showBlockedBy then | if showBlockedBy then table.insert(out, "! Blockiert durch") end | ||
table.insert(out, "! Status") | table.insert(out, "! Status") | ||
for _, t in ipairs(list) do | for _, t in ipairs(list) do | ||
| Zeile 225: | Zeile 213: | ||
-- ========== PUBLIC: Tree view ========== | -- ========== PUBLIC: Tree view ========== | ||
function p.tree(frame) | function p.tree(frame) | ||
local args = frame.args | local args = frame.args | ||
local parentArgs = frame:getParent() and frame:getParent().args or {} | local parentArgs = frame:getParent() and frame:getParent().args or {} | ||
local project = args.project or parentArgs.project | local project = args.project or parentArgs.project | ||
local root = args.root or parentArgs.root | local root = args.root or parentArgs.root | ||
if not project or project == "" then | if not project or project == "" then | ||
return "Fehler: project= | return "Fehler: project=... fehlt." | ||
end | end | ||
local tasks = loadProjectTasks(project) | local tasks = loadProjectTasks(project) | ||
local children = {} | local children = {} | ||
for title, data in pairs(tasks) do | for title, data in pairs(tasks) do | ||
| Zeile 251: | Zeile 235: | ||
for _, list in pairs(children) do table.sort(list) end | for _, list in pairs(children) do table.sort(list) end | ||
local roots = {} | local roots = {} | ||
if root and root ~= "" then | if root and root ~= "" then | ||
| Zeile 263: | Zeile 246: | ||
end | end | ||
table.sort(roots) | table.sort(roots) | ||
end | |||
local function badgeFor(t) | |||
local st = tasks[t] and tasks[t].status or "todo" | |||
local bb = computeBlockedBy(tasks, t) | |||
if norm(st) == "done" then return " ✅" end | |||
if norm(st) == "onhold" then return " ⏸️" end | |||
if #bb > 0 then return " ⛔" end | |||
if norm(st) == "doing" then return " ▶️" end | |||
return "" | |||
end | end | ||
| Zeile 268: | Zeile 261: | ||
depth = depth or 0 | depth = depth or 0 | ||
local st = tasks[t] and tasks[t].status or "todo" | local st = tasks[t] and tasks[t].status or "todo" | ||
local line = string.rep("*", math.min(depth + 1, 6)) .. | local line = string.rep("*", math.min(depth + 1, 6)) .. | ||
" " .. wikiLink(t) .. | " " .. wikiLink(t) .. badgeFor(t) .. | ||
" <span style='opacity:.7'>(" .. st .. ")</span>" | " <span style='opacity:.7'>(" .. st .. ")</span>" | ||
local out = { line } | local out = { line } | ||
for _, c in ipairs(children[t] or {}) do | |||
local sub = renderNode(c, depth + 1) | local sub = renderNode(c, depth + 1) | ||
for _, l in ipairs(sub) do table.insert(out, l) end | for _, l in ipairs(sub) do table.insert(out, l) end | ||
| Zeile 297: | Zeile 276: | ||
table.insert(out, "== Task Tree ==") | table.insert(out, "== Task Tree ==") | ||
for _, r in ipairs(roots) do | for _, r in ipairs(roots) do | ||
for _, l in ipairs(renderNode(r, 0)) do table.insert(out, l) end | |||
for _, l in ipairs( | |||
end | end | ||
return table.concat(out, "\n") | return table.concat(out, "\n") | ||
Aktuelle Version vom 25. Februar 2026, 05:00 Uhr
Die Dokumentation für dieses Modul kann unter Modul:ProjectTasks/Doku erstellt werden
local p = {}
-- ---------- helpers ----------
local function canonicalTitle(s)
if not s then return nil end
s = mw.text.trim(tostring(s))
-- remove surrounding [[...]]
s = mw.ustring.gsub(s, "^%[%[", "")
s = mw.ustring.gsub(s, "%]%]$", "")
s = mw.ustring.gsub(s, "%]%].*$", "") -- if extra garbage after ]]
s = mw.ustring.gsub(s, "%[%[", "")
s = mw.ustring.gsub(s, "%]%]", "")
-- if "Title|Label" -> keep only Title
local pipePos = mw.ustring.find(s, "|", 1, true)
if pipePos then
s = mw.ustring.sub(s, 1, pipePos - 1)
end
s = mw.text.trim(s)
if s == "" then return nil end
return s
end
local function norm(s)
if not s then return "" end
s = mw.text.trim(tostring(s))
return mw.ustring.lower(s)
end
local function stripNs(title)
return mw.ustring.gsub(title or "", "^[^:]+:", "")
end
local function wikiLink(title, label)
title = canonicalTitle(title)
if not title or title == "" then return "" end
label = label or stripNs(title)
return string.format("[[%s|%s]]", title, label)
end
local function getPrintouts(row)
if row.printouts then return row.printouts end
return row
end
local function asList(v)
if not v then return {} end
if type(v) ~= "table" then return { v } end
return v
end
local function valueToString(v)
if type(v) == "table" then
if v.fulltext then return v.fulltext end
if v.value then return tostring(v.value) end
if v[1] then return valueToString(v[1]) end
end
if type(v) == "string" then return v end
if type(v) == "number" then return tostring(v) end
return nil
end
local function firstText(po, key)
local v = po[key]
if not v then return nil end
if type(v) == "table" and v[1] ~= nil then
return valueToString(v[1])
end
return valueToString(v)
end
local function listPages(po, key)
local out = {}
local v = po[key]
for _, it in ipairs(asList(v)) do
local t = canonicalTitle(valueToString(it))
if t then table.insert(out, t) end
end
return out
end
-- IMPORTANT: robust title extraction from SMW results
local function rowTitle(row)
local t = row.fulltext or row.title
if not t and row[1] then
if type(row[1]) == "string" then t = row[1]
elseif type(row[1]) == "table" and row[1].fulltext then t = row[1].fulltext end
end
return canonicalTitle(t)
end
-- ---------- load all tasks for a project ----------
local function loadProjectTasks(project)
local q = {
string.format("[[Gehört zu Projekt::%s]]", project),
"?Status",
"?Teil von",
"?Abhängig von",
"limit=2000"
}
local res = mw.smw.ask(q) or {}
local tasks = {}
for _, row in ipairs(res) do
local title = rowTitle(row)
if title then
local po = getPrintouts(row)
local st = norm(firstText(po, "Status"))
local parent = firstText(po, "Teil von")
local deps = listPages(po, "Abhängig von")
if st == "" then st = "todo" end
tasks[title] = tasks[title] or { deps = {}, blockers = {} }
tasks[title].status = st
tasks[title].parent = parent
tasks[title].deps = deps
end
end
return tasks
end
local function computeBlockedBy(tasks, title)
local data = tasks[title]
if not data then return {} end
local blockedBy = {}
for _, dep in ipairs(data.deps or {}) do
local depData = tasks[dep]
local depStatus = depData and depData.status or "todo"
if norm(depStatus) ~= "done" then
table.insert(blockedBy, dep)
end
end
table.sort(blockedBy)
return blockedBy
end
-- ========== PUBLIC: Ready/Blocked lists ==========
function p.readyBlocked(frame)
local args = frame.args
local parentArgs = frame:getParent() and frame:getParent().args or {}
local project = args.project or parentArgs.project
if not project or project == "" then
return "Fehler: project=... fehlt."
end
local tasks = loadProjectTasks(project)
local ready, blocked, onhold, done = {}, {}, {}, {}
for title, data in pairs(tasks) do
local st = norm(data.status)
local bb = computeBlockedBy(tasks, title)
if st == "done" then
table.insert(done, title)
elseif st == "onhold" then
table.insert(onhold, title)
else
if #bb == 0 then table.insert(ready, title) else table.insert(blocked, title) end
end
end
table.sort(ready); table.sort(blocked); table.sort(onhold); table.sort(done)
local function renderList(list, showBlockedBy)
if #list == 0 then return "''(keine)''" end
local out = {}
table.insert(out, '{| class="wikitable sortable" style="width:100%;"')
table.insert(out, "! Task")
if showBlockedBy then table.insert(out, "! Blockiert durch") end
table.insert(out, "! Status")
for _, t in ipairs(list) do
local st = tasks[t] and tasks[t].status or "todo"
table.insert(out, "|-")
table.insert(out, "| " .. wikiLink(t))
if showBlockedBy then
local bb = computeBlockedBy(tasks, t)
local bbLinks = {}
for _, dep in ipairs(bb) do table.insert(bbLinks, wikiLink(dep)) end
table.insert(out, "| " .. (#bbLinks > 0 and table.concat(bbLinks, ", ") or "–"))
end
table.insert(out, "| " .. st)
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
local out = {}
table.insert(out, "== Ready ==")
table.insert(out, "Tasks, die startbar sind (alle Dependencies erledigt oder keine).")
table.insert(out, renderList(ready, false))
table.insert(out, "\n== Blocked ==")
table.insert(out, "Tasks, die blockiert sind (mindestens eine Dependency ist nicht ''done'').")
table.insert(out, renderList(blocked, true))
table.insert(out, "\n== On hold ==")
table.insert(out, renderList(onhold, false))
table.insert(out, "\n== Done ==")
table.insert(out, renderList(done, false))
return table.concat(out, "\n")
end
-- ========== PUBLIC: Tree view ==========
function p.tree(frame)
local args = frame.args
local parentArgs = frame:getParent() and frame:getParent().args or {}
local project = args.project or parentArgs.project
local root = args.root or parentArgs.root
if not project or project == "" then
return "Fehler: project=... fehlt."
end
local tasks = loadProjectTasks(project)
local children = {}
for title, data in pairs(tasks) do
local parent = data.parent
if parent and parent ~= "" then
children[parent] = children[parent] or {}
table.insert(children[parent], title)
end
end
for _, list in pairs(children) do table.sort(list) end
local roots = {}
if root and root ~= "" then
roots = { root }
else
for title, data in pairs(tasks) do
local parent = data.parent
if not parent or parent == "" or not tasks[parent] then
table.insert(roots, title)
end
end
table.sort(roots)
end
local function badgeFor(t)
local st = tasks[t] and tasks[t].status or "todo"
local bb = computeBlockedBy(tasks, t)
if norm(st) == "done" then return " ✅" end
if norm(st) == "onhold" then return " ⏸️" end
if #bb > 0 then return " ⛔" end
if norm(st) == "doing" then return " ▶️" end
return ""
end
local function renderNode(t, depth)
depth = depth or 0
local st = tasks[t] and tasks[t].status or "todo"
local line = string.rep("*", math.min(depth + 1, 6)) ..
" " .. wikiLink(t) .. badgeFor(t) ..
" <span style='opacity:.7'>(" .. st .. ")</span>"
local out = { line }
for _, c in ipairs(children[t] or {}) do
local sub = renderNode(c, depth + 1)
for _, l in ipairs(sub) do table.insert(out, l) end
end
return out
end
local out = {}
table.insert(out, "== Task Tree ==")
for _, r in ipairs(roots) do
for _, l in ipairs(renderNode(r, 0)) do table.insert(out, l) end
end
return table.concat(out, "\n")
end
return p