Some checks failed
Deploy Development / deploy (push) Successful in 52s
Test Suite / pytest-backend (push) Failing after 5m3s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
join_gate, strand_project_id, Join-Auto-Schalten, multi-active Gates, Progression-Demo und PO-Lock-Spec. FE: Today strang-gruppiert, Join-Warteliste in Kontrolle. Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
6.7 KiB
Python
229 lines
6.7 KiB
Python
"""A1 — Stage-bound practice exercises (Activity Set)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime, timezone
|
||
from typing import Any, Optional
|
||
|
||
from services.recurring import (
|
||
create_recurring_element,
|
||
list_recurring_for_initiative,
|
||
update_recurring_element,
|
||
)
|
||
|
||
# Starter exercise sets per stage title (Spagat reference path).
|
||
STAGE_PRACTICE_SETS: dict[str, list[tuple[str, str]]] = {
|
||
"Stufe 1 — Basis": [
|
||
("Vorbeuge-Dehnung", "Täglich 3×30 Sek. — Hüfte und Beinrückseite"),
|
||
("Hüftöffner (Schmetterling)", "Täglich sitzend, leichter Druck"),
|
||
],
|
||
"Stufe 2 — Aufbau": [
|
||
("Seitlicher Spagat — links", "Täglich halten, optional Messwert (Grad)"),
|
||
("Seitlicher Spagat — rechts", "Täglich halten, optional Messwert (Grad)"),
|
||
("Aktive Vorbeuge", "Täglich 3× kurz halten"),
|
||
],
|
||
"Stufe 3 — Ziel": [
|
||
("Voller Spagat", "Täglich Zielposition — Messwert dokumentieren"),
|
||
],
|
||
"Dehnung — Einstieg": [
|
||
("Vorbeuge halten", "Täglich 3×30 Sek."),
|
||
],
|
||
"Hüftrotation — Einstieg": [
|
||
("Hüftkreisen im Stand", "Täglich 10 Wiederholungen je Richtung"),
|
||
],
|
||
"Dehnung — nach Join": [
|
||
("Enddehnung Mawashi-Vorbereitung", "Täglich 2×45 Sek."),
|
||
],
|
||
"Hüftrotation — nach Join": [
|
||
("Explosive Hüftrotation", "Täglich 3×8 Wiederholungen"),
|
||
],
|
||
}
|
||
|
||
|
||
def get_active_maturity_stages(
|
||
*, tenant_id: str, initiative_id: str
|
||
) -> list[dict[str, Any]]:
|
||
from services.roadmap import list_roadmap_items_for_initiative
|
||
|
||
stages = [
|
||
item
|
||
for item in list_roadmap_items_for_initiative(
|
||
tenant_id=tenant_id, initiative_id=initiative_id
|
||
)
|
||
if item.get("item_type") == "maturity_stage" and item.get("status") == "active"
|
||
]
|
||
stages.sort(key=lambda s: (s.get("sort_order", 0), s.get("title", "")))
|
||
return stages
|
||
|
||
|
||
def get_active_maturity_stage(
|
||
*, tenant_id: str, initiative_id: str
|
||
) -> Optional[dict[str, Any]]:
|
||
stages = get_active_maturity_stages(
|
||
tenant_id=tenant_id, initiative_id=initiative_id
|
||
)
|
||
return stages[0] if stages else None
|
||
|
||
|
||
def list_practices_for_stage(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
stage_id: str,
|
||
) -> list[dict[str, Any]]:
|
||
items = list_recurring_for_initiative(
|
||
tenant_id=tenant_id, initiative_id=initiative_id
|
||
)
|
||
return [
|
||
item
|
||
for item in items
|
||
if item.get("roadmap_item_id") == stage_id
|
||
]
|
||
|
||
|
||
def pause_all_practices(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
user_id: Optional[str] = None,
|
||
) -> list[str]:
|
||
paused: list[str] = []
|
||
for item in list_recurring_for_initiative(
|
||
tenant_id=tenant_id, initiative_id=initiative_id
|
||
):
|
||
if item.get("status") == "active":
|
||
update_recurring_element(
|
||
tenant_id=tenant_id,
|
||
recurring_id=item["id"],
|
||
status="paused",
|
||
user_id=user_id,
|
||
)
|
||
paused.append(item["title"])
|
||
return paused
|
||
|
||
|
||
def pause_practices_except_stage(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
keep_stage_id: str,
|
||
strand_project_id: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
) -> list[str]:
|
||
paused: list[str] = []
|
||
for item in list_recurring_for_initiative(
|
||
tenant_id=tenant_id, initiative_id=initiative_id
|
||
):
|
||
if item.get("status") != "active":
|
||
continue
|
||
if item.get("roadmap_item_id") == keep_stage_id:
|
||
continue
|
||
if strand_project_id is not None and item.get("project_id") != strand_project_id:
|
||
continue
|
||
update_recurring_element(
|
||
tenant_id=tenant_id,
|
||
recurring_id=item["id"],
|
||
status="paused",
|
||
user_id=user_id,
|
||
)
|
||
paused.append(item["title"])
|
||
return paused
|
||
|
||
|
||
def seed_practices_for_stage(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
stage: dict[str, Any],
|
||
user_id: Optional[str] = None,
|
||
) -> list[dict[str, Any]]:
|
||
"""Idempotent: create missing exercises for a stage; activate existing."""
|
||
stage_id = stage["id"]
|
||
stage_title = stage.get("title") or ""
|
||
strand_project_id = stage.get("strand_project_id")
|
||
defs = STAGE_PRACTICE_SETS.get(stage_title)
|
||
if not defs:
|
||
defs = [
|
||
(
|
||
f"Übung — {stage_title}",
|
||
f"Tägliche Routine für {stage_title}",
|
||
)
|
||
]
|
||
|
||
existing = list_practices_for_stage(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
stage_id=stage_id,
|
||
)
|
||
existing_by_title = {item["title"]: item for item in existing}
|
||
created: list[dict[str, Any]] = []
|
||
now = datetime.now(timezone.utc)
|
||
|
||
for title, description in defs:
|
||
if title in existing_by_title:
|
||
item = existing_by_title[title]
|
||
if item.get("status") != "active":
|
||
updated = update_recurring_element(
|
||
tenant_id=tenant_id,
|
||
recurring_id=item["id"],
|
||
status="active",
|
||
next_due_at=now,
|
||
user_id=user_id,
|
||
)
|
||
if updated:
|
||
created.append(updated)
|
||
else:
|
||
created.append(item)
|
||
continue
|
||
|
||
row = create_recurring_element(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
description=description,
|
||
status="active",
|
||
interval_days=1,
|
||
next_due_at=now,
|
||
roadmap_item_id=stage_id,
|
||
project_id=strand_project_id,
|
||
user_id=user_id,
|
||
)
|
||
created.append(row)
|
||
|
||
return created
|
||
|
||
|
||
def activate_stage_practice_set(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
stage: dict[str, Any],
|
||
user_id: Optional[str] = None,
|
||
) -> dict[str, Any]:
|
||
strand_project_id = stage.get("strand_project_id")
|
||
if strand_project_id:
|
||
paused = pause_practices_except_stage(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
keep_stage_id=stage["id"],
|
||
strand_project_id=strand_project_id,
|
||
user_id=user_id,
|
||
)
|
||
else:
|
||
paused = pause_all_practices(
|
||
tenant_id=tenant_id, initiative_id=initiative_id, user_id=user_id
|
||
)
|
||
exercises = seed_practices_for_stage(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
stage=stage,
|
||
user_id=user_id,
|
||
)
|
||
return {
|
||
"stage_id": stage["id"],
|
||
"stage_title": stage.get("title"),
|
||
"paused_practices": paused,
|
||
"active_practices": [e["title"] for e in exercises],
|
||
"practice_ids": [e["id"] for e in exercises],
|
||
}
|