""" Übungs-Fließtext: Inline-Verweise auf exercise_media.id (MEDIA_ASSETS_AND_ARCHIVE_SPEC §11). - Kurzsyntax beim Speichern → kanonisches . - Verweise müssen für die betreffende Übung gültige exercise_media-Zeilen sein. """ from __future__ import annotations import re from typing import FrozenSet, Optional, Set from fastapi import HTTPException # {{exerciseMedia:123}} (Whitespace tolerant, case-insensitive Schlüssel) _BRACE_PATTERN = re.compile(r"\{\{\s*exerciseMedia\s*:\s*(\d+)\s*\}\}", re.IGNORECASE) # bereits gespeichertes Markup (einfache Anführungszeichen-varianten durch Regex abgedeckt) _DATA_ATTR_PATTERN = re.compile(r'data-shinkan-exercise-media\s*=\s*["\']?(\d+)["\']?', re.IGNORECASE) RICH_HTML_EXERCISE_FIELDS: FrozenSet[str] = frozenset( {"summary", "goal", "execution", "preparation", "trainer_notes"} ) def normalize_inline_exercise_media_markup(html: Optional[str]) -> Optional[str]: """Wandelt {{exerciseMedia:id}} in kanonisches span mit data-shinkan-exercise-media.""" if html is None: return None if not isinstance(html, str): html = str(html) stripped = html.strip() if not stripped: return html def _repl(match: re.Match) -> str: mid = int(match.group(1)) return ( f'' ) return _BRACE_PATTERN.sub(_repl, html) def collect_inline_exercise_media_ids(html: Optional[str]) -> Set[int]: """Sammelt alle referenzierten exercise_media.ids aus Kurzsyntax und kanonischem Span.""" if html is None or not isinstance(html, str): return set() if not html.strip(): return set() ids: Set[int] = set() ids.update(int(m) for m in _BRACE_PATTERN.findall(html)) ids.update(int(m) for m in _DATA_ATTR_PATTERN.findall(html)) return ids def assert_no_inline_media_references_on_create(ids: Set[int]) -> None: """Neue Übung hat noch keine exercise_media-Zeilen — Platzhalter verboten.""" if not ids: return raise HTTPException( status_code=400, detail={ "code": "INLINE_EXERCISE_MEDIA_ON_CREATE", "message": ( "Medienverweise im Fließtext sind beim ersten Anlegen der Übung nicht möglich. " "Bitte Übung ohne Platzhalter speichern, Medien hochladen oder verknüpfen " "und die Verweise dann bearbeiten ({{exerciseMedia:id}} oder „Medium einfügen“)." ), "invalid_exercise_media_ids": sorted(ids), }, ) def validate_inline_exercise_media_ids_for_exercise(cur, exercise_id: int, ids: Set[int]) -> None: """Prüft, dass jede genannte exercise_media.id zu dieser Übung gehört.""" if not ids: return sid = sorted(ids) ph = ",".join(["%s"] * len(sid)) cur.execute( f"SELECT id FROM exercise_media WHERE exercise_id = %s AND id IN ({ph})", (exercise_id, *sid), ) found = set() for row in cur.fetchall(): rid = row["id"] if isinstance(row, dict) else row[0] found.add(int(rid)) missing = ids - found if missing: raise HTTPException( status_code=400, detail={ "code": "INLINE_EXERCISE_MEDIA_INVALID", "message": "Ein oder mehrere eingebettete Medien-Verweise gehören nicht zu dieser Übung.", "invalid_exercise_media_ids": sorted(missing), }, )