feat(P-11): implement legal hold functionality for media assets and update app version to 0.8.86
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 57s
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 57s
This commit is contained in:
parent
f79f83e8f9
commit
ee54f8380f
|
|
@ -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,
|
"""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.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,
|
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
|
FROM exercise_media em
|
||||||
LEFT JOIN media_assets ma ON ma.id = em.media_asset_id
|
LEFT JOIN media_assets ma ON ma.id = em.media_asset_id
|
||||||
WHERE em.exercise_id = %s
|
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,
|
"""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.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,
|
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
|
FROM exercise_media em
|
||||||
LEFT JOIN media_assets ma ON ma.id = em.media_asset_id
|
LEFT JOIN media_assets ma ON ma.id = em.media_asset_id
|
||||||
WHERE em.id = %s AND em.exercise_id = %s""",
|
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():
|
if (media.get("embed_url") or "").strip():
|
||||||
raise HTTPException(status_code=400, detail="Embed-Medien haben keine Datei-URL")
|
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()
|
lc = (media.get("asset_lifecycle_state") or "active").strip().lower()
|
||||||
if lc == "trash_hidden":
|
if lc == "trash_hidden":
|
||||||
_assert_can_edit_exercise(cur, exercise_id, tenant)
|
_assert_can_edit_exercise(cur, exercise_id, tenant)
|
||||||
|
|
|
||||||
|
|
@ -558,7 +558,7 @@ def _list_main_visibility_where(
|
||||||
raise HTTPException(status_code=400, detail="Ungültiger lifecycle-Filter")
|
raise HTTPException(status_code=400, detail="Ungültiger lifecycle-Filter")
|
||||||
|
|
||||||
active_sql, active_params = _list_active_visibility_clause(
|
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(
|
trash_sql, trash_params = _list_trash_visibility_clause(
|
||||||
is_plat, is_sup, profile_id, admin_club_ids
|
is_plat, is_sup, profile_id, admin_club_ids
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.85"
|
APP_VERSION = "0.8.86"
|
||||||
BUILD_DATE = "2026-05-11"
|
BUILD_DATE = "2026-05-11"
|
||||||
DB_SCHEMA_VERSION = "20260511051"
|
DB_SCHEMA_VERSION = "20260511051"
|
||||||
|
|
||||||
|
|
@ -15,13 +15,13 @@ MODULE_VERSIONS = {
|
||||||
"admin_users": "1.0.0", # GET /api/admin/users
|
"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)
|
"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_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_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
|
"media_lifecycle": "1.1.0", # P-11: Retention-Job ueberspringt Legal-Hold-Assets
|
||||||
"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.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_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
|
||||||
|
|
@ -33,6 +33,17 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
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",
|
"version": "0.8.85",
|
||||||
"date": "2026-05-11",
|
"date": "2026-05-11",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,15 @@ export default function ExerciseMediaEmbed({ exerciseId, media, layoutSize = 'me
|
||||||
: { maxWidth: 'min(560px, 85vw)', marginTop: '0.5rem' }
|
: { maxWidth: 'min(560px, 85vw)', marginTop: '0.5rem' }
|
||||||
|
|
||||||
if (!media || exerciseId == null) return null
|
if (!media || exerciseId == null) return null
|
||||||
|
|
||||||
|
if (media.asset_legal_hold_active) {
|
||||||
|
return (
|
||||||
|
<div style={{ ...box, color: 'var(--danger)', fontSize: '0.85rem', padding: '8px 0' }}>
|
||||||
|
Medium nicht verfügbar (gesperrt)
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (media.embed_url) {
|
if (media.embed_url) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,32 @@ export default function ExerciseMediaThumbTile({ exerciseId, media, onOpenPrevie
|
||||||
height: '100%',
|
height: '100%',
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (media.asset_legal_hold_active) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: 'rgba(216, 90, 48, 0.10)',
|
||||||
|
border: '1px solid rgba(216, 90, 48, 0.35)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
title: 'Gesperrt',
|
||||||
|
}}
|
||||||
|
title="Medium gesperrt"
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '11px', color: 'var(--danger)', textAlign: 'center', padding: '4px' }}>
|
||||||
|
Gesperrt
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
|
|
||||||
|
|
@ -1676,7 +1676,11 @@ function ExerciseFormPage() {
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<h3 style={{ marginTop: 0, fontSize: '1.05rem' }}>Vorschau</h3>
|
<h3 style={{ marginTop: 0, fontSize: '1.05rem' }}>Vorschau</h3>
|
||||||
{mediaPreview.embed_url ? (
|
{mediaPreview.asset_legal_hold_active ? (
|
||||||
|
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>
|
||||||
|
Dieses Medium ist gesperrt und steht nicht zur Verfügung.
|
||||||
|
</p>
|
||||||
|
) : mediaPreview.embed_url ? (
|
||||||
<p style={{ fontSize: '14px', wordBreak: 'break-all' }}>
|
<p style={{ fontSize: '14px', wordBreak: 'break-all' }}>
|
||||||
<a href={mediaPreview.embed_url} target="_blank" rel="noreferrer">
|
<a href={mediaPreview.embed_url} target="_blank" rel="noreferrer">
|
||||||
{mediaPreview.embed_url}
|
{mediaPreview.embed_url}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Shinkan Jinkendo Frontend Version
|
// 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 BUILD_DATE = "2026-05-11"
|
||||||
|
|
||||||
export const PAGE_VERSIONS = {
|
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
|
MediaLibraryPage: "1.6.0", // P-11: Legal-Hold-Badge, Superadmin-Aktionen, Bestaetigungs-Dialog
|
||||||
ExerciseInlineFileMediaModal: "1.1.0", // P-06: RightsDeclarationDialog vor Upload
|
ExerciseInlineFileMediaModal: "1.1.0", // P-06: RightsDeclarationDialog vor Upload
|
||||||
ExerciseInlineEmbedModal: "1.1.0", // P-06: RightsDeclarationDialog vor Embed-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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user