Compare commits
No commits in common. "dbc9057601a061647bef195e837dbbe3e09aa2f0" and "6136813f606f2f41b4014c8c6556ed4079322716" have entirely different histories.
dbc9057601
...
6136813f60
|
|
@ -265,8 +265,6 @@ class ExerciseUpdate(BaseModel):
|
||||||
# §4.2: Übung → official — angehängte Datei-Assets anheben + Copyright (nur mit expliziter Bestätigung)
|
# §4.2: Übung → official — angehängte Datei-Assets anheben + Copyright (nur mit expliziter Bestätigung)
|
||||||
promote_attached_media_for_official: Optional[bool] = None
|
promote_attached_media_for_official: Optional[bool] = None
|
||||||
default_official_media_copyright: Optional[str] = Field(default=None, max_length=2000)
|
default_official_media_copyright: Optional[str] = Field(default=None, max_length=2000)
|
||||||
# Vereins-Übung: fehlende Copyrights an Datei-Assets nach Prompt-Text setzen (PUT-Retry)
|
|
||||||
default_club_media_copyright: Optional[str] = Field(default=None, max_length=2000)
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def normalize_goal_execution(self):
|
def normalize_goal_execution(self):
|
||||||
|
|
@ -349,7 +347,6 @@ class ExerciseBulkMetadataPatch(BaseModel):
|
||||||
target_group_ids: Optional[list[int]] = Field(default=None, max_length=_MAX_BULK_RELATION_IDS_PER_KIND)
|
target_group_ids: Optional[list[int]] = Field(default=None, max_length=_MAX_BULK_RELATION_IDS_PER_KIND)
|
||||||
promote_attached_media_for_official: bool = False
|
promote_attached_media_for_official: bool = False
|
||||||
default_official_media_copyright: Optional[str] = Field(default=None, max_length=2000)
|
default_official_media_copyright: Optional[str] = Field(default=None, max_length=2000)
|
||||||
default_club_media_copyright: Optional[str] = Field(default=None, max_length=2000)
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def at_least_one_patch_field(self):
|
def at_least_one_patch_field(self):
|
||||||
|
|
@ -652,19 +649,10 @@ def apply_official_exercise_media_rules(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def apply_club_exercise_media_copyright_rules(
|
def apply_club_exercise_media_copyright_rules(cur, exercise_id: int, next_visibility: str) -> None:
|
||||||
cur,
|
|
||||||
exercise_id: int,
|
|
||||||
next_visibility: str,
|
|
||||||
*,
|
|
||||||
default_club_media_copyright: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Vereins-sichtbare Übung: angehängte Archiv-Dateien müssen aktiv sein und einen Copyright-Vermerk haben
|
Vereins-sichtbare Übung: angehängte Archiv-Dateien müssen aktiv sein und einen Copyright-Vermerk haben
|
||||||
(wie bei offiziellen Übungen, ohne Sichtbarkeits-Promotion der Assets).
|
(wie bei offiziellen Übungen, ohne Sichtbarkeits-Promotion der Assets).
|
||||||
|
|
||||||
Fehlt das Copyright, kann der Client nach Nutzerbestätigung `default_club_media_copyright`
|
|
||||||
mitsenden (mind. 3 Zeichen) — wird auf alle betroffenen Assets mit zu kurzem/leerem Vermerk gesetzt.
|
|
||||||
"""
|
"""
|
||||||
nv = (next_visibility or "private").strip().lower()
|
nv = (next_visibility or "private").strip().lower()
|
||||||
if nv != "club":
|
if nv != "club":
|
||||||
|
|
@ -713,11 +701,7 @@ def apply_club_exercise_media_copyright_rules(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if not missing_cr:
|
if missing_cr:
|
||||||
return
|
|
||||||
|
|
||||||
default_cr = _normalize_media_copyright_notice(default_club_media_copyright)
|
|
||||||
if len(default_cr) < _MIN_OFFICIAL_MEDIA_COPYRIGHT_LEN:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=422,
|
status_code=422,
|
||||||
detail={
|
detail={
|
||||||
|
|
@ -730,61 +714,6 @@ def apply_club_exercise_media_copyright_rules(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
ids = [int(x["media_asset_id"]) for x in missing_cr]
|
|
||||||
ph = ",".join(["%s"] * len(ids))
|
|
||||||
cur.execute(
|
|
||||||
f"""
|
|
||||||
UPDATE media_assets
|
|
||||||
SET copyright_notice = %s, updated_at = NOW()
|
|
||||||
WHERE id IN ({ph})
|
|
||||||
AND (
|
|
||||||
copyright_notice IS NULL
|
|
||||||
OR LENGTH(TRIM(copyright_notice)) < %s
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
(default_cr, *ids, _MIN_OFFICIAL_MEDIA_COPYRIGHT_LEN),
|
|
||||||
)
|
|
||||||
|
|
||||||
rows2 = _fetch_exercise_linked_file_assets(cur, exercise_id)
|
|
||||||
missing_after: List[Dict[str, Any]] = []
|
|
||||||
blocking_after: List[Dict[str, Any]] = []
|
|
||||||
for r in rows2:
|
|
||||||
aid = int(r["id"])
|
|
||||||
lc = (r.get("lifecycle_state") or "").strip().lower()
|
|
||||||
cr = _normalize_media_copyright_notice(r.get("copyright_notice"))
|
|
||||||
if lc != "active":
|
|
||||||
blocking_after.append({"media_asset_id": aid})
|
|
||||||
continue
|
|
||||||
if len(cr) < _MIN_OFFICIAL_MEDIA_COPYRIGHT_LEN:
|
|
||||||
missing_after.append(
|
|
||||||
{
|
|
||||||
"media_asset_id": aid,
|
|
||||||
"original_filename": r.get("original_filename"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if blocking_after:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=422,
|
|
||||||
detail={
|
|
||||||
"code": "CLUB_MEDIA_LIFECYCLE",
|
|
||||||
"message": "Archiv-Medium nach Copyright-Update nicht mehr aktiv.",
|
|
||||||
"media_assets": blocking_after,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if missing_after:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=422,
|
|
||||||
detail={
|
|
||||||
"code": "CLUB_MEDIA_COPYRIGHT_REQUIRED",
|
|
||||||
"message": (
|
|
||||||
f"Copyright konnte nicht auf alle Dateien angewendet werden "
|
|
||||||
f"(mind. {_MIN_OFFICIAL_MEDIA_COPYRIGHT_LEN} Zeichen pro Eintrag prüfen)."
|
|
||||||
),
|
|
||||||
"media_assets": missing_after,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _abs_media_path(file_path_db: str, media_root: Path) -> Optional[Path]:
|
def _abs_media_path(file_path_db: str, media_root: Path) -> Optional[Path]:
|
||||||
if not file_path_db or file_path_db.startswith("http"):
|
if not file_path_db or file_path_db.startswith("http"):
|
||||||
|
|
@ -1393,12 +1322,7 @@ def bulk_patch_exercises_metadata(
|
||||||
|
|
||||||
if (next_vis or "").strip().lower() == "club":
|
if (next_vis or "").strip().lower() == "club":
|
||||||
try:
|
try:
|
||||||
apply_club_exercise_media_copyright_rules(
|
apply_club_exercise_media_copyright_rules(cur, ex_id, next_vis)
|
||||||
cur,
|
|
||||||
ex_id,
|
|
||||||
next_vis,
|
|
||||||
default_club_media_copyright=body.default_club_media_copyright,
|
|
||||||
)
|
|
||||||
except HTTPException as he:
|
except HTTPException as he:
|
||||||
d = he.detail
|
d = he.detail
|
||||||
entry: Dict[str, Any] = {"id": ex_id}
|
entry: Dict[str, Any] = {"id": ex_id}
|
||||||
|
|
@ -1989,7 +1913,6 @@ def update_exercise(
|
||||||
raw_promo = data.pop("promote_attached_media_for_official", None)
|
raw_promo = data.pop("promote_attached_media_for_official", None)
|
||||||
promote_media_flag = raw_promo is True
|
promote_media_flag = raw_promo is True
|
||||||
default_official_copy = data.pop("default_official_media_copyright", None)
|
default_official_copy = data.pop("default_official_media_copyright", None)
|
||||||
default_club_copy = data.pop("default_club_media_copyright", None)
|
|
||||||
|
|
||||||
merged_rich = {fld: rich_row.get(fld) for fld in RICH_HTML_EXERCISE_FIELDS}
|
merged_rich = {fld: rich_row.get(fld) for fld in RICH_HTML_EXERCISE_FIELDS}
|
||||||
for fld in RICH_HTML_EXERCISE_FIELDS:
|
for fld in RICH_HTML_EXERCISE_FIELDS:
|
||||||
|
|
@ -2070,12 +1993,7 @@ def update_exercise(
|
||||||
|
|
||||||
assign_exercise_relations(cur, conn, exercise_id, data, do_commit=False)
|
assign_exercise_relations(cur, conn, exercise_id, data, do_commit=False)
|
||||||
try:
|
try:
|
||||||
apply_club_exercise_media_copyright_rules(
|
apply_club_exercise_media_copyright_rules(cur, exercise_id, next_vis)
|
||||||
cur,
|
|
||||||
exercise_id,
|
|
||||||
next_vis,
|
|
||||||
default_club_media_copyright=default_club_copy,
|
|
||||||
)
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -60,49 +60,3 @@ def test_apply_club_exercise_media_copyright_ok() -> None:
|
||||||
apply_club_exercise_media_copyright_rules(object(), 42, "club")
|
apply_club_exercise_media_copyright_rules(object(), 42, "club")
|
||||||
finally:
|
finally:
|
||||||
exercises_mod._fetch_exercise_linked_file_assets = orig
|
exercises_mod._fetch_exercise_linked_file_assets = orig
|
||||||
|
|
||||||
|
|
||||||
def test_apply_club_exercise_media_copyright_applies_default() -> None:
|
|
||||||
"""Nach Bestätigung setzt der Client default_club_media_copyright — Backend schreibt auf Assets."""
|
|
||||||
orig_fetch = exercises_mod._fetch_exercise_linked_file_assets
|
|
||||||
calls = {"n": 0}
|
|
||||||
|
|
||||||
def mock_fetch(_cur, _eid: int):
|
|
||||||
calls["n"] += 1
|
|
||||||
if calls["n"] == 1:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"visibility": "private",
|
|
||||||
"club_id": 1,
|
|
||||||
"lifecycle_state": "active",
|
|
||||||
"copyright_notice": "",
|
|
||||||
"original_filename": "x.jpg",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"visibility": "private",
|
|
||||||
"club_id": 1,
|
|
||||||
"lifecycle_state": "active",
|
|
||||||
"copyright_notice": "© Testverein",
|
|
||||||
"original_filename": "x.jpg",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
class MockCur:
|
|
||||||
def execute(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
exercises_mod._fetch_exercise_linked_file_assets = mock_fetch
|
|
||||||
try:
|
|
||||||
apply_club_exercise_media_copyright_rules(
|
|
||||||
MockCur(),
|
|
||||||
42,
|
|
||||||
"club",
|
|
||||||
default_club_media_copyright="© Testverein",
|
|
||||||
)
|
|
||||||
assert calls["n"] == 2
|
|
||||||
finally:
|
|
||||||
exercises_mod._fetch_exercise_linked_file_assets = orig_fetch
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.65"
|
APP_VERSION = "0.8.64"
|
||||||
BUILD_DATE = "2026-05-08"
|
BUILD_DATE = "2026-05-08"
|
||||||
DB_SCHEMA_VERSION = "20260508049"
|
DB_SCHEMA_VERSION = "20260508049"
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ MODULE_VERSIONS = {
|
||||||
"groups": "0.1.0",
|
"groups": "0.1.0",
|
||||||
"skills": "0.1.0",
|
"skills": "0.1.0",
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.19.1", # Verein: PUT default_club_media_copyright + Prompt beim Speichern (fehlende File-Asset-Copyrights)
|
"exercises": "2.19.0", # Inline-Medien §11: Fließtext-Platzhalter exercise_media.id, Normalisierung/Validierung; CREATE ohne Platzhalter
|
||||||
"training_units": "0.2.0",
|
"training_units": "0.2.0",
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
|
"planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
|
||||||
|
|
@ -29,13 +29,6 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
{
|
|
||||||
"version": "0.8.65",
|
|
||||||
"date": "2026-05-08",
|
|
||||||
"changes": [
|
|
||||||
"Übung auf Verein: fehlende Copyrights an Datei-Assets wieder per Bestätigung + Eingabe beim Speichern setzbar (PUT default_club_media_copyright; Bulk-PATCH ebenfalls)",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "0.8.64",
|
"version": "0.8.64",
|
||||||
"date": "2026-05-08",
|
"date": "2026-05-08",
|
||||||
|
|
|
||||||
|
|
@ -654,23 +654,10 @@ function ExerciseFormPage() {
|
||||||
firstErr.code === 'CLUB_MEDIA_COPYRIGHT_REQUIRED' &&
|
firstErr.code === 'CLUB_MEDIA_COPYRIGHT_REQUIRED' &&
|
||||||
firstErr.payload?.media_assets
|
firstErr.payload?.media_assets
|
||||||
) {
|
) {
|
||||||
const miss = firstErr.payload.media_assets.length
|
alert(
|
||||||
const msg =
|
'Vereinsöffentliche Übungen brauchen bei jeder verknüpften Datei einen Copyright-Vermerk (mind. 3 Zeichen). Bitte in der Medienbibliothek oder den Mediendetails nachtragen.',
|
||||||
`Vereinsöffentliche Übungen brauchen bei jeder verknüpften Datei einen Copyright-Vermerk (mind. 3 Zeichen). ` +
|
|
||||||
`${miss} Datei(en) sind noch ohne ausreichenden Vermerk. ` +
|
|
||||||
`Beim Speichern einen gemeinsamen Vermerk für diese Dateien setzen?`
|
|
||||||
if (!window.confirm(msg)) throw firstErr
|
|
||||||
const defaultCopyright = window.prompt(
|
|
||||||
'Copyright-Vermerk für die betroffenen Dateien (mind. 3 Zeichen):',
|
|
||||||
'© ',
|
|
||||||
)
|
)
|
||||||
if (!defaultCopyright || String(defaultCopyright).trim().length < 3) {
|
|
||||||
alert('Mindestens 3 Zeichen für den Copyright-Vermerk.')
|
|
||||||
throw firstErr
|
throw firstErr
|
||||||
}
|
|
||||||
await saveOnce({
|
|
||||||
default_club_media_copyright: String(defaultCopyright).trim(),
|
|
||||||
})
|
|
||||||
} else if (firstErr.status === 422 && firstErr.code === 'CLUB_MEDIA_LIFECYCLE') {
|
} else if (firstErr.status === 422 && firstErr.code === 'CLUB_MEDIA_LIFECYCLE') {
|
||||||
alert(
|
alert(
|
||||||
'Speichern nicht möglich: mindestens ein verknüpftes Medium ist nicht aktiv (Papierkorb). Bitte reaktivieren oder entfernen.',
|
'Speichern nicht möglich: mindestens ein verknüpftes Medium ist nicht aktiv (Papierkorb). Bitte reaktivieren oder entfernen.',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user