feat: Training Type Profiles Phase 2.1 - Backend Profile Management (#15)
Admin endpoints for profile configuration:
- Extended TrainingTypeCreate/Update models with profile field
- Added profile column to all SELECT queries
- Profile templates for Running, Meditation, Strength Training
- Template endpoints: list, get, apply
- Profile stats endpoint (configured/unconfigured count)
New file: profile_templates.py
- TEMPLATE_RUNNING: Endurance-focused with HR zones
- TEMPLATE_MEDITATION: Mental-focused (low HR ≤ instead of ≥)
- TEMPLATE_STRENGTH: Strength-focused
API Endpoints:
- GET /api/admin/training-types/profiles/templates
- GET /api/admin/training-types/profiles/templates/{key}
- POST /api/admin/training-types/{id}/profile/apply-template
- GET /api/admin/training-types/profiles/stats
Next: Frontend Admin-UI (ProfileEditor component)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ca7d9b2e3f
commit
d7145874cf
450
backend/profile_templates.py
Normal file
450
backend/profile_templates.py
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
"""
|
||||||
|
Training Type Profile Templates
|
||||||
|
Pre-configured profiles for common training types.
|
||||||
|
|
||||||
|
Issue: #15
|
||||||
|
Date: 2026-03-23
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# TEMPLATE: LAUFEN (Running) - Ausdauer-fokussiert
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
TEMPLATE_RUNNING = {
|
||||||
|
"version": "1.0",
|
||||||
|
"name": "Laufen (Standard)",
|
||||||
|
"description": "Ausdauerlauf mit Herzfrequenz-Zonen",
|
||||||
|
|
||||||
|
"rule_sets": {
|
||||||
|
"minimum_requirements": {
|
||||||
|
"enabled": True,
|
||||||
|
"pass_strategy": "weighted_score",
|
||||||
|
"pass_threshold": 0.6,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "duration_min",
|
||||||
|
"operator": "gte",
|
||||||
|
"value": 15,
|
||||||
|
"weight": 5,
|
||||||
|
"optional": False,
|
||||||
|
"reason": "Mindestens 15 Minuten für Trainingseffekt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr",
|
||||||
|
"operator": "gte",
|
||||||
|
"value": 100,
|
||||||
|
"weight": 3,
|
||||||
|
"optional": False,
|
||||||
|
"reason": "Puls muss für Ausdauerreiz erhöht sein"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "distance_km",
|
||||||
|
"operator": "gte",
|
||||||
|
"value": 1.0,
|
||||||
|
"weight": 2,
|
||||||
|
"optional": False,
|
||||||
|
"reason": "Mindestens 1 km Distanz"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"intensity_zones": {
|
||||||
|
"enabled": True,
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"id": "regeneration",
|
||||||
|
"name": "Regeneration",
|
||||||
|
"color": "#4CAF50",
|
||||||
|
"effect": "Aktive Erholung",
|
||||||
|
"target_duration_min": 30,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr_percent",
|
||||||
|
"operator": "between",
|
||||||
|
"value": [50, 60]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grundlagenausdauer",
|
||||||
|
"name": "Grundlagenausdauer",
|
||||||
|
"color": "#2196F3",
|
||||||
|
"effect": "Fettverbrennung, aerobe Basis",
|
||||||
|
"target_duration_min": 45,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr_percent",
|
||||||
|
"operator": "between",
|
||||||
|
"value": [60, 70]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "entwicklungsbereich",
|
||||||
|
"name": "Entwicklungsbereich",
|
||||||
|
"color": "#FF9800",
|
||||||
|
"effect": "VO2max-Training, Laktattoleranz",
|
||||||
|
"target_duration_min": 30,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr_percent",
|
||||||
|
"operator": "between",
|
||||||
|
"value": [70, 80]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "schwellentraining",
|
||||||
|
"name": "Schwellentraining",
|
||||||
|
"color": "#F44336",
|
||||||
|
"effect": "Anaerobe Schwelle, Wettkampftempo",
|
||||||
|
"target_duration_min": 20,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr_percent",
|
||||||
|
"operator": "between",
|
||||||
|
"value": [80, 90]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"training_effects": {
|
||||||
|
"enabled": True,
|
||||||
|
"default_effects": {
|
||||||
|
"primary_abilities": [
|
||||||
|
{
|
||||||
|
"category": "konditionell",
|
||||||
|
"ability": "ausdauer",
|
||||||
|
"intensity": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondary_abilities": [
|
||||||
|
{
|
||||||
|
"category": "konditionell",
|
||||||
|
"ability": "schnelligkeit",
|
||||||
|
"intensity": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "koordinativ",
|
||||||
|
"ability": "rhythmus",
|
||||||
|
"intensity": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "psychisch",
|
||||||
|
"ability": "willenskraft",
|
||||||
|
"intensity": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metabolic_focus": ["aerobic", "fat_oxidation"],
|
||||||
|
"muscle_groups": ["legs", "core", "cardiovascular"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"periodization": {
|
||||||
|
"enabled": True,
|
||||||
|
"frequency": {
|
||||||
|
"per_week_optimal": 3,
|
||||||
|
"per_week_max": 5
|
||||||
|
},
|
||||||
|
"recovery": {
|
||||||
|
"min_hours_between": 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"performance_indicators": {
|
||||||
|
"enabled": False
|
||||||
|
},
|
||||||
|
|
||||||
|
"safety": {
|
||||||
|
"enabled": True,
|
||||||
|
"warnings": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr_percent",
|
||||||
|
"operator": "gt",
|
||||||
|
"value": 95,
|
||||||
|
"severity": "high",
|
||||||
|
"message": "Herzfrequenz zu hoch - Überbelastungsrisiko"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "duration_min",
|
||||||
|
"operator": "gt",
|
||||||
|
"value": 180,
|
||||||
|
"severity": "medium",
|
||||||
|
"message": "Sehr lange Einheit - achte auf Regeneration"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# TEMPLATE: MEDITATION - Mental-fokussiert (≤ statt ≥ bei HR!)
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
TEMPLATE_MEDITATION = {
|
||||||
|
"version": "1.0",
|
||||||
|
"name": "Meditation (Standard)",
|
||||||
|
"description": "Mentales Training mit niedrigem Puls",
|
||||||
|
|
||||||
|
"rule_sets": {
|
||||||
|
"minimum_requirements": {
|
||||||
|
"enabled": True,
|
||||||
|
"pass_strategy": "weighted_score",
|
||||||
|
"pass_threshold": 0.6,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "duration_min",
|
||||||
|
"operator": "gte",
|
||||||
|
"value": 5,
|
||||||
|
"weight": 5,
|
||||||
|
"optional": False,
|
||||||
|
"reason": "Mindestens 5 Minuten für Entspannungseffekt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr",
|
||||||
|
"operator": "lte",
|
||||||
|
"value": 80,
|
||||||
|
"weight": 4,
|
||||||
|
"optional": False,
|
||||||
|
"reason": "Niedriger Puls zeigt Entspannung an"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"intensity_zones": {
|
||||||
|
"enabled": True,
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"id": "deep_relaxation",
|
||||||
|
"name": "Tiefenentspannung",
|
||||||
|
"color": "#4CAF50",
|
||||||
|
"effect": "Parasympathikus-Aktivierung",
|
||||||
|
"target_duration_min": 10,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr_percent",
|
||||||
|
"operator": "between",
|
||||||
|
"value": [35, 45]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "light_meditation",
|
||||||
|
"name": "Leichte Meditation",
|
||||||
|
"color": "#2196F3",
|
||||||
|
"effect": "Achtsamkeit, Fokus",
|
||||||
|
"target_duration_min": 15,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr_percent",
|
||||||
|
"operator": "between",
|
||||||
|
"value": [45, 55]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"training_effects": {
|
||||||
|
"enabled": True,
|
||||||
|
"default_effects": {
|
||||||
|
"primary_abilities": [
|
||||||
|
{
|
||||||
|
"category": "kognitiv",
|
||||||
|
"ability": "konzentration",
|
||||||
|
"intensity": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "psychisch",
|
||||||
|
"ability": "stressresistenz",
|
||||||
|
"intensity": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondary_abilities": [
|
||||||
|
{
|
||||||
|
"category": "kognitiv",
|
||||||
|
"ability": "wahrnehmung",
|
||||||
|
"intensity": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "psychisch",
|
||||||
|
"ability": "selbstvertrauen",
|
||||||
|
"intensity": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metabolic_focus": ["parasympathetic_activation"],
|
||||||
|
"muscle_groups": []
|
||||||
|
},
|
||||||
|
|
||||||
|
"periodization": {
|
||||||
|
"enabled": True,
|
||||||
|
"frequency": {
|
||||||
|
"per_week_optimal": 5,
|
||||||
|
"per_week_max": 7
|
||||||
|
},
|
||||||
|
"recovery": {
|
||||||
|
"min_hours_between": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"performance_indicators": {
|
||||||
|
"enabled": False
|
||||||
|
},
|
||||||
|
|
||||||
|
"safety": {
|
||||||
|
"enabled": True,
|
||||||
|
"warnings": [
|
||||||
|
{
|
||||||
|
"parameter": "avg_hr",
|
||||||
|
"operator": "gt",
|
||||||
|
"value": 100,
|
||||||
|
"severity": "medium",
|
||||||
|
"message": "Herzfrequenz zu hoch für Meditation - bist du wirklich entspannt?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# TEMPLATE: KRAFTTRAINING - Kraft-fokussiert
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
TEMPLATE_STRENGTH = {
|
||||||
|
"version": "1.0",
|
||||||
|
"name": "Krafttraining (Standard)",
|
||||||
|
"description": "Krafttraining mit moderater Herzfrequenz",
|
||||||
|
|
||||||
|
"rule_sets": {
|
||||||
|
"minimum_requirements": {
|
||||||
|
"enabled": True,
|
||||||
|
"pass_strategy": "weighted_score",
|
||||||
|
"pass_threshold": 0.5,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"parameter": "duration_min",
|
||||||
|
"operator": "gte",
|
||||||
|
"value": 20,
|
||||||
|
"weight": 5,
|
||||||
|
"optional": False,
|
||||||
|
"reason": "Mindestens 20 Minuten für Muskelreiz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "kcal_active",
|
||||||
|
"operator": "gte",
|
||||||
|
"value": 100,
|
||||||
|
"weight": 2,
|
||||||
|
"optional": True,
|
||||||
|
"reason": "Mindest-Kalorienverbrauch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"intensity_zones": {
|
||||||
|
"enabled": False
|
||||||
|
},
|
||||||
|
|
||||||
|
"training_effects": {
|
||||||
|
"enabled": True,
|
||||||
|
"default_effects": {
|
||||||
|
"primary_abilities": [
|
||||||
|
{
|
||||||
|
"category": "konditionell",
|
||||||
|
"ability": "kraft",
|
||||||
|
"intensity": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondary_abilities": [
|
||||||
|
{
|
||||||
|
"category": "koordinativ",
|
||||||
|
"ability": "differenzierung",
|
||||||
|
"intensity": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "psychisch",
|
||||||
|
"ability": "willenskraft",
|
||||||
|
"intensity": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metabolic_focus": ["anaerobic", "muscle_growth"],
|
||||||
|
"muscle_groups": ["full_body"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"periodization": {
|
||||||
|
"enabled": True,
|
||||||
|
"frequency": {
|
||||||
|
"per_week_optimal": 3,
|
||||||
|
"per_week_max": 5
|
||||||
|
},
|
||||||
|
"recovery": {
|
||||||
|
"min_hours_between": 48
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"performance_indicators": {
|
||||||
|
"enabled": False
|
||||||
|
},
|
||||||
|
|
||||||
|
"safety": {
|
||||||
|
"enabled": True,
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# TEMPLATE REGISTRY
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
TEMPLATES = {
|
||||||
|
"running": {
|
||||||
|
"name_de": "Laufen",
|
||||||
|
"name_en": "Running",
|
||||||
|
"icon": "🏃",
|
||||||
|
"categories": ["cardio", "running"],
|
||||||
|
"template": TEMPLATE_RUNNING
|
||||||
|
},
|
||||||
|
"meditation": {
|
||||||
|
"name_de": "Meditation",
|
||||||
|
"name_en": "Meditation",
|
||||||
|
"icon": "🧘",
|
||||||
|
"categories": ["geist", "meditation"],
|
||||||
|
"template": TEMPLATE_MEDITATION
|
||||||
|
},
|
||||||
|
"strength": {
|
||||||
|
"name_de": "Krafttraining",
|
||||||
|
"name_en": "Strength Training",
|
||||||
|
"icon": "💪",
|
||||||
|
"categories": ["kraft", "krafttraining"],
|
||||||
|
"template": TEMPLATE_STRENGTH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_template(template_key: str) -> dict:
|
||||||
|
"""Get profile template by key."""
|
||||||
|
template_info = TEMPLATES.get(template_key)
|
||||||
|
if not template_info:
|
||||||
|
return None
|
||||||
|
return template_info["template"]
|
||||||
|
|
||||||
|
|
||||||
|
def list_templates() -> list:
|
||||||
|
"""List all available templates."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"name_de": info["name_de"],
|
||||||
|
"name_en": info["name_en"],
|
||||||
|
"icon": info["icon"],
|
||||||
|
"categories": info["categories"]
|
||||||
|
}
|
||||||
|
for key, info in TEMPLATES.items()
|
||||||
|
]
|
||||||
|
|
@ -11,6 +11,7 @@ from psycopg2.extras import Json
|
||||||
|
|
||||||
from db import get_db, get_cursor, r2d
|
from db import get_db, get_cursor, r2d
|
||||||
from auth import require_auth, require_admin
|
from auth import require_auth, require_admin
|
||||||
|
from profile_templates import list_templates, get_template
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/admin/training-types", tags=["admin", "training-types"])
|
router = APIRouter(prefix="/api/admin/training-types", tags=["admin", "training-types"])
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -26,6 +27,7 @@ class TrainingTypeCreate(BaseModel):
|
||||||
description_en: Optional[str] = None
|
description_en: Optional[str] = None
|
||||||
sort_order: int = 0
|
sort_order: int = 0
|
||||||
abilities: Optional[dict] = None
|
abilities: Optional[dict] = None
|
||||||
|
profile: Optional[dict] = None # Training Type Profile (Phase 2 #15)
|
||||||
|
|
||||||
|
|
||||||
class TrainingTypeUpdate(BaseModel):
|
class TrainingTypeUpdate(BaseModel):
|
||||||
|
|
@ -38,6 +40,7 @@ class TrainingTypeUpdate(BaseModel):
|
||||||
description_en: Optional[str] = None
|
description_en: Optional[str] = None
|
||||||
sort_order: Optional[int] = None
|
sort_order: Optional[int] = None
|
||||||
abilities: Optional[dict] = None
|
abilities: Optional[dict] = None
|
||||||
|
profile: Optional[dict] = None # Training Type Profile (Phase 2 #15)
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
|
|
@ -51,7 +54,7 @@ def list_training_types_admin(session: dict = Depends(require_admin)):
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id, category, subcategory, name_de, name_en, icon,
|
SELECT id, category, subcategory, name_de, name_en, icon,
|
||||||
description_de, description_en, sort_order, abilities,
|
description_de, description_en, sort_order, abilities,
|
||||||
created_at
|
profile, created_at
|
||||||
FROM training_types
|
FROM training_types
|
||||||
ORDER BY sort_order, category, subcategory
|
ORDER BY sort_order, category, subcategory
|
||||||
""")
|
""")
|
||||||
|
|
@ -68,7 +71,7 @@ def get_training_type(type_id: int, session: dict = Depends(require_admin)):
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id, category, subcategory, name_de, name_en, icon,
|
SELECT id, category, subcategory, name_de, name_en, icon,
|
||||||
description_de, description_en, sort_order, abilities,
|
description_de, description_en, sort_order, abilities,
|
||||||
created_at
|
profile, created_at
|
||||||
FROM training_types
|
FROM training_types
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (type_id,))
|
""", (type_id,))
|
||||||
|
|
@ -86,14 +89,15 @@ def create_training_type(data: TrainingTypeCreate, session: dict = Depends(requi
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
# Convert abilities dict to JSONB
|
# Convert abilities and profile dict to JSONB
|
||||||
abilities_json = data.abilities if data.abilities else {}
|
abilities_json = data.abilities if data.abilities else {}
|
||||||
|
profile_json = data.profile if data.profile else None
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO training_types
|
INSERT INTO training_types
|
||||||
(category, subcategory, name_de, name_en, icon,
|
(category, subcategory, name_de, name_en, icon,
|
||||||
description_de, description_en, sort_order, abilities)
|
description_de, description_en, sort_order, abilities, profile)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (
|
""", (
|
||||||
data.category,
|
data.category,
|
||||||
|
|
@ -104,7 +108,8 @@ def create_training_type(data: TrainingTypeCreate, session: dict = Depends(requi
|
||||||
data.description_de,
|
data.description_de,
|
||||||
data.description_en,
|
data.description_en,
|
||||||
data.sort_order,
|
data.sort_order,
|
||||||
Json(abilities_json)
|
Json(abilities_json),
|
||||||
|
Json(profile_json) if profile_json else None
|
||||||
))
|
))
|
||||||
|
|
||||||
new_id = cur.fetchone()['id']
|
new_id = cur.fetchone()['id']
|
||||||
|
|
@ -155,6 +160,9 @@ def update_training_type(
|
||||||
if data.abilities is not None:
|
if data.abilities is not None:
|
||||||
updates.append("abilities = %s")
|
updates.append("abilities = %s")
|
||||||
values.append(Json(data.abilities))
|
values.append(Json(data.abilities))
|
||||||
|
if data.profile is not None:
|
||||||
|
updates.append("profile = %s")
|
||||||
|
values.append(Json(data.profile))
|
||||||
|
|
||||||
if not updates:
|
if not updates:
|
||||||
raise HTTPException(400, "No fields to update")
|
raise HTTPException(400, "No fields to update")
|
||||||
|
|
@ -280,3 +288,122 @@ def get_abilities_taxonomy(session: dict = Depends(require_auth)):
|
||||||
}
|
}
|
||||||
|
|
||||||
return taxonomy
|
return taxonomy
|
||||||
|
|
||||||
|
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# TRAINING TYPE PROFILES - Phase 2 (#15)
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
@router.get("/profiles/templates")
|
||||||
|
def list_profile_templates(session: dict = Depends(require_admin)):
|
||||||
|
"""
|
||||||
|
List all available profile templates.
|
||||||
|
|
||||||
|
Returns templates for common training types (Running, Meditation, Strength, etc.)
|
||||||
|
"""
|
||||||
|
return list_templates()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/profiles/templates/{template_key}")
|
||||||
|
def get_profile_template(template_key: str, session: dict = Depends(require_admin)):
|
||||||
|
"""
|
||||||
|
Get a specific profile template by key.
|
||||||
|
|
||||||
|
Keys: running, meditation, strength
|
||||||
|
"""
|
||||||
|
template = get_template(template_key)
|
||||||
|
if not template:
|
||||||
|
raise HTTPException(404, f"Template '{template_key}' not found")
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{type_id}/profile/apply-template")
|
||||||
|
def apply_profile_template(
|
||||||
|
type_id: int,
|
||||||
|
data: dict,
|
||||||
|
session: dict = Depends(require_admin)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Apply a profile template to a training type.
|
||||||
|
|
||||||
|
Body: { "template_key": "running" }
|
||||||
|
"""
|
||||||
|
template_key = data.get("template_key")
|
||||||
|
if not template_key:
|
||||||
|
raise HTTPException(400, "template_key required")
|
||||||
|
|
||||||
|
template = get_template(template_key)
|
||||||
|
if not template:
|
||||||
|
raise HTTPException(404, f"Template '{template_key}' not found")
|
||||||
|
|
||||||
|
# Apply template to training type
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
# Check if training type exists
|
||||||
|
cur.execute("SELECT id, name_de FROM training_types WHERE id = %s", (type_id,))
|
||||||
|
training_type = cur.fetchone()
|
||||||
|
if not training_type:
|
||||||
|
raise HTTPException(404, "Training type not found")
|
||||||
|
|
||||||
|
# Update profile
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE training_types
|
||||||
|
SET profile = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (Json(template), type_id))
|
||||||
|
|
||||||
|
logger.info(f"[ADMIN] Applied template '{template_key}' to training type {type_id} ({training_type['name_de']})")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": f"Template '{template_key}' applied successfully",
|
||||||
|
"training_type_id": type_id,
|
||||||
|
"training_type_name": training_type['name_de'],
|
||||||
|
"template_key": template_key
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/profiles/stats")
|
||||||
|
def get_profile_stats(session: dict = Depends(require_admin)):
|
||||||
|
"""
|
||||||
|
Get statistics about configured profiles.
|
||||||
|
|
||||||
|
Returns count of training types with/without profiles.
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(profile) as configured,
|
||||||
|
COUNT(*) - COUNT(profile) as unconfigured
|
||||||
|
FROM training_types
|
||||||
|
""")
|
||||||
|
stats = cur.fetchone()
|
||||||
|
|
||||||
|
# Get list of types with profiles
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, name_de, category, subcategory
|
||||||
|
FROM training_types
|
||||||
|
WHERE profile IS NOT NULL
|
||||||
|
ORDER BY name_de
|
||||||
|
""")
|
||||||
|
configured_types = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Get list of types without profiles
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, name_de, category, subcategory
|
||||||
|
FROM training_types
|
||||||
|
WHERE profile IS NULL
|
||||||
|
ORDER BY name_de
|
||||||
|
""")
|
||||||
|
unconfigured_types = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": stats['total'],
|
||||||
|
"configured": stats['configured'],
|
||||||
|
"unconfigured": stats['unconfigured'],
|
||||||
|
"configured_types": configured_types,
|
||||||
|
"unconfigured_types": unconfigured_types
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user