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 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", title):sub(1, 10)
base = base:sub(1, 30) .. "_" .. h
end
return base
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" then
if v[1] ~= nil then
return valueToString(v[1])
end
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 = valueToString(it)
if t and t ~= "" then table.insert(out, t) end
end
return out
end
local function wikiLink(title, label)
if not title or title == "" then return "" end
label = label or stripNs(title)
return string.format("[[%s|%s]]", title, label)
end
-- ---------- core: 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",
"mainlabel=-",
"limit=2000"
}
local res = mw.smw.ask(q) or {}
-- tasks[title] = { status=..., parent=..., deps={...}, blockers={...} }
local tasks = {}
for _, row in ipairs(res) do
local title = row.fulltext or row[1]
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
-- Build reverse blockers: if B depends on A => A blocks B (if A not done)
for t, data in pairs(tasks) do
for _, dep in ipairs(data.deps or {}) do
tasks[dep] = tasks[dep] or { deps = {}, blockers = {} }
table.insert(tasks[dep].blockers, t)
end
end
return tasks
end
-- ---------- compute blocked-by for each task ----------
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
local function isDone(status)
return norm(status) == "done"
end
local function isActive(status)
status = norm(status)
return status ~= "done" and status ~= "onhold"
end
-- ========== PUBLIC: Ready/Blocked lists ==========
-- Usage:
-- {{#invoke:ProjectTasks|readyBlocked|project=Project:MeinProjekt}}
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=Project:... fehlt."
end
local tasks = loadProjectTasks(project)
local ready = {}
local blocked = {}
local onhold = {}
local done = {}
for title, data in pairs(tasks) do
local st = norm(data.status)
local blockedBy = computeBlockedBy(tasks, title)
if st == "done" then
table.insert(done, title)
elseif st == "onhold" then
table.insert(onhold, title)
else
if #blockedBy == 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 ==========
-- Two modes:
-- (A) Project tree: {{#invoke:ProjectTasks|tree|project=Project:MeinProjekt}}
-- (B) Subtree for one task: {{#invoke:ProjectTasks|tree|root=Task:Foo|project=Project:MeinProjekt}}
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 -- optional Task:...
if not project or project == "" then
return "Fehler: project=Project:... fehlt."
end
local tasks = loadProjectTasks(project)
-- children[parentTitle] = { child1, child2, ... }
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
-- roots = tasks without parent (or parent not in project)
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 renderNode(t, depth)
depth = depth or 0
local st = tasks[t] and tasks[t].status or "todo"
local bb = computeBlockedBy(tasks, t)
local badge = ""
if st == "done" then
badge = " ✅"
elseif st == "onhold" then
badge = " ⏸️"
elseif #bb > 0 then
badge = " ⛔"
elseif st == "doing" then
badge = " ▶️"
end
local line = string.rep("*", math.min(depth + 1, 6)) ..
" " .. wikiLink(t) .. badge ..
" <span style='opacity:.7'>(" .. st .. ")</span>"
local out = { line }
local kids = children[t] or {}
for _, c in ipairs(kids) 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
local lines = renderNode(r, 0)
for _, l in ipairs(lines) do table.insert(out, l) end
end
return table.concat(out, "\n")
end
-- Usage: {{#invoke:ProjectTasks|badge|project=Project:MeinProjekt|task={{FULLPAGENAME}}}}
function p.badge(frame)
local args = frame.args
local parentArgs = frame:getParent() and frame:getParent().args or {}
local project = args.project or parentArgs.project
local task = args.task or parentArgs.task
if not project or project == "" or not task or task == "" then
return ""
end
local tasks = loadProjectTasks(project)
local data = tasks[task]
local st = data and norm(data.status) or "todo"
local bb = computeBlockedBy(tasks, task)
if st == "done" then return "✅ done" end
if st == "onhold" then return "⏸️ on hold" end
if #bb > 0 then return "⛔ blocked" end
if st == "doing" then return "▶️ doing" end
return "🟦 todo"
end
return p