app/sprint.py + /sprint/{o}/{r} (rendu markdown, copier/télécharger)
+ /api/sprint en JSON: livré avec temps de cycle, plus lentes,
reporté, chiffres de période. Lien depuis le bandeau milestone.
Navigation: pilules Aujourd'hui/Prévisions et actions palette.
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""Sprint story (roadmap #4): the milestone's tale, written by the journal.
|
|
|
|
Nobody writes the end-of-milestone report by hand. Kanbante's
|
|
event journal already knows what shipped, how long it took and
|
|
what dragged — this module just turns that into clean markdown,
|
|
ready to archive in ObsiGate.
|
|
"""
|
|
|
|
|
|
def format_sprint_markdown(
|
|
repo: str,
|
|
milestone_title: str,
|
|
due_on: str | None,
|
|
period_label: str,
|
|
delivered: list[dict],
|
|
carried: list[dict],
|
|
counts: dict,
|
|
avg_cycle_days: float | None,
|
|
) -> str:
|
|
lines = [f"# 📖 Récit de sprint — {repo} · {milestone_title}", ""]
|
|
lines.append(f"*Période : {period_label}*")
|
|
if due_on:
|
|
lines.append(f"*Échéance du milestone : {due_on[:10]}*")
|
|
lines.append("")
|
|
|
|
lines.append(f"## ✅ Livré ({len(delivered)})")
|
|
if delivered:
|
|
for item in delivered:
|
|
cycle = (
|
|
f" — ⏱️ {item['cycle_days']} j de cycle"
|
|
if item.get("cycle_days") is not None
|
|
else ""
|
|
)
|
|
lines.append(
|
|
f"- [#{item['number']}]({item.get('html_url', '')}) "
|
|
f"{item.get('title', '')}{cycle}".rstrip()
|
|
)
|
|
else:
|
|
lines.append("- Rien n'a été fermé dans ce milestone pour l'instant.")
|
|
lines.append("")
|
|
|
|
slowest = sorted(
|
|
(d for d in delivered if d.get("cycle_days") is not None),
|
|
key=lambda d: -d["cycle_days"],
|
|
)[:3]
|
|
if slowest:
|
|
lines.append("## 🐌 Les plus lentes")
|
|
for item in slowest:
|
|
lines.append(
|
|
f"- #{item['number']} {item.get('title', '')} "
|
|
f"— {item['cycle_days']} j de cycle"
|
|
)
|
|
lines.append("")
|
|
|
|
lines.append(f"## ↩️ Reporté ({len(carried)})")
|
|
if carried:
|
|
for item in carried:
|
|
lines.append(
|
|
f"- #{item['number']} {item.get('title', '')} "
|
|
f"— encore en *{item.get('column_label', 'backlog')}*"
|
|
)
|
|
else:
|
|
lines.append("- Rien : tout le milestone est livré. 🎉")
|
|
lines.append("")
|
|
|
|
lines.append("## 📈 En chiffres")
|
|
lines.append(f"- Issues du milestone : {len(delivered) + len(carried)}")
|
|
lines.append(f"- Fermées : {len(delivered)}")
|
|
if avg_cycle_days is not None:
|
|
lines.append(f"- Temps de cycle moyen : {avg_cycle_days} j")
|
|
lines.append(f"- Déplacements de cartes : {counts.get('moved', 0)}")
|
|
lines.append(f"- Commentaires : {counts.get('commented', 0)}")
|
|
lines.append(f"- Réouvertures : {counts.get('reopened', 0)}")
|
|
lines.append("")
|
|
lines.append(
|
|
"*Récit généré par Kanbante à partir de son journal d'événements "
|
|
"et des issues Gitea du milestone.*"
|
|
)
|
|
return "\n".join(lines)
|