fix(P-13): leserliche Journals, readOnly-Felder, Meldungs-Badge auf Medienkacheln
Some checks failed
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Failing after 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 56s
Some checks failed
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Failing after 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 56s
- ReportContentModal: Name/E-Mail readOnly fuer eingeloggte Nutzer - MediaLibraryPage: content_report_filed im Journal leserlich (Meldegrund, Prioritaet, Status DE) - MediaLibraryPage: Badge auf Medienkacheln zeigt offene Meldungsanzahl (nur Admins) - media_assets.py: open_report_count Subquery fuer Admin-Sicht in Listendaten - Inbox-500-Fix: ma.media_kind -> ma.mime_type (alle 3 Stellen) - PATCH content_reports: Statuswechsel wird in Audit-Log protokolliert - E-Mails: Dateiname statt Medium-ID, lesbarer Inhalt version: 0.8.91 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8dd748e7d9
commit
bb4d927090
|
|
@ -341,32 +341,33 @@ def _notify_report_submitted(
|
|||
priority: str,
|
||||
target_type: str,
|
||||
target_id: int,
|
||||
target_label: str,
|
||||
admin_emails: list[str],
|
||||
) -> None:
|
||||
"""Sendet Bestaetigungs-Mail an Melder + Benachrichtigung an alle Plattform-Admins (best-effort)."""
|
||||
app_url = (os.getenv("APP_URL") or "https://shinkan.jinkendo.de").rstrip("/")
|
||||
reason_label = REASON_LABELS_DE.get(reason, reason)
|
||||
target_label = ("Medium" if target_type == "media_asset" else "Übung") + f" #{target_id}"
|
||||
prio_label = "DRINGEND" if priority == "high" else "normal"
|
||||
|
||||
# Bestaetigungs-Mail an Melder
|
||||
confirmation_body = (
|
||||
f"Hallo {reporter_name},\n\n"
|
||||
f"Ihre Meldung (#{report_id}) wurde entgegengenommen.\n\n"
|
||||
f"Meldegrund: {reason_label}\n"
|
||||
f"Meldegrund: {reason_label}\n"
|
||||
f"Gemeldeter Inhalt: {target_label}\n\n"
|
||||
f"Ein Administrator wird Ihre Meldung zeitnah prüfen.\n\n"
|
||||
f"Shinkan Jinkendo"
|
||||
f"Ein Administrator wird Ihre Meldung zeitnah prüfen. Sie müssen nichts weiter tun.\n\n"
|
||||
f"Shinkan Jinkendo\n"
|
||||
f"{app_url}"
|
||||
)
|
||||
_send_email(reporter_email, f"Meldung #{report_id} eingegangen – Shinkan Jinkendo", confirmation_body)
|
||||
|
||||
# Admin-Benachrichtigung
|
||||
prio_label = "DRINGEND" if priority == "high" else "normal"
|
||||
admin_body = (
|
||||
f"Neue Inhaltsmeldung #{report_id} [{prio_label}]\n\n"
|
||||
f"Meldegrund: {reason_label}\n"
|
||||
f"Ziel: {target_label}\n"
|
||||
f"Gemeldet von: {reporter_name} <{reporter_email}>\n\n"
|
||||
f"Zur Inbox: {app_url}/inbox\n"
|
||||
f"Meldegrund: {reason_label}\n"
|
||||
f"Gemeldeter Inhalt: {target_label}\n"
|
||||
f"Gemeldet von: {reporter_name} <{reporter_email}>\n\n"
|
||||
f"Posteingang öffnen: {app_url}/inbox\n"
|
||||
)
|
||||
subject = f"Inhaltsmeldung #{report_id} [{prio_label}] – Shinkan Jinkendo"
|
||||
for email in admin_emails:
|
||||
|
|
@ -456,6 +457,24 @@ def submit_content_report(
|
|||
result = r2d(row)
|
||||
report_id = int(result["id"])
|
||||
|
||||
# Dateinamen / Zielbezeichnung fuer E-Mail abfragen
|
||||
target_label_for_email = f"{'Medium' if body.target_type == 'media_asset' else 'Übung'} #{body.target_id}"
|
||||
if body.target_type == "media_asset":
|
||||
cur.execute("SELECT original_filename FROM media_assets WHERE id = %s", (body.target_id,))
|
||||
fn_row = cur.fetchone()
|
||||
if fn_row:
|
||||
fn = r2d(fn_row).get("original_filename") or ""
|
||||
if fn:
|
||||
target_label_for_email = f"{fn} (Medium #{body.target_id})"
|
||||
|
||||
elif body.target_type == "exercise":
|
||||
cur.execute("SELECT name FROM exercises WHERE id = %s", (body.target_id,))
|
||||
ex_row = cur.fetchone()
|
||||
if ex_row:
|
||||
ex_name = r2d(ex_row).get("name") or ""
|
||||
if ex_name:
|
||||
target_label_for_email = f"{ex_name} (Übung #{body.target_id})"
|
||||
|
||||
# Audit-Log-Eintrag (nur fuer media_asset-Meldungen)
|
||||
if body.target_type == "media_asset":
|
||||
write_audit_log_entry(
|
||||
|
|
@ -466,9 +485,8 @@ def submit_content_report(
|
|||
{},
|
||||
{
|
||||
"content_report_id": report_id,
|
||||
"report_reason": body.report_reason,
|
||||
"priority": priority,
|
||||
"reporter_email": body.reporter_email,
|
||||
"report_reason": REASON_LABELS_DE.get(body.report_reason, body.report_reason),
|
||||
"priority": "hoch" if priority == "high" else "normal",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -486,6 +504,7 @@ def submit_content_report(
|
|||
priority=priority,
|
||||
target_type=body.target_type,
|
||||
target_id=body.target_id,
|
||||
target_label=target_label_for_email,
|
||||
admin_emails=admin_emails,
|
||||
)
|
||||
|
||||
|
|
@ -539,7 +558,7 @@ def list_inbox_content_reports(
|
|||
cr.updated_at,
|
||||
ma.original_filename AS target_filename,
|
||||
ma.visibility AS target_visibility,
|
||||
ma.media_kind AS target_media_kind,
|
||||
ma.mime_type AS target_mime_type,
|
||||
ma.legal_hold_active AS target_legal_hold_active,
|
||||
ex.name AS target_exercise_name,
|
||||
rev.name AS reviewed_by_name,
|
||||
|
|
@ -596,7 +615,7 @@ def list_inbox_content_reports(
|
|||
cr.updated_at,
|
||||
ma.original_filename AS target_filename,
|
||||
ma.visibility AS target_visibility,
|
||||
ma.media_kind AS target_media_kind,
|
||||
ma.mime_type AS target_mime_type,
|
||||
ma.legal_hold_active AS target_legal_hold_active,
|
||||
ex.name AS target_exercise_name,
|
||||
rev.name AS reviewed_by_name,
|
||||
|
|
@ -643,7 +662,7 @@ def get_content_report(
|
|||
cr.*,
|
||||
ma.original_filename AS target_filename,
|
||||
ma.visibility AS target_visibility,
|
||||
ma.media_kind AS target_media_kind,
|
||||
ma.mime_type AS target_mime_type,
|
||||
ma.legal_hold_active AS target_legal_hold_active,
|
||||
ma.legal_hold_reason_code AS target_legal_hold_reason_code,
|
||||
ex.name AS target_exercise_name,
|
||||
|
|
@ -686,12 +705,25 @@ def patch_content_report(
|
|||
detail="Bei Abschluss ohne Massnahme ist eine Begründung (resolution_note) Pflicht.",
|
||||
)
|
||||
|
||||
STATUS_LABELS_DE = {
|
||||
"submitted": "Eingegangen",
|
||||
"under_review": "In Bearbeitung",
|
||||
"resolved_no_action": "Abgeschlossen (kein Handlungsbedarf)",
|
||||
"resolved_legal_hold": "Abgeschlossen (Legal Hold)",
|
||||
"rejected_invalid": "Abgewiesen (ungültig)",
|
||||
}
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute("SELECT id, status FROM content_reports WHERE id = %s", (report_id,))
|
||||
cur.execute(
|
||||
"SELECT id, status, target_type, target_id FROM content_reports WHERE id = %s",
|
||||
(report_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Meldung nicht gefunden")
|
||||
old_report = r2d(row)
|
||||
old_status = old_report["status"]
|
||||
|
||||
updates = ["updated_at = NOW()"]
|
||||
params = []
|
||||
|
|
@ -721,6 +753,24 @@ def patch_content_report(
|
|||
params,
|
||||
)
|
||||
updated = r2d(cur.fetchone())
|
||||
new_status = updated["status"]
|
||||
|
||||
# Audit-Log-Eintrag bei Status-Aenderung auf media_asset-Meldungen
|
||||
if body.status is not None and old_status != new_status and old_report["target_type"] == "media_asset":
|
||||
resolution = (body.resolution_note or "").strip() or None
|
||||
write_audit_log_entry(
|
||||
cur,
|
||||
int(old_report["target_id"]),
|
||||
pid,
|
||||
"content_report_filed",
|
||||
{"status": STATUS_LABELS_DE.get(old_status, old_status)},
|
||||
{
|
||||
"content_report_id": report_id,
|
||||
"status": STATUS_LABELS_DE.get(new_status, new_status),
|
||||
**({"begründung": resolution} if resolution else {}),
|
||||
},
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
return {"ok": True, "id": updated["id"], "status": updated["status"]}
|
||||
|
|
|
|||
|
|
@ -1168,10 +1168,26 @@ def list_media_assets(
|
|||
show_club = sup or is_adm or bool(admin_club_ids)
|
||||
asset_ids = [int(r["id"]) for r in rows]
|
||||
usage_map = _usage_for_media_assets(cur, asset_ids)
|
||||
# open_report_count: nur für Admins relevant; immer befüllen (0 für normale Nutzer ok)
|
||||
report_count_map: dict[int, int] = {}
|
||||
if asset_ids and (is_adm or bool(admin_club_ids)):
|
||||
cur.execute(
|
||||
"""SELECT target_id, COUNT(*) AS cnt
|
||||
FROM content_reports
|
||||
WHERE target_type = 'media_asset'
|
||||
AND target_id = ANY(%s)
|
||||
AND status IN ('submitted', 'under_review')
|
||||
GROUP BY target_id""",
|
||||
(asset_ids,),
|
||||
)
|
||||
for row in cur.fetchall():
|
||||
rd = r2d(row)
|
||||
report_count_map[int(rd["target_id"])] = int(rd["cnt"])
|
||||
for r in rows:
|
||||
r["permissions"] = _item_permissions(r, tenant, admin_club_ids)
|
||||
tid = int(r["id"])
|
||||
r["usage"] = usage_map.get(tid, {"exercises": [], "training_units": []})
|
||||
r["open_report_count"] = report_count_map.get(tid, 0)
|
||||
tags_val = r.get("tags")
|
||||
if tags_val is None:
|
||||
r["tags"] = []
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.90"
|
||||
APP_VERSION = "0.8.91"
|
||||
BUILD_DATE = "2026-05-11"
|
||||
DB_SCHEMA_VERSION = "20260511053"
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ 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.1", # acting_profile_id in write_audit_log_entry auf Optional[int] (P-13 anonyme Meldungen)
|
||||
"media_assets": "1.18.0", # P-11: Legal-Hold nur fuer Superadmin sichtbar (nicht fuer alle Plattform-Admins)
|
||||
"media_assets": "1.18.1", # P-13: open_report_count in Listendaten (fuer 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",
|
||||
|
|
@ -34,6 +34,16 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.91",
|
||||
"date": "2026-05-11",
|
||||
"changes": [
|
||||
"Fix P-13: Name- und E-Mail-Felder im Melde-Dialog fuer eingeloggte Nutzer nicht mehr bearbeitbar (readOnly).",
|
||||
"Fix P-13: Journaleintraege fuer content_report_filed leserlich (Meldegrund, Prioritaet, Status auf Deutsch).",
|
||||
"Fix P-13: Badge auf Medienkacheln zeigt Anzahl offener Meldungen (nur fuer Admins).",
|
||||
"Fix P-13: Statuswechsel an Meldungen werden im Journal des Mediums protokolliert.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.90",
|
||||
"date": "2026-05-11",
|
||||
|
|
|
|||
|
|
@ -143,7 +143,9 @@ export default function ReportContentModal({ targetType, targetId, targetLabel,
|
|||
type="text"
|
||||
className="form-input"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onChange={user?.name ? undefined : (e) => setName(e.target.value)}
|
||||
readOnly={!!user?.name}
|
||||
style={user?.name ? { background: 'var(--surface2)', color: 'var(--text2)' } : undefined}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -154,7 +156,9 @@ export default function ReportContentModal({ targetType, targetId, targetLabel,
|
|||
type="email"
|
||||
className="form-input"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={user?.email ? undefined : (e) => setEmail(e.target.value)}
|
||||
readOnly={!!user?.email}
|
||||
style={user?.email ? { background: 'var(--surface2)', color: 'var(--text2)' } : undefined}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ function eventTypeLabel(et) {
|
|||
lifecycle_change: 'Lifecycle geändert',
|
||||
legal_hold_set: 'Sofortsperre gesetzt',
|
||||
legal_hold_released: 'Sofortsperre aufgehoben',
|
||||
content_report_filed: 'Inhaltsmeldung',
|
||||
}
|
||||
return MAP[et] || et
|
||||
}
|
||||
|
|
@ -978,6 +979,20 @@ export default function MediaLibraryPage() {
|
|||
title="Altbestand – Rechtserklärung nach neuem Standard (P-06) noch nicht erfasst"
|
||||
>Altbestand ⚠</span>
|
||||
)}
|
||||
{it.open_report_count > 0 && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.7rem',
|
||||
background: 'var(--danger)',
|
||||
color: '#fff',
|
||||
borderRadius: '10px',
|
||||
padding: '1px 6px',
|
||||
marginLeft: 4,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
title={`${it.open_report_count} offene Inhaltsmeldung${it.open_report_count > 1 ? 'en' : ''}`}
|
||||
>{it.open_report_count} Meldung{it.open_report_count > 1 ? 'en' : ''}</span>
|
||||
)}
|
||||
</div>
|
||||
{(it.tags || []).length ? (
|
||||
<div className="media-library__tag-chips">
|
||||
|
|
@ -1598,6 +1613,39 @@ export default function MediaLibraryPage() {
|
|||
<span>{nw.rights_status || '—'}</span>
|
||||
</div>
|
||||
</>
|
||||
) : evt.event_type === 'content_report_filed' ? (
|
||||
<>
|
||||
{nw.content_report_id ? (
|
||||
<div className="media-library__journal-audit-row">
|
||||
<span className="media-library__journal-audit-label">Meldungs-ID:</span>
|
||||
<span>#{nw.content_report_id}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{nw.report_reason ? (
|
||||
<div className="media-library__journal-audit-row">
|
||||
<span className="media-library__journal-audit-label">Meldegrund:</span>
|
||||
<span>{nw.report_reason}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{nw.priority ? (
|
||||
<div className="media-library__journal-audit-row">
|
||||
<span className="media-library__journal-audit-label">Priorität:</span>
|
||||
<span>{nw.priority}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{nw.status ? (
|
||||
<div className="media-library__journal-audit-row">
|
||||
<span className="media-library__journal-audit-label">Status:</span>
|
||||
<span>{old.status ? `${old.status} → ` : ''}{nw.status}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{nw.begründung ? (
|
||||
<div className="media-library__journal-audit-row">
|
||||
<span className="media-library__journal-audit-label">Begründung:</span>
|
||||
<span>{nw.begründung}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
Object.keys(nw).map((k) => (
|
||||
<div key={k} className="media-library__journal-audit-row">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.8.90"
|
||||
export const APP_VERSION = "0.8.91"
|
||||
export const BUILD_DATE = "2026-05-11"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
|
|
@ -20,7 +20,7 @@ export const PAGE_VERSIONS = {
|
|||
TrainingCoachPage: "1.0.0",
|
||||
AdminCatalogsPage: "2.2.0",
|
||||
TrainerContextsPage: "1.0.0",
|
||||
MediaLibraryPage: "1.8.0", // P-13: MediaPreviewModal (geteilt) + Melde-Button in Viewer
|
||||
MediaLibraryPage: "1.9.0", // P-13: open_report_count Badge + Journal content_report_filed
|
||||
ExerciseFormPage: "1.1.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer
|
||||
ExerciseInlineFileMediaModal: "1.1.0", // P-06: RightsDeclarationDialog vor Upload
|
||||
ExerciseInlineEmbedModal: "1.1.0", // P-06: RightsDeclarationDialog vor Embed-Upload
|
||||
|
|
@ -30,5 +30,5 @@ export const PAGE_VERSIONS = {
|
|||
InboxPage: "2.1.0", // P-13: target_filename/target_exercise_name fix; Club-Admin-Sicht
|
||||
OrgInboxContext: "1.1.0", // P-13: canSeeContentReports schliesst Club-Admins ein
|
||||
MediaPreviewModal: "1.0.0", // P-13: geteilter Medienvorschau-Dialog (Melden + Bearbeiten optional)
|
||||
ReportContentModal: "1.0.0", // P-13: Melde-Formular (Grund, Beschreibung, Name, E-Mail)
|
||||
ReportContentModal: "1.1.0", // P-13: Name/E-Mail readOnly fuer eingeloggte Nutzer
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user