feat: enhance tenant context integration and update access layer endpoints
- Implemented `library_content_visibility_sql` for managing visibility of exercises, training planning, and framework programs based on tenant context. - Updated access layer documentation to reflect changes in endpoint visibility and governance requirements. - Bumped application version to 0.8.23 in both backend and frontend files. - Enhanced changelog to document the new version and changes made in this release.
This commit is contained in:
parent
4b6fd49940
commit
c919e02441
|
|
@ -10,12 +10,19 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C.
|
|||
| clubs | CRUD Organisation | ja | — | `can_manage_club_org` / member | schrittweise auf TenantContext |
|
||||
| club_memberships | `/clubs/{id}/members*` | ja | geplant | ja | |
|
||||
| club_join_requests | `/clubs/{id}/join-requests*` | ja | geplant | ja | |
|
||||
| exercises | `GET /api/exercises`, Detail | ja | geplant | `visibility` + Mitgliedschaft | |
|
||||
| training_planning | diverse | ja | geplant | `exercise_visible` / Gruppe | |
|
||||
| training_framework_programs | diverse | ja | geplant | analog Übungen | |
|
||||
| exercises | `GET /api/exercises`, `POST /api/exercises` | ja | `get_tenant_context` | ja | Liste club nach aktivem Verein; POST Governance + Default club_id |
|
||||
| exercises | Detail/PATCH (Übriges) | teils | `require_auth` | Owner/Admin | später Tenant optional |
|
||||
| training_planning | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Vorlagen-Liste wie Übungen; POST Vorlage Default club_id |
|
||||
| training_framework_programs | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Liste + POST Default club_id |
|
||||
| admin_users | `GET /api/admin/users` | Plattform | optional | Admin-Rolle | |
|
||||
| Sonstige | skills, methods, catalogs | zu klären | — | oft global | Zeilen ergänzen |
|
||||
|
||||
**Legende:** „geplant“ = beim nächsten Umbau dieser Router `get_tenant_context` verwenden bzw. zentrale Governance-Helfer.
|
||||
|
||||
Letzte Änderung: 2026-05-05 (Initial)
|
||||
Letzte Änderung: 2026-05-05 — Stufe B/C partiell (Bibliothekslisten + Planung); `GET /training-units` ohne automatischen club_id-Filter (Kompatibilität).
|
||||
|
||||
---
|
||||
|
||||
### Hinweis `GET /training-units`
|
||||
|
||||
Kein impliziter Filter nach `effective_club_id` (Multi-Verein-Kalender); bei Bedarf `club_id` Query setzen.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ from pydantic import BaseModel, Field, model_validator
|
|||
|
||||
from db import get_db, get_cursor, r2d
|
||||
from auth import require_auth
|
||||
from club_tenancy import exercise_visible_to_profile, is_platform_admin
|
||||
from club_tenancy import assert_valid_governance_visibility, exercise_visible_to_profile, is_platform_admin
|
||||
from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -577,14 +578,14 @@ def list_exercises(
|
|||
default=False,
|
||||
description="Wenn true: Feld variants[] pro Übung (id, variant_name, sequence_order) für Planung/UI",
|
||||
),
|
||||
session: dict = Depends(require_auth),
|
||||
tenant: TenantContext = Depends(get_tenant_context),
|
||||
):
|
||||
"""
|
||||
Liste aller Übungen mit Filtern.
|
||||
Lightweight Response (ohne M:N Details, nur IDs und Namen).
|
||||
Optional include_variants für Variantenauswahl in der Trainingsplanung.
|
||||
"""
|
||||
profile_id = session["profile_id"]
|
||||
profile_id = tenant.profile_id
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -593,24 +594,16 @@ def list_exercises(
|
|||
where = ["1=1"]
|
||||
params = []
|
||||
|
||||
# Mandanten-Sichtbarkeit: official / eigene private / club nur eigene Vereine (Plattform-Admin: alles)
|
||||
role = session.get("role")
|
||||
role = tenant.global_role
|
||||
if not is_platform_admin(role):
|
||||
where.append(
|
||||
"""(
|
||||
e.visibility = 'official'
|
||||
OR (e.visibility = 'private' AND e.created_by = %s)
|
||||
OR (
|
||||
e.visibility = 'club'
|
||||
AND e.club_id IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM club_members cm
|
||||
WHERE cm.profile_id = %s AND cm.club_id = e.club_id AND cm.status = 'active'
|
||||
)
|
||||
)
|
||||
)"""
|
||||
vis_sql, vis_params = library_content_visibility_sql(
|
||||
alias="e",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
params.extend([profile_id, profile_id])
|
||||
where.append(vis_sql)
|
||||
params.extend(vis_params)
|
||||
|
||||
vis_list = _merge_str_any(visibility_any, visibility)
|
||||
if vis_list:
|
||||
|
|
@ -789,12 +782,12 @@ def get_exercise(
|
|||
@router.post("/exercises", status_code=201)
|
||||
def create_exercise(
|
||||
body: ExerciseCreate,
|
||||
session: dict = Depends(require_auth),
|
||||
tenant: TenantContext = Depends(get_tenant_context),
|
||||
):
|
||||
"""
|
||||
Erstellt eine neue Übung mit allen M:N Relations.
|
||||
"""
|
||||
profile_id = session["profile_id"]
|
||||
profile_id = tenant.profile_id
|
||||
|
||||
# Validierung
|
||||
if body.status not in ("draft", "in_review", "approved", "archived"):
|
||||
|
|
@ -802,8 +795,15 @@ def create_exercise(
|
|||
if body.visibility not in ("private", "club", "official"):
|
||||
raise HTTPException(status_code=400, detail="Ungültige Visibility")
|
||||
|
||||
club_id = body.club_id
|
||||
if body.visibility == "club" and club_id is None:
|
||||
club_id = tenant.effective_club_id
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
assert_valid_governance_visibility(
|
||||
cur, profile_id, tenant.global_role, body.visibility, club_id
|
||||
)
|
||||
|
||||
# Equipment als JSONB
|
||||
equipment_json = json.dumps(body.equipment) if body.equipment else None
|
||||
|
|
@ -822,7 +822,7 @@ def create_exercise(
|
|||
body.duration_min, body.duration_max,
|
||||
body.group_size_min, body.group_size_max,
|
||||
equipment_json,
|
||||
body.visibility, body.status, profile_id, body.club_id,
|
||||
body.visibility, body.status, profile_id, club_id,
|
||||
)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from typing import Any, Dict, List, Optional, Sequence
|
|||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from auth import require_auth
|
||||
from club_tenancy import (
|
||||
assert_valid_governance_visibility,
|
||||
exercise_visible_to_profile,
|
||||
|
|
@ -26,6 +25,8 @@ from routers.training_planning import (
|
|||
_validate_variant_for_exercise,
|
||||
)
|
||||
|
||||
from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["training_framework_programs"])
|
||||
_VALID_VISIBILITY = frozenset({"private", "club", "official"})
|
||||
|
||||
|
|
@ -67,6 +68,14 @@ def _framework_access(cur, framework_id: int, profile_id: int, role: str) -> Dic
|
|||
return row
|
||||
|
||||
|
||||
def _response_framework_detail(framework_id: int, profile_id: int, role: str) -> Dict[str, Any]:
|
||||
"""Einzelabruf nach Schreiboperation (ohne FastAPI-Depends-Schleife)."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row = _framework_access(cur, framework_id, profile_id, role)
|
||||
return _hydrate_framework(cur, row)
|
||||
|
||||
|
||||
def _training_type_ids(cur, framework_id: int) -> List[int]:
|
||||
cur.execute(
|
||||
"""
|
||||
|
|
@ -308,9 +317,9 @@ def _insert_slots_and_blueprints(
|
|||
|
||||
|
||||
@router.get("/training-framework-programs")
|
||||
def list_training_framework_programs(session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def list_training_framework_programs(tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
base_sel = """
|
||||
|
|
@ -344,40 +353,36 @@ def list_training_framework_programs(session=Depends(require_auth)):
|
|||
if is_platform_admin(role):
|
||||
cur.execute(base_sel + " ORDER BY fp.updated_at DESC NULLS LAST, fp.title")
|
||||
else:
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="fp",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
base_sel
|
||||
+ """ WHERE (
|
||||
fp.visibility = 'official'
|
||||
OR (fp.visibility = 'private' AND fp.created_by = %s)
|
||||
OR (
|
||||
fp.visibility = 'club'
|
||||
AND fp.club_id IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM club_members cm
|
||||
WHERE cm.profile_id = %s AND cm.club_id = fp.club_id AND cm.status = 'active'
|
||||
)
|
||||
)
|
||||
)
|
||||
+ f""" WHERE ({vis_clause})
|
||||
ORDER BY fp.updated_at DESC NULLS LAST, fp.title""",
|
||||
(profile_id, profile_id),
|
||||
vis_params,
|
||||
)
|
||||
return [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
|
||||
@router.get("/training-framework-programs/{framework_id}")
|
||||
def get_training_framework_program(framework_id: int, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row = _framework_access(cur, framework_id, profile_id, role)
|
||||
return _hydrate_framework(cur, row)
|
||||
def get_training_framework_program(
|
||||
framework_id: int, tenant: TenantContext = Depends(get_tenant_context)
|
||||
):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
return _response_framework_detail(framework_id, profile_id, role)
|
||||
|
||||
|
||||
@router.post("/training-framework-programs")
|
||||
def create_training_framework_program(data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def create_training_framework_program(
|
||||
data: dict, tenant: TenantContext = Depends(get_tenant_context)
|
||||
):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
if not _has_planning_role(role):
|
||||
raise HTTPException(status_code=403, detail="Nur Planungsberechtigte dürfen Rahmenprogramme anlegen")
|
||||
|
||||
|
|
@ -390,6 +395,9 @@ def create_training_framework_program(data: dict, session=Depends(require_auth))
|
|||
club_id = data.get("club_id")
|
||||
if club_id in ("", []):
|
||||
club_id = None
|
||||
if vis == "club" and club_id is None:
|
||||
club_id = tenant.effective_club_id
|
||||
|
||||
goals_in = data.get("goals")
|
||||
slots_in = data.get("slots")
|
||||
if not isinstance(goals_in, list) or not goals_in:
|
||||
|
|
@ -432,13 +440,15 @@ def create_training_framework_program(data: dict, session=Depends(require_auth))
|
|||
_replace_target_groups(cur, fid, tg_ids)
|
||||
conn.commit()
|
||||
|
||||
return get_training_framework_program(fid, session)
|
||||
return _response_framework_detail(fid, profile_id, role)
|
||||
|
||||
|
||||
@router.put("/training-framework-programs/{framework_id}")
|
||||
def update_training_framework_program(framework_id: int, data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def update_training_framework_program(
|
||||
framework_id: int, data: dict, tenant: TenantContext = Depends(get_tenant_context)
|
||||
):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
if not _has_planning_role(role):
|
||||
raise HTTPException(status_code=403, detail="Keine Berechtigung")
|
||||
|
||||
|
|
@ -546,13 +556,15 @@ def update_training_framework_program(framework_id: int, data: dict, session=Dep
|
|||
|
||||
conn.commit()
|
||||
|
||||
return get_training_framework_program(framework_id, session)
|
||||
return _response_framework_detail(framework_id, profile_id, role)
|
||||
|
||||
|
||||
@router.delete("/training-framework-programs/{framework_id}")
|
||||
def delete_training_framework_program(framework_id: int, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def delete_training_framework_program(
|
||||
framework_id: int, tenant: TenantContext = Depends(get_tenant_context)
|
||||
):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row_fw = _fetch_framework_row(cur, framework_id)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional
|
|||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from db import get_db, get_cursor, r2d
|
||||
from auth import require_auth
|
||||
from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql
|
||||
from club_tenancy import (
|
||||
assert_valid_governance_visibility,
|
||||
exercise_visible_to_profile,
|
||||
|
|
@ -560,9 +560,9 @@ def _template_access(cur, tid: int, profile_id: int, role: str) -> Dict[str, Any
|
|||
|
||||
|
||||
@router.get("/training-plan-templates")
|
||||
def list_training_plan_templates(session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def list_training_plan_templates(tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
if is_platform_admin(role):
|
||||
|
|
@ -576,35 +576,30 @@ def list_training_plan_templates(session=Depends(require_auth)):
|
|||
"""
|
||||
)
|
||||
else:
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="t",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
f"""
|
||||
SELECT t.*,
|
||||
(SELECT COUNT(*) FROM training_plan_template_sections s WHERE s.template_id = t.id)
|
||||
AS sections_count
|
||||
FROM training_plan_templates t
|
||||
WHERE (
|
||||
t.visibility = 'official'
|
||||
OR (t.visibility = 'private' AND t.created_by = %s)
|
||||
OR (
|
||||
t.visibility = 'club'
|
||||
AND t.club_id IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM club_members cm
|
||||
WHERE cm.profile_id = %s AND cm.club_id = t.club_id AND cm.status = 'active'
|
||||
)
|
||||
)
|
||||
)
|
||||
WHERE ({vis_clause})
|
||||
ORDER BY t.updated_at DESC NULLS LAST, t.name
|
||||
""",
|
||||
(profile_id, profile_id),
|
||||
vis_params,
|
||||
)
|
||||
return [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
|
||||
@router.get("/training-plan-templates/{template_id}")
|
||||
def get_training_plan_template(template_id: int, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def get_training_plan_template(template_id: int, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row = _template_access(cur, template_id, profile_id, role)
|
||||
|
|
@ -622,9 +617,9 @@ def get_training_plan_template(template_id: int, session=Depends(require_auth)):
|
|||
|
||||
|
||||
@router.post("/training-plan-templates")
|
||||
def create_training_plan_template(data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def create_training_plan_template(data: dict, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
if not _has_planning_role(role):
|
||||
raise HTTPException(status_code=403, detail="Nur Trainer dürfen Vorlagen anlegen")
|
||||
name = (data.get("name") or "").strip()
|
||||
|
|
@ -635,6 +630,8 @@ def create_training_plan_template(data: dict, session=Depends(require_auth)):
|
|||
club_id = data.get("club_id")
|
||||
if club_id in ("", []):
|
||||
club_id = None
|
||||
if visibility == "club" and club_id is None:
|
||||
club_id = tenant.effective_club_id
|
||||
sections_in = data.get("sections") or []
|
||||
|
||||
with get_db() as conn:
|
||||
|
|
@ -662,13 +659,13 @@ def create_training_plan_template(data: dict, session=Depends(require_auth)):
|
|||
(tid, order_ix, title, sec.get("guidance_text")),
|
||||
)
|
||||
conn.commit()
|
||||
return get_training_plan_template(tid, session)
|
||||
return get_training_plan_template(tid, tenant)
|
||||
|
||||
|
||||
@router.put("/training-plan-templates/{template_id}")
|
||||
def update_training_plan_template(template_id: int, data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def update_training_plan_template(template_id: int, data: dict, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row_prev = _fetch_training_plan_template_row(cur, template_id)
|
||||
|
|
@ -731,13 +728,13 @@ def update_training_plan_template(template_id: int, data: dict, session=Depends(
|
|||
(template_id, order_ix, title, sec.get("guidance_text")),
|
||||
)
|
||||
conn.commit()
|
||||
return get_training_plan_template(template_id, session)
|
||||
return get_training_plan_template(template_id, tenant)
|
||||
|
||||
|
||||
@router.delete("/training-plan-templates/{template_id}")
|
||||
def delete_training_plan_template(template_id: int, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def delete_training_plan_template(template_id: int, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
row_del = _fetch_training_plan_template_row(cur, template_id)
|
||||
|
|
@ -760,10 +757,10 @@ def list_training_units(
|
|||
assigned_to_me: bool = Query(default=False),
|
||||
sort: str = Query(default="desc"),
|
||||
limit: Optional[int] = Query(default=None),
|
||||
session=Depends(require_auth),
|
||||
tenant: TenantContext = Depends(get_tenant_context),
|
||||
):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
gid = _optional_positive_int(group_id, "group_id") if group_id else None
|
||||
cid = _optional_positive_int(club_id, "club_id") if club_id else None
|
||||
|
|
@ -873,9 +870,9 @@ def list_training_units(
|
|||
|
||||
|
||||
@router.get("/training-units/{unit_id}")
|
||||
def get_training_unit(unit_id: int, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def get_training_unit(unit_id: int, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -937,9 +934,9 @@ def get_training_unit(unit_id: int, session=Depends(require_auth)):
|
|||
|
||||
|
||||
@router.post("/training-units")
|
||||
def create_training_unit(data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def create_training_unit(data: dict, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
group_id = data.get("group_id")
|
||||
planned_date = data.get("planned_date")
|
||||
|
|
@ -996,13 +993,13 @@ def create_training_unit(data: dict, session=Depends(require_auth)):
|
|||
|
||||
conn.commit()
|
||||
|
||||
return get_training_unit(unit_id, session)
|
||||
return get_training_unit(unit_id, tenant)
|
||||
|
||||
|
||||
@router.put("/training-units/{unit_id}")
|
||||
def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def update_training_unit(unit_id: int, data: dict, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -1143,13 +1140,13 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
|
|||
|
||||
conn.commit()
|
||||
|
||||
return get_training_unit(unit_id, session)
|
||||
return get_training_unit(unit_id, tenant)
|
||||
|
||||
|
||||
@router.delete("/training-units/{unit_id}")
|
||||
def delete_training_unit(unit_id: int, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
def delete_training_unit(unit_id: int, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -1179,10 +1176,10 @@ def delete_training_unit(unit_id: int, session=Depends(require_auth)):
|
|||
|
||||
|
||||
@router.post("/training-units/from-framework-slot")
|
||||
def create_training_unit_from_framework_slot(data: dict, session=Depends(require_auth)):
|
||||
def create_training_unit_from_framework_slot(data: dict, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
"""Geplante Einheit aus Rahmen-Slot-Blueprint kopieren (Lineage über origin_framework_slot_id)."""
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
if not _has_planning_role(role):
|
||||
raise HTTPException(status_code=403, detail="Nur Planungsberechtigte dürfen Trainingseinheiten erstellen")
|
||||
|
|
@ -1243,12 +1240,12 @@ def create_training_unit_from_framework_slot(data: dict, session=Depends(require
|
|||
|
||||
conn.commit()
|
||||
|
||||
return get_training_unit(new_id, session)
|
||||
return get_training_unit(new_id, tenant)
|
||||
|
||||
|
||||
@router.post("/training-units/quick-create")
|
||||
def quick_create_training_unit(data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
def quick_create_training_unit(data: dict, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
|
||||
group_id = data.get("group_id")
|
||||
planned_date = data.get("planned_date")
|
||||
|
|
@ -1274,7 +1271,7 @@ def quick_create_training_unit(data: dict, session=Depends(require_auth)):
|
|||
if not group:
|
||||
raise HTTPException(status_code=404, detail="Trainingsgruppe nicht gefunden")
|
||||
|
||||
role = session.get("role")
|
||||
role = tenant.global_role
|
||||
co_trainers = group["co_trainer_ids"] or []
|
||||
|
||||
if not _has_planning_role(role):
|
||||
|
|
@ -1316,5 +1313,5 @@ def quick_create_training_unit(data: dict, session=Depends(require_auth)):
|
|||
|
||||
conn.commit()
|
||||
|
||||
return get_training_unit(unit_id, session)
|
||||
return get_training_unit(unit_id, tenant)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,45 @@ def parse_active_club_header(raw: Optional[str]) -> Optional[int]:
|
|||
return v
|
||||
|
||||
|
||||
def library_content_visibility_sql(
|
||||
*,
|
||||
alias: str,
|
||||
profile_id: int,
|
||||
role: str,
|
||||
effective_club_id: Optional[int],
|
||||
) -> tuple[str, List[Any]]:
|
||||
"""
|
||||
WHERE-Baustein für Bibliothekslisten (Übungen, Vorlagen, Rahmenprogramme):
|
||||
official, eigene private, club nur im aktiven Vereinskontext (effective_club_id).
|
||||
Plattform-Admin: keine Einschränkung (TRUE).
|
||||
Ohne effective_club_id: kein club-Zweig (nur official + private).
|
||||
"""
|
||||
if is_platform_admin(role):
|
||||
return "TRUE", []
|
||||
|
||||
parts: List[str] = [
|
||||
f"{alias}.visibility = 'official'",
|
||||
f"({alias}.visibility = 'private' AND {alias}.created_by = %s)",
|
||||
]
|
||||
params: List[Any] = [profile_id]
|
||||
|
||||
if effective_club_id is not None:
|
||||
parts.append(
|
||||
f"""(
|
||||
{alias}.visibility = 'club'
|
||||
AND {alias}.club_id IS NOT NULL
|
||||
AND {alias}.club_id = %s
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM club_members cm
|
||||
WHERE cm.profile_id = %s AND cm.club_id = {alias}.club_id AND cm.status = 'active'
|
||||
)
|
||||
)"""
|
||||
)
|
||||
params.extend([effective_club_id, profile_id])
|
||||
|
||||
return "(" + " OR ".join(parts) + ")", params
|
||||
|
||||
|
||||
@dataclass
|
||||
class TenantContext:
|
||||
profile_id: int
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.22"
|
||||
APP_VERSION = "0.8.23"
|
||||
BUILD_DATE = "2026-05-05"
|
||||
DB_SCHEMA_VERSION = "20260505041"
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
"auth": "1.2.0", # Erster/bootstrap Nutzer und ADMIN_BOOTSTRAP_EMAILS → superadmin (nicht mehr admin)
|
||||
"profiles": "1.4.0", # GET /profiles/me: effective_club_id (TenantContext-Auflösung); TenantContext-Modul
|
||||
"tenant_context": "1.0.0", # resolve/get_depends; library_content_visibility_sql
|
||||
"clubs": "0.4.0", # public-directory, members/directory; Vereins-GUI verwendet Endpoints
|
||||
"club_memberships": "1.0.0",
|
||||
"club_join_requests": "1.0.0",
|
||||
|
|
@ -14,10 +15,10 @@ MODULE_VERSIONS = {
|
|||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.4.0", # Multi-Tenancy: club-Sichtbarkeit nur im eigenen Verein
|
||||
"exercises": "2.5.0", # list/create: TenantContext + Governance; club-Liste nach aktivem Verein
|
||||
"training_units": "0.1.0",
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.7.0", # Vorlagen + Rahmenprogramme: Listen/GET wie Übungen (visibility/club); Governance-Validierung
|
||||
"planning": "0.8.0", # TenantContext auf allen Planungs-Endpunkten; Vorlagen-Liste wie Übungen nach aktivem Verein
|
||||
"import_wiki": "1.0.0",
|
||||
"admin": "1.0.0",
|
||||
"membership": "1.0.0",
|
||||
|
|
@ -26,6 +27,14 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.23",
|
||||
"date": "2026-05-05",
|
||||
"changes": [
|
||||
"ACCESS_LAYER: library_content_visibility_sql + TenantContext an Übungen-Liste, Rahmenprogramme, Trainingsplanung",
|
||||
"POST Übung/Vorlage/Rahmenprogramm: visibility club ohne club_id → effective_club_id; POST Übung mit assert_valid_governance_visibility",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.22",
|
||||
"date": "2026-05-05",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.8.22"
|
||||
export const APP_VERSION = "0.8.23"
|
||||
export const BUILD_DATE = "2026-05-05"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user