feat(0.35.0): jalons en écriture depuis la page Projet (issue #26)
CI / test (push) Successful in 2m22s
CI / test (push) Successful in 2m22s
- Clients : create_milestone / update_milestone (cache invalidé) ; GitHub hérite des mêmes endpoints. - API Kanbante : création, modification (titre/échéance, échéance vidée = retirée), fermeture ; CSRF, validation de date avant toute écriture, erreurs forge en 502. - Page Projet : formulaire 'Nouveau jalon' + édition/ fermeture par jalon (project.js versionné). - Échéance à midi UTC : à minuit, le fuseau de l'instance Gitea faisait reculer la date affichée d'un jour (réel). - 291 tests verts (5 nouveaux, palier 24) ; cycle réel complet sur Projets/kanbante (jalons smoke #37/#38 créés, dates vérifiées, fermés).
This commit is contained in:
+133
@@ -3147,7 +3147,10 @@ async def project_page(request: Request, owner: str, repo: str):
|
||||
est = forecast_service.estimate(opened, weekly)
|
||||
milestones.append(
|
||||
{
|
||||
"id": m.get("id"),
|
||||
"title": m.get("title", ""),
|
||||
"description": m.get("description") or "",
|
||||
"due_iso": due_date.isoformat() if due_date else "",
|
||||
"total": total,
|
||||
"closed": closed,
|
||||
"open": opened,
|
||||
@@ -4373,6 +4376,136 @@ async def my_issues(request: Request):
|
||||
)
|
||||
|
||||
|
||||
def _milestone_due_iso(due: str) -> tuple[str | None, bool]:
|
||||
"""Validate a YYYY-MM-DD form date -> (ISO instant, ok)."""
|
||||
due = (due or "").strip()
|
||||
if not due:
|
||||
return None, True
|
||||
try:
|
||||
parsed = datetime.strptime(due, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return None, False
|
||||
# Midi UTC, pas minuit : Gitea renvoie l'échéance dans le
|
||||
# fuseau de son instance (Toronto = UTC-5) — à minuit UTC,
|
||||
# la date affichée reculait d'un jour (attrapé en réel).
|
||||
return parsed.strftime("%Y-%m-%dT12:00:00Z"), True
|
||||
|
||||
|
||||
@app.post("/api/repos/{owner}/{repo}/milestones")
|
||||
async def api_milestone_create(
|
||||
request: Request,
|
||||
owner: str,
|
||||
repo: str,
|
||||
title: str = Form(""),
|
||||
description: str = Form(""),
|
||||
due_on: str = Form(""),
|
||||
csrf_token: str = Form(""),
|
||||
):
|
||||
"""Create a milestone on the forge (issue #26)."""
|
||||
if not _csrf_valid(request, csrf_token):
|
||||
return _csrf_error()
|
||||
title = title.strip()
|
||||
if not title or len(title) > 255:
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": "Titre requis (255 caractères max)"},
|
||||
status_code=400,
|
||||
)
|
||||
due_iso, ok = _milestone_due_iso(due_on)
|
||||
if not ok:
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": "Échéance invalide (format AAAA-MM-JJ)"},
|
||||
status_code=400,
|
||||
)
|
||||
client, _cid, _ws = await _resolve_board_scope(request, owner, repo)
|
||||
try:
|
||||
milestone = await client.create_milestone(
|
||||
owner, repo, title, description.strip(), due_iso
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": f"La forge répond {exc.response.status_code}"},
|
||||
status_code=502,
|
||||
)
|
||||
return JSONResponse(
|
||||
{"ok": True, "milestone": {
|
||||
"id": milestone.get("id"),
|
||||
"title": milestone.get("title"),
|
||||
"due_on": milestone.get("due_on"),
|
||||
}},
|
||||
status_code=201,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/repos/{owner}/{repo}/milestones/{milestone_id}/update")
|
||||
async def api_milestone_update(
|
||||
request: Request,
|
||||
owner: str,
|
||||
repo: str,
|
||||
milestone_id: int,
|
||||
title: str = Form(""),
|
||||
due_on: str = Form(""),
|
||||
csrf_token: str = Form(""),
|
||||
):
|
||||
"""Edit a milestone's title / due date (issue #26).
|
||||
|
||||
An empty due date CLEARS the milestone's deadline.
|
||||
"""
|
||||
if not _csrf_valid(request, csrf_token):
|
||||
return _csrf_error()
|
||||
title = title.strip()
|
||||
if not title or len(title) > 255:
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": "Titre requis (255 caractères max)"},
|
||||
status_code=400,
|
||||
)
|
||||
due_iso, ok = _milestone_due_iso(due_on)
|
||||
if not ok:
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": "Échéance invalide (format AAAA-MM-JJ)"},
|
||||
status_code=400,
|
||||
)
|
||||
client, _cid, _ws = await _resolve_board_scope(request, owner, repo)
|
||||
try:
|
||||
milestone = await client.update_milestone(
|
||||
owner, repo, milestone_id, title=title,
|
||||
due_on=due_iso, clear_due=due_iso is None,
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": f"La forge répond {exc.response.status_code}"},
|
||||
status_code=502,
|
||||
)
|
||||
return {"ok": True, "milestone": {
|
||||
"id": milestone.get("id"),
|
||||
"title": milestone.get("title"),
|
||||
"due_on": milestone.get("due_on"),
|
||||
}}
|
||||
|
||||
|
||||
@app.post("/api/repos/{owner}/{repo}/milestones/{milestone_id}/close")
|
||||
async def api_milestone_close(
|
||||
request: Request,
|
||||
owner: str,
|
||||
repo: str,
|
||||
milestone_id: int,
|
||||
csrf_token: str = Form(""),
|
||||
):
|
||||
"""Close a milestone on the forge (issue #26)."""
|
||||
if not _csrf_valid(request, csrf_token):
|
||||
return _csrf_error()
|
||||
client, _cid, _ws = await _resolve_board_scope(request, owner, repo)
|
||||
try:
|
||||
await client.update_milestone(
|
||||
owner, repo, milestone_id, state="closed"
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return JSONResponse(
|
||||
{"ok": False, "error": f"La forge répond {exc.response.status_code}"},
|
||||
status_code=502,
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Briefing ───────────────────────────────────────────────
|
||||
|
||||
async def _briefing_data() -> dict:
|
||||
|
||||
Reference in New Issue
Block a user