All checks were successful
Deploy Development / deploy (push) Successful in 1m1s
Test Suite / pytest-backend (push) Successful in 4m43s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 16s
roadmap_items hat kein initiative_id — Single-Active-Update join auf roadmaps. test_ap20e an Uebungssets angepasst. Co-authored-by: Cursor <cursoragent@cursor.com>
194 lines
5.4 KiB
Python
194 lines
5.4 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"),
|
||
],
|
||
}
|
||
|
||
|
||
def get_active_maturity_stage(
|
||
*, tenant_id: str, initiative_id: str
|
||
) -> Optional[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"
|
||
]
|
||
if not stages:
|
||
return None
|
||
stages.sort(key=lambda s: (s.get("sort_order", 0), s.get("title", "")))
|
||
return stages[0]
|
||
|
||
|
||
def enforce_single_active_maturity_stage(
|
||
cur,
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
keep_active_id: str,
|
||
) -> None:
|
||
"""At most one maturity_stage may be active."""
|
||
cur.execute(
|
||
"""
|
||
UPDATE roadmap_items ri
|
||
SET status = 'planned', updated_at = NOW()
|
||
FROM roadmaps r
|
||
WHERE ri.roadmap_id = r.id
|
||
AND ri.tenant_id = %s
|
||
AND r.initiative_id = %s
|
||
AND ri.item_type = 'maturity_stage'
|
||
AND ri.status IN ('active', 'at_risk')
|
||
AND ri.id != %s
|
||
""",
|
||
(tenant_id, initiative_id, keep_active_id),
|
||
)
|
||
|
||
|
||
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 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 ""
|
||
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,
|
||
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]:
|
||
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],
|
||
}
|