Files
gitea-dashboard/src/gitea_dashboard/display.py
sylvain 50768db31f fix(audit): sort milestones, sort JSON export, extract test helper
- Fix milestone section using unsorted repos list
- Apply --sort to --format json output
- Rename _sort_repos to sort_repos (now used by cli.py)
- Extract shared make_repo helper to tests/helpers.py
- Move exporter import to module level in cli.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 04:07:41 +01:00

205 lines
6.2 KiB
Python

"""Formatage et affichage Rich du dashboard Gitea."""
from __future__ import annotations
from datetime import datetime, timezone
from rich.console import Console
from rich.table import Table
from gitea_dashboard.collector import RepoData
def _format_repo_name(repo: RepoData) -> str:
"""Formate le nom du repo avec les indicateurs visuels."""
indicators = []
if repo.is_fork:
indicators.append("[F]")
if repo.is_archived:
indicators.append("[A]")
if repo.is_mirror:
indicators.append("[M]")
if indicators:
return f"{repo.name} {' '.join(indicators)}"
return repo.name
def _format_relative_date(iso_date: str) -> str:
"""Convertit une date ISO en date relative lisible."""
try:
dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
except (ValueError, AttributeError):
return ""
now = datetime.now(timezone.utc)
delta = now - dt
days = delta.days
if days < 0:
return "dans le futur"
if days == 0:
return "aujourd'hui"
if days == 1:
return "il y a 1j"
if days < 30:
return f"il y a {days}j"
months = days // 30
if months < 12:
return f"il y a {months}m"
years = days // 365
return f"il y a {years}a"
def _format_release(release: dict | None) -> str:
"""Formate la release pour l'affichage."""
if release is None:
return "\u2014"
tag = release.get("tag_name", "")
published = release.get("published_at", "")
if published:
relative = _format_relative_date(published)
if relative:
return f"{tag} ({relative})"
return tag
def _colorize_milestone_due(due_on: str | None) -> str:
"""Retourne le style Rich selon la proximite de l'echeance.
- Rouge : echeance depassee
- Jaune : echeance dans les 7 prochains jours
- Vert : echeance dans plus de 7 jours
- Chaine vide : pas d'echeance definie
"""
if not due_on:
return ""
try:
dt = datetime.fromisoformat(due_on.replace("Z", "+00:00"))
except (ValueError, AttributeError):
return ""
now = datetime.now(timezone.utc)
delta = dt - now
days = delta.days
if days < 0:
return "red"
if days < 7:
return "yellow"
return "green"
def sort_repos(repos: list[RepoData], sort_key: str) -> list[RepoData]:
"""Trie la liste des repos selon le critere donne.
Args:
repos: Liste des repos a trier.
sort_key: Critere de tri parmi :
- "name" : alphabetique par nom (defaut)
- "issues" : par nombre d'issues ouvertes (decroissant)
- "release" : par date de derniere release (plus recent d'abord)
- "activity" : par date du dernier commit (plus recent d'abord)
"""
if sort_key == "name":
return sorted(repos, key=lambda r: r.name.lower())
if sort_key == "issues":
return sorted(repos, key=lambda r: r.open_issues, reverse=True)
if sort_key == "release":
# Repos sans release en dernier (date vide = epoch 0)
def release_date(r: RepoData) -> str:
if r.latest_release and r.latest_release.get("published_at"):
return r.latest_release["published_at"]
return ""
return sorted(repos, key=release_date, reverse=True)
if sort_key == "activity":
# Repos sans commit en dernier (date vide = epoch 0)
return sorted(repos, key=lambda r: r.last_commit_date or "", reverse=True)
return repos
def render_dashboard(
repos: list[RepoData],
console: Console | None = None,
sort_key: str = "name",
) -> None:
"""Affiche le dashboard complet dans le terminal.
- Tableau principal : nom repo, indicateurs (fork/archive/mirror),
issues ouvertes, derniere release (tag + date relative)
- Section milestones : par repo ayant des milestones,
nom, progression (closed/total), date echeance
Le parametre console permet l'injection pour les tests.
"""
if console is None:
console = Console()
if not repos:
console.print("Aucun repo trouve.")
return
# Tri des repos
sorted_repos = sort_repos(repos, sort_key)
# Tableau principal
table = Table(title="Gitea Dashboard")
table.add_column("Repo", style="bold")
table.add_column("Issues", justify="right")
table.add_column("Release")
table.add_column("Dernier commit")
for repo in sorted_repos:
name = _format_repo_name(repo)
issues_str = str(repo.open_issues)
issues_style = "red" if repo.open_issues > 0 else "green"
release_str = _format_release(repo.latest_release)
commit_str = (
_format_relative_date(repo.last_commit_date) if repo.last_commit_date else "\u2014"
)
table.add_row(
name,
f"[{issues_style}]{issues_str}[/{issues_style}]",
release_str,
commit_str,
)
console.print(table)
# Section milestones — uniquement si au moins un repo en a
repos_with_milestones = [r for r in sorted_repos if r.milestones]
if repos_with_milestones:
console.print()
console.print("[bold]Milestones[/bold]")
for repo in repos_with_milestones:
for ms in repo.milestones:
title = ms["title"]
closed = ms["closed_issues"]
total = ms["open_issues"] + ms["closed_issues"]
pct = round(closed / total * 100) if total > 0 else 0
line = f" {repo.name} / {title} : {closed}/{total} ({pct}%)"
due_on = ms.get("due_on")
if due_on:
# Extraire juste la date (YYYY-MM-DD)
try:
dt = datetime.fromisoformat(due_on.replace("Z", "+00:00"))
line += f" \u2014 echeance {dt.strftime('%Y-%m-%d')}"
except (ValueError, AttributeError):
pass
# Coloration selon la proximite de l'echeance
style = _colorize_milestone_due(due_on)
if style:
console.print(f"[{style}]{line}[/{style}]")
else:
console.print(line)