Modul:ProjectTasks
Die Dokumentation für dieses Modul kann unter Modul:ProjectTasks/Doku erstellt werden
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 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 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 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