feat(exercises): bump version to 0.8.65 and enhance club media copyright handling
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 30s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Successful in 25s
Test Suite / pytest-backend (pull_request) Successful in 23s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 6s
Test Suite / playwright-tests (pull_request) Successful in 23s
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 30s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Successful in 25s
Test Suite / pytest-backend (pull_request) Successful in 23s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 6s
Test Suite / playwright-tests (pull_request) Successful in 23s
- Incremented application version to 0.8.65 and updated changelog with new features. - Added support for setting default copyright notices for club exercises, allowing users to apply a common copyright notice to linked media assets. - Enhanced error handling to prompt users for copyright information when required. - Updated tests to verify the new copyright handling functionality.
This commit is contained in:
parent
a7cecca36f
commit
59fb8a5527
|
|
@ -265,6 +265,8 @@ class ExerciseUpdate(BaseModel):
|
|||
# §4.2: Übung → official — angehängte Datei-Assets anheben + Copyright (nur mit expliziter Bestätigung)
|
||||
promote_attached_media_for_official: Optional[bool] = None
|
||||
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")
|
||||
def normalize_goal_execution(self):
|
||||
|
|
@ -347,6 +349,7 @@ class ExerciseBulkMetadataPatch(BaseModel):
|
|||
target_group_ids: Optional[list[int]] = Field(default=None, max_length=_MAX_BULK_RELATION_IDS_PER_KIND)
|
||||
promote_attached_media_for_official: bool = False
|
||||
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")
|
||||
def at_least_one_patch_field(self):
|
||||
|
|
@ -649,10 +652,19 @@ def apply_official_exercise_media_rules(
|
|||
)
|
||||
|
||||
|
||||
def apply_club_exercise_media_copyright_rules(cur, exercise_id: int, next_visibility: str) -> None:
|
||||
def apply_club_exercise_media_copyright_rules(
|
||||
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
|
||||
(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()
|
||||
if nv != "club":
|
||||
|
|
@ -701,7 +713,11 @@ def apply_club_exercise_media_copyright_rules(cur, exercise_id: int, next_visibi
|
|||
},
|
||||
)
|
||||
|
||||
if missing_cr:
|
||||
if not missing_cr:
|
||||
return
|
||||
|
||||
default_cr = _normalize_media_copyright_notice(default_club_media_copyright)
|
||||
if len(default_cr) < _MIN_OFFICIAL_MEDIA_COPYRIGHT_LEN:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail={
|
||||
|
|
@ -714,6 +730,61 @@ def apply_club_exercise_media_copyright_rules(cur, exercise_id: int, next_visibi
|
|||
},
|
||||
)
|
||||
|
||||
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]:
|
||||
if not file_path_db or file_path_db.startswith("http"):
|
||||
|
|
@ -1322,7 +1393,12 @@ def bulk_patch_exercises_metadata(
|
|||
|
||||
if (next_vis or "").strip().lower() == "club":
|
||||
try:
|
||||
apply_club_exercise_media_copyright_rules(cur, ex_id, next_vis)
|
||||
apply_club_exercise_media_copyright_rules(
|
||||
cur,
|
||||
ex_id,
|
||||
next_vis,
|
||||
default_club_media_copyright=body.default_club_media_copyright,
|
||||
)
|
||||
except HTTPException as he:
|
||||
d = he.detail
|
||||
entry: Dict[str, Any] = {"id": ex_id}
|
||||
|
|
@ -1913,6 +1989,7 @@ def update_exercise(
|
|||
raw_promo = data.pop("promote_attached_media_for_official", None)
|
||||
promote_media_flag = raw_promo is True
|
||||
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}
|
||||
for fld in RICH_HTML_EXERCISE_FIELDS:
|
||||
|
|
@ -1993,7 +2070,12 @@ def update_exercise(
|
|||
|
||||
assign_exercise_relations(cur, conn, exercise_id, data, do_commit=False)
|
||||
try:
|
||||
apply_club_exercise_media_copyright_rules(cur, exercise_id, next_vis)
|
||||
apply_club_exercise_media_copyright_rules(
|
||||
cur,
|
||||
exercise_id,
|
||||
next_vis,
|
||||
default_club_media_copyright=default_club_copy,
|
||||
)
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -60,3 +60,49 @@ def test_apply_club_exercise_media_copyright_ok() -> None:
|
|||
apply_club_exercise_media_copyright_rules(object(), 42, "club")
|
||||
finally:
|
||||
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
|
||||
|
||||
APP_VERSION = "0.8.64"
|
||||
APP_VERSION = "0.8.65"
|
||||
BUILD_DATE = "2026-05-08"
|
||||
DB_SCHEMA_VERSION = "20260508049"
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ MODULE_VERSIONS = {
|
|||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.19.0", # Inline-Medien §11: Fließtext-Platzhalter exercise_media.id, Normalisierung/Validierung; CREATE ohne Platzhalter
|
||||
"exercises": "2.19.1", # Verein: PUT default_club_media_copyright + Prompt beim Speichern (fehlende File-Asset-Copyrights)
|
||||
"training_units": "0.2.0",
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
|
||||
|
|
@ -29,6 +29,13 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
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",
|
||||
"date": "2026-05-08",
|
||||
|
|
|
|||
|
|
@ -654,10 +654,23 @@ function ExerciseFormPage() {
|
|||
firstErr.code === 'CLUB_MEDIA_COPYRIGHT_REQUIRED' &&
|
||||
firstErr.payload?.media_assets
|
||||
) {
|
||||
alert(
|
||||
'Vereinsöffentliche Übungen brauchen bei jeder verknüpften Datei einen Copyright-Vermerk (mind. 3 Zeichen). Bitte in der Medienbibliothek oder den Mediendetails nachtragen.',
|
||||
const miss = firstErr.payload.media_assets.length
|
||||
const msg =
|
||||
`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):',
|
||||
'© ',
|
||||
)
|
||||
throw firstErr
|
||||
if (!defaultCopyright || String(defaultCopyright).trim().length < 3) {
|
||||
alert('Mindestens 3 Zeichen für den Copyright-Vermerk.')
|
||||
throw firstErr
|
||||
}
|
||||
await saveOnce({
|
||||
default_club_media_copyright: String(defaultCopyright).trim(),
|
||||
})
|
||||
} else if (firstErr.status === 422 && firstErr.code === 'CLUB_MEDIA_LIFECYCLE') {
|
||||
alert(
|
||||
'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