Modul:ProjectTasks: Unterschied zwischen den Versionen

Aus Kyffhäuser KI
Zur Navigation springen Zur Suche springen
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…“
 
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 mermaidId(title)
local function wikiLink(title, label)
   local base = mw.ustring.gsub(title, "[^%w]", "_")
   title = canonicalTitle(title)
   if mw.ustring.match(base, "^%d") then base = "T_" .. base end
   if not title or title == "" then return "" end
   if #base > 40 then
   label = label or stripNs(title)
    local h = mw.hash.hashValue("md5", title):sub(1, 10)
  return string.format("[[%s|%s]]", title, label)
    base = base:sub(1, 30) .. "_" .. h
  end
  return base
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" then
   if type(v) == "table" and v[1] ~= nil then
    if v[1] ~= nil then
    return valueToString(v[1])
      return valueToString(v[1])
    end
   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 = valueToString(it)
local t = canonicalTitle(valueToString(it))
    if t and t ~= "" then table.insert(out, t) end
if t then table.insert(out, t) end
   end
   end
   return out
   return out
end
end


local function wikiLink(title, label)
-- IMPORTANT: robust title extraction from SMW results
   if not title or title == "" then return "" end
local function rowTitle(row)
   label = label or stripNs(title)
  local t = row.fulltext or row.title
   return string.format("[[%s|%s]]", title, label)
   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


-- ---------- core: load all tasks for a project ----------
-- ---------- 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",
    "mainlabel=-",
     "limit=2000"
     "limit=2000"
   }
   }
   local res = mw.smw.ask(q) or {}
   local res = mw.smw.ask(q) or {}


  -- tasks[title] = { status=..., parent=..., deps={...}, blockers={...} }
   local tasks = {}
   local tasks = {}


   for _, row in ipairs(res) do
   for _, row in ipairs(res) do
     local title = row.fulltext or row[1]
     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
  -- 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
   end
   end
Zeile 114: Zeile 126:
end
end


-- ---------- compute blocked-by for each task ----------
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
local function isDone(status)
  return norm(status) == "done"
end
local function isActive(status)
  status = norm(status)
  return status ~= "done" and status ~= "onhold"
end
end


-- ========== PUBLIC: Ready/Blocked lists ==========
-- ========== PUBLIC: Ready/Blocked lists ==========
-- Usage:
-- {{#invoke:ProjectTasks|readyBlocked|project=Project:MeinProjekt}}
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=Project:... fehlt."
     return "Fehler: project=... fehlt."
   end
   end


   local tasks = loadProjectTasks(project)
   local tasks = loadProjectTasks(project)


   local ready = {}
   local ready, blocked, onhold, done = {}, {}, {}, {}
  local blocked = {}
  local onhold = {}
  local 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 blockedBy = computeBlockedBy(tasks, title)
     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 #blockedBy == 0 then
       if #bb == 0 then table.insert(ready, title) else table.insert(blocked, title) end
        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)
  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, "! 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 ==========
-- 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)
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 -- optional Task:...
   local root = args.root or parentArgs.root


   if not project or project == "" then
   if not project or project == "" then
     return "Fehler: project=Project:... fehlt."
     return "Fehler: project=... fehlt."
   end
   end


   local tasks = loadProjectTasks(project)
   local tasks = loadProjectTasks(project)


  -- children[parentTitle] = { child1, child2, ... }
   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


  -- roots = tasks without parent (or parent not in project)
   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 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)) ..
     local line = string.rep("*", math.min(depth + 1, 6)) ..
       " " .. wikiLink(t) .. badge ..
       " " .. 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 kids = children[t] or {}
    for _, c in ipairs(kids) 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
    local lines = renderNode(r, 0)
     for _, l in ipairs(renderNode(r, 0)) do table.insert(out, l) end
     for _, l in ipairs(lines) do table.insert(out, l) end
   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