From ee54f8380f29e5da294af047fa25fca0beed3f11 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 11 May 2026 13:34:41 +0200 Subject: [PATCH] feat(P-11): implement legal hold functionality for media assets and update app version to 0.8.86 --- backend/routers/exercises.py | 12 +++++++-- backend/routers/media_assets.py | 2 +- backend/version.py | 17 +++++++++--- .../src/components/ExerciseMediaEmbed.jsx | 9 +++++++ .../src/components/ExerciseMediaThumbTile.jsx | 26 +++++++++++++++++++ frontend/src/pages/ExerciseFormPage.jsx | 6 ++++- frontend/src/version.js | 4 ++- 7 files changed, 68 insertions(+), 8 deletions(-) diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py index 89e8939..39e0075 100644 --- a/backend/routers/exercises.py +++ b/backend/routers/exercises.py @@ -923,7 +923,8 @@ def enrich_exercise_detail(exercise_id: int, cur) -> dict: """SELECT em.id, em.media_type, em.file_path, em.file_size, em.mime_type, em.original_filename, em.embed_url, em.embed_platform, em.title, em.description, em.sort_order, em.is_primary, em.context, em.media_asset_id, ma.copyright_notice AS asset_copyright_notice, - ma.lifecycle_state AS asset_lifecycle_state + ma.lifecycle_state AS asset_lifecycle_state, + ma.legal_hold_active AS asset_legal_hold_active FROM exercise_media em LEFT JOIN media_assets ma ON ma.id = em.media_asset_id WHERE em.exercise_id = %s @@ -2466,7 +2467,8 @@ def _fetch_media_row(cur, exercise_id: int, media_id: int) -> Optional[dict]: """SELECT em.id, em.exercise_id, em.media_type, em.file_path, em.file_size, em.mime_type, em.original_filename, em.embed_url, em.embed_platform, em.title, em.description, em.sort_order, em.is_primary, em.context, em.created_at, em.media_asset_id, ma.storage_key AS asset_storage_key, - ma.lifecycle_state AS asset_lifecycle_state + ma.lifecycle_state AS asset_lifecycle_state, + ma.legal_hold_active AS asset_legal_hold_active FROM exercise_media em LEFT JOIN media_assets ma ON ma.id = em.media_asset_id WHERE em.id = %s AND em.exercise_id = %s""", @@ -2496,6 +2498,12 @@ def download_exercise_media_file( if (media.get("embed_url") or "").strip(): raise HTTPException(status_code=400, detail="Embed-Medien haben keine Datei-URL") + if bool(media.get("asset_legal_hold_active")): + raise HTTPException( + status_code=451, + detail={"code": "LEGAL_HOLD_ACTIVE", "message": "Dieses Medium ist gesperrt und steht nicht zur Verfügung."} + ) + lc = (media.get("asset_lifecycle_state") or "active").strip().lower() if lc == "trash_hidden": _assert_can_edit_exercise(cur, exercise_id, tenant) diff --git a/backend/routers/media_assets.py b/backend/routers/media_assets.py index 16bd252..975eabe 100644 --- a/backend/routers/media_assets.py +++ b/backend/routers/media_assets.py @@ -558,7 +558,7 @@ def _list_main_visibility_where( raise HTTPException(status_code=400, detail="Ungültiger lifecycle-Filter") active_sql, active_params = _list_active_visibility_clause( - is_plat, profile_id, include_legal_hold=(is_plat or is_sup) + is_plat, profile_id, include_legal_hold=is_sup ) trash_sql, trash_params = _list_trash_visibility_clause( is_plat, is_sup, profile_id, admin_club_ids diff --git a/backend/version.py b/backend/version.py index cfed7f6..f7e5c4d 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.85" +APP_VERSION = "0.8.86" BUILD_DATE = "2026-05-11" DB_SCHEMA_VERSION = "20260511051" @@ -15,13 +15,13 @@ MODULE_VERSIONS = { "admin_users": "1.0.0", # GET /api/admin/users "platform_media_storage": "1.0.0", # GET/PUT /api/admin/platform-media-storage (Superadmin-Pfad unter MEDIA_ROOT) "media_rights": "1.3.0", # P-11: write_audit_log_entry + legal_hold_set/released events - "media_assets": "1.17.0", # P-11: Legal-Hold-Sichtbarkeitsfilter + Admin-Endpoints (set/release/list) + "media_assets": "1.18.0", # P-11: Legal-Hold nur fuer Superadmin sichtbar (nicht fuer alle Plattform-Admins) "media_legal_hold": "1.0.0", # P-11: Sofortsperre-Services (set_legal_hold, release_legal_hold) "media_lifecycle": "1.1.0", # P-11: Retention-Job ueberspringt Legal-Hold-Assets "groups": "0.1.0", "skills": "0.1.0", "methods": "0.1.0", - "exercises": "2.22.0", # P-11: assert_not_under_legal_hold bei from-asset Medienverknuepfung + "exercises": "2.23.0", # P-11: enrich_exercise_detail + download_file blocken Legal-Hold-Assets (451) "training_units": "0.2.0", "training_programs": "0.1.0", "planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile @@ -33,6 +33,17 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.86", + "date": "2026-05-11", + "changes": [ + "Fix P-11: download_exercise_media_file gibt 451 zurück für Legal-Hold-Assets (Datei nicht mehr auslieferbar).", + "Fix P-11: enrich_exercise_detail liefert asset_legal_hold_active im Media-Array (Frontend-Komponenten koennen Hold erkennen).", + "Fix P-11: ExerciseMediaEmbed + ExerciseMediaThumbTile zeigen 'Medium nicht verfügbar / Gesperrt' statt Datei laden.", + "Fix P-11: ExerciseFormPage Vorschau-Modal zeigt Hinweis statt Datei bei Legal-Hold.", + "Fix P-11: Media-Bibliothek-Liste (list_media_assets) schliesst Legal-Hold fuer Plattform-Admins aus — nur Superadmin sieht sie.", + ], + }, { "version": "0.8.85", "date": "2026-05-11", diff --git a/frontend/src/components/ExerciseMediaEmbed.jsx b/frontend/src/components/ExerciseMediaEmbed.jsx index 7e3f11e..37cb634 100644 --- a/frontend/src/components/ExerciseMediaEmbed.jsx +++ b/frontend/src/components/ExerciseMediaEmbed.jsx @@ -16,6 +16,15 @@ export default function ExerciseMediaEmbed({ exerciseId, media, layoutSize = 'me : { maxWidth: 'min(560px, 85vw)', marginTop: '0.5rem' } if (!media || exerciseId == null) return null + + if (media.asset_legal_hold_active) { + return ( +
+ Medium nicht verfügbar (gesperrt) +
+ ) + } + if (media.embed_url) { return (
+ + Gesperrt + +
+ ) + } + return (
e.stopPropagation()} >

Vorschau

- {mediaPreview.embed_url ? ( + {mediaPreview.asset_legal_hold_active ? ( +

+ Dieses Medium ist gesperrt und steht nicht zur Verfügung. +

+ ) : mediaPreview.embed_url ? (

{mediaPreview.embed_url} diff --git a/frontend/src/version.js b/frontend/src/version.js index caae68c..c397729 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -1,6 +1,6 @@ // Shinkan Jinkendo Frontend Version -export const APP_VERSION = "0.8.85" +export const APP_VERSION = "0.8.86" export const BUILD_DATE = "2026-05-11" export const PAGE_VERSIONS = { @@ -23,4 +23,6 @@ export const PAGE_VERSIONS = { MediaLibraryPage: "1.6.0", // P-11: Legal-Hold-Badge, Superadmin-Aktionen, Bestaetigungs-Dialog ExerciseInlineFileMediaModal: "1.1.0", // P-06: RightsDeclarationDialog vor Upload ExerciseInlineEmbedModal: "1.1.0", // P-06: RightsDeclarationDialog vor Embed-Upload + ExerciseMediaEmbed: "1.1.0", // P-11: Legal-Hold-Placeholder statt Datei + ExerciseMediaThumbTile: "1.1.0", // P-11: Legal-Hold-Kachel statt Datei-Vorschau }