All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 24s
- Added functionality for inline media references in exercise text using `{{exerciseMedia:id}}` syntax, which normalizes to a canonical `<span>` element.
- Updated the frontend to utilize `ExerciseRichTextBlock` for rendering exercise content, allowing for embedded media display.
- Enhanced the Rich Text Editor to support inserting inline media placeholders.
- Version bump to 0.8.60 to reflect these changes in media handling and exercise content management.
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
"""
|
|
Übungs-Fließtext: Inline-Verweise auf exercise_media.id (MEDIA_ASSETS_AND_ARCHIVE_SPEC §11).
|
|
|
|
- Kurzsyntax beim Speichern → kanonisches <span data-shinkan-exercise-media="…">.
|
|
- 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'<span data-shinkan-exercise-media="{mid}" class="shinkan-inline-media"></span>'
|
|
|
|
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),
|
|
},
|
|
)
|