shinkan-jinkendo/backend/routers/skills.py
Lars 505a8e5e38
Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 14s
feat: Skills & Methods catalog complete
Backend:
- Created routers/skills.py with full CRUD
  - Skills: list (with category filter), get, create, update, delete
  - Methods: list (with category filter), get, create, update, delete
  - Default: only show active items
  - Read access: all authenticated users
  - Write access: admin only
- Registered skills router in main.py

Frontend:
- Complete SkillsPage with 2 tabs (Fähigkeiten, Trainingsmethoden)
- Browse by category with cards layout
- Admin CRUD forms (importance rating for skills, duration/group size for methods)
- Mobile-responsive grid layout
- Updated api.js with all skill/method functions
- Added /skills route to App.jsx

Migration already exists: 003_catalogs.sql (skills, training_methods + seed data)

Next: Training Planning (core feature)
2026-04-22 16:50:31 +02:00

341 lines
11 KiB
Python

"""
Skills & Methods Catalog Endpoints for Shinkan Jinkendo
Handles CRUD operations for skills and training methods.
Read access for all authenticated users.
Write access for admins only.
"""
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, Query
from db import get_db, get_cursor, r2d
from auth import require_auth
router = APIRouter(prefix="/api", tags=["skills"])
# ── List Skills ───────────────────────────────────────────────────────
@router.get("/skills")
def list_skills(
category: Optional[str] = Query(default=None),
status: Optional[str] = Query(default=None),
session=Depends(require_auth)
):
"""
List all skills (public for authenticated users).
Filters:
- category: kihon, kumite, kata, selbstverteidigung, fitness
- status: active, inactive
"""
with get_db() as conn:
cur = get_cursor(conn)
query = "SELECT * FROM skills"
params = []
where = []
if category:
where.append("category = %s")
params.append(category)
if status:
where.append("status = %s")
params.append(status)
else:
# Default: only active skills
where.append("status = 'active'")
if where:
query += " WHERE " + " AND ".join(where)
query += " ORDER BY importance DESC, name"
cur.execute(query, params)
rows = cur.fetchall()
return [r2d(r) for r in rows]
# ── Get Skill ─────────────────────────────────────────────────────────
@router.get("/skills/{skill_id}")
def get_skill(skill_id: int, session=Depends(require_auth)):
"""Get skill by ID."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT * FROM skills WHERE id = %s", (skill_id,))
skill = cur.fetchone()
if not skill:
raise HTTPException(404, "Fähigkeit nicht gefunden")
return r2d(skill)
# ── Create Skill ──────────────────────────────────────────────────────
@router.post("/skills")
def create_skill(data: dict, session=Depends(require_auth)):
"""Create new skill (admin only)."""
role = session.get('role')
if role not in ['admin', 'superadmin']:
raise HTTPException(403, "Nur Admins dürfen Fähigkeiten erstellen")
name = data.get('name')
if not name:
raise HTTPException(400, "Name ist Pflichtfeld")
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("""
INSERT INTO skills (name, category, description, importance, keywords, status)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id
""", (
name,
data.get('category'),
data.get('description'),
data.get('importance'),
data.get('keywords'),
data.get('status', 'active')
))
skill_id = cur.fetchone()['id']
conn.commit()
return get_skill(skill_id, session)
# ── Update Skill ──────────────────────────────────────────────────────
@router.put("/skills/{skill_id}")
def update_skill(skill_id: int, data: dict, session=Depends(require_auth)):
"""Update skill (admin only)."""
role = session.get('role')
if role not in ['admin', 'superadmin']:
raise HTTPException(403, "Nur Admins dürfen Fähigkeiten bearbeiten")
with get_db() as conn:
cur = get_cursor(conn)
# Check existence
cur.execute("SELECT id FROM skills WHERE id = %s", (skill_id,))
if not cur.fetchone():
raise HTTPException(404, "Fähigkeit nicht gefunden")
# Update
cur.execute("""
UPDATE skills SET
name = %s,
category = %s,
description = %s,
importance = %s,
keywords = %s,
status = %s,
updated_at = NOW()
WHERE id = %s
""", (
data.get('name'),
data.get('category'),
data.get('description'),
data.get('importance'),
data.get('keywords'),
data.get('status'),
skill_id
))
conn.commit()
return get_skill(skill_id, session)
# ── Delete Skill ──────────────────────────────────────────────────────
@router.delete("/skills/{skill_id}")
def delete_skill(skill_id: int, session=Depends(require_auth)):
"""Delete skill (superadmin only)."""
role = session.get('role')
if role != 'superadmin':
raise HTTPException(403, "Nur Superadmins dürfen Fähigkeiten löschen")
with get_db() as conn:
cur = get_cursor(conn)
# Check existence
cur.execute("SELECT id FROM skills WHERE id = %s", (skill_id,))
if not cur.fetchone():
raise HTTPException(404, "Fähigkeit nicht gefunden")
# Delete
cur.execute("DELETE FROM skills WHERE id = %s", (skill_id,))
conn.commit()
return {"ok": True}
# ── List Training Methods ─────────────────────────────────────────────
@router.get("/methods")
def list_methods(
category: Optional[str] = Query(default=None),
status: Optional[str] = Query(default=None),
session=Depends(require_auth)
):
"""
List all training methods (public for authenticated users).
Filters:
- category: kondition, didaktik, koordination, kraft, etc.
- status: active, inactive
"""
with get_db() as conn:
cur = get_cursor(conn)
query = "SELECT * FROM training_methods"
params = []
where = []
if category:
where.append("category = %s")
params.append(category)
if status:
where.append("status = %s")
params.append(status)
else:
# Default: only active methods
where.append("status = 'active'")
if where:
query += " WHERE " + " AND ".join(where)
query += " ORDER BY name"
cur.execute(query, params)
rows = cur.fetchall()
return [r2d(r) for r in rows]
# ── Get Training Method ───────────────────────────────────────────────
@router.get("/methods/{method_id}")
def get_method(method_id: int, session=Depends(require_auth)):
"""Get training method by ID."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT * FROM training_methods WHERE id = %s", (method_id,))
method = cur.fetchone()
if not method:
raise HTTPException(404, "Trainingsmethode nicht gefunden")
return r2d(method)
# ── Create Training Method ────────────────────────────────────────────
@router.post("/methods")
def create_method(data: dict, session=Depends(require_auth)):
"""Create new training method (admin only)."""
role = session.get('role')
if role not in ['admin', 'superadmin']:
raise HTTPException(403, "Nur Admins dürfen Trainingsmethoden erstellen")
name = data.get('name')
if not name:
raise HTTPException(400, "Name ist Pflichtfeld")
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("""
INSERT INTO training_methods (
name, abbreviation, category, description,
typical_duration, typical_group_size,
related_skills, keywords, status
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
name,
data.get('abbreviation'),
data.get('category'),
data.get('description'),
data.get('typical_duration'),
data.get('typical_group_size'),
data.get('related_skills'),
data.get('keywords'),
data.get('status', 'active')
))
method_id = cur.fetchone()['id']
conn.commit()
return get_method(method_id, session)
# ── Update Training Method ────────────────────────────────────────────
@router.put("/methods/{method_id}")
def update_method(method_id: int, data: dict, session=Depends(require_auth)):
"""Update training method (admin only)."""
role = session.get('role')
if role not in ['admin', 'superadmin']:
raise HTTPException(403, "Nur Admins dürfen Trainingsmethoden bearbeiten")
with get_db() as conn:
cur = get_cursor(conn)
# Check existence
cur.execute("SELECT id FROM training_methods WHERE id = %s", (method_id,))
if not cur.fetchone():
raise HTTPException(404, "Trainingsmethode nicht gefunden")
# Update
cur.execute("""
UPDATE training_methods SET
name = %s,
abbreviation = %s,
category = %s,
description = %s,
typical_duration = %s,
typical_group_size = %s,
related_skills = %s,
keywords = %s,
status = %s,
updated_at = NOW()
WHERE id = %s
""", (
data.get('name'),
data.get('abbreviation'),
data.get('category'),
data.get('description'),
data.get('typical_duration'),
data.get('typical_group_size'),
data.get('related_skills'),
data.get('keywords'),
data.get('status'),
method_id
))
conn.commit()
return get_method(method_id, session)
# ── Delete Training Method ────────────────────────────────────────────
@router.delete("/methods/{method_id}")
def delete_method(method_id: int, session=Depends(require_auth)):
"""Delete training method (superadmin only)."""
role = session.get('role')
if role != 'superadmin':
raise HTTPException(403, "Nur Superadmins dürfen Trainingsmethoden löschen")
with get_db() as conn:
cur = get_cursor(conn)
# Check existence
cur.execute("SELECT id FROM training_methods WHERE id = %s", (method_id,))
if not cur.fetchone():
raise HTTPException(404, "Trainingsmethode nicht gefunden")
# Delete
cur.execute("DELETE FROM training_methods WHERE id = %s", (method_id,))
conn.commit()
return {"ok": True}