DGSVO Compliance update 1 #30

Merged
Lars merged 48 commits from develop into main 2026-05-12 06:34:15 +02:00
4 changed files with 30 additions and 59 deletions
Showing only changes of commit 56e952f084 - Show all commits

View File

@ -159,37 +159,30 @@ def check_rights_coverage(cur: Any, asset_id: int, target_visibility: str) -> st
Returns: Returns:
'ok' - vorhandene Erklaerung reicht aus 'ok' - vorhandene Erklaerung reicht aus
'insufficient' - Erklaerung vorhanden, aber fuer niedrigere Sichtbarkeit 'legacy' - Altmedium ohne Erklaerung (legacy_unreviewed)
'legacy' - Altmedium ohne Erklaerung
'blocked' - durch Admin gesperrt 'blocked' - durch Admin gesperrt
'no_declaration' - neues Medium ohne Erklaerung (sollte nicht vorkommen) 'no_declaration' - neues Medium ohne Erklaerung (sollte nicht vorkommen)
Hinweis: Eine P-06-Erklaerung beschreibt den Inhalt (Rechteinhaber, Personen, Musik etc.)
und ist sichtbarkeitsunabhaengig. rights_status='declared' gilt daher fuer alle
Sichtbarkeits-Stufen ohne Levelvergleich.
""" """
cur.execute( cur.execute(
"SELECT rights_status, rights_declared_for_visibility FROM media_assets WHERE id = %s", "SELECT rights_status FROM media_assets WHERE id = %s",
(asset_id,), (asset_id,),
) )
row = cur.fetchone() row = cur.fetchone()
if not row: if not row:
return "no_declaration" return "no_declaration"
# psycopg2 RealDictCursor oder ähnlich rs = (row[0] if not hasattr(row, "keys") else row["rights_status"] or "").strip().lower()
if hasattr(row, "keys"):
rs = row["rights_status"]
rdv = row["rights_declared_for_visibility"]
else:
rs, rdv = row[0], row[1]
rs = (rs or "").strip().lower()
rdv = (rdv or "").strip().lower() if rdv else None
if rs == "blocked": if rs == "blocked":
return "blocked" return "blocked"
if rs == "legacy_unreviewed": if rs == "legacy_unreviewed":
return "legacy" return "legacy"
if rs == "declared": if rs == "declared":
if rights_covers_target(rdv, target_visibility):
return "ok" return "ok"
return "insufficient"
return "no_declaration" return "no_declaration"
@ -210,19 +203,6 @@ def assert_rights_for_promotion(cur: Any, asset_id: int, target_visibility: str)
"asset_id": asset_id, "asset_id": asset_id,
}, },
) )
if status == "insufficient":
raise HTTPException(
status_code=400,
detail={
"code": "RIGHTS_SCOPE_INSUFFICIENT",
"message": (
f"Die vorhandene Erklaerung gilt nicht fuer die Ziel-Sichtbarkeit '{target_visibility}'. "
"Bitte eine neue Erklaerung fuer diese Sichtbarkeit abgeben."
),
"asset_id": asset_id,
"target_visibility": target_visibility,
},
)
if status == "blocked": if status == "blocked":
raise HTTPException( raise HTTPException(
status_code=403, status_code=403,

View File

@ -215,28 +215,18 @@ class TestCheckRightsCoverage:
assert check_rights_coverage(cur, 1, "private") == "no_declaration" assert check_rights_coverage(cur, 1, "private") == "no_declaration"
def test_blocked_returns_blocked(self): def test_blocked_returns_blocked(self):
cur = self._cur({"rights_status": "blocked", "rights_declared_for_visibility": None}) cur = self._cur({"rights_status": "blocked"})
assert check_rights_coverage(cur, 1, "private") == "blocked" assert check_rights_coverage(cur, 1, "private") == "blocked"
def test_legacy_returns_legacy(self): def test_legacy_returns_legacy(self):
cur = self._cur({"rights_status": "legacy_unreviewed", "rights_declared_for_visibility": None}) cur = self._cur({"rights_status": "legacy_unreviewed"})
assert check_rights_coverage(cur, 1, "club") == "legacy" assert check_rights_coverage(cur, 1, "club") == "legacy"
def test_declared_private_covers_private(self): def test_declared_covers_any_visibility(self):
cur = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "private"}) # Eine bestehende Erklaerung gilt sichtbarkeitsunabhaengig
assert check_rights_coverage(cur, 1, "private") == "ok" for target in ("private", "club", "official"):
cur = self._cur({"rights_status": "declared"})
def test_declared_private_insufficient_for_club(self): assert check_rights_coverage(cur, 1, target) == "ok", f"failed for target={target}"
cur = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "private"})
assert check_rights_coverage(cur, 1, "club") == "insufficient"
def test_declared_official_covers_all(self):
cur = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "official"})
assert check_rights_coverage(cur, 1, "private") == "ok"
cur2 = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "official"})
assert check_rights_coverage(cur2, 1, "club") == "ok"
cur3 = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "official"})
assert check_rights_coverage(cur3, 1, "official") == "ok"
# =========================================================================== # ===========================================================================
@ -250,31 +240,25 @@ class TestAssertRightsForPromotion:
cur.fetchone.return_value = row cur.fetchone.return_value = row
return cur return cur
def test_ok_passes(self): def test_ok_passes_for_any_target(self):
cur = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "official"}) for target in ("private", "club", "official"):
assert_rights_for_promotion(cur, 1, "official") # no raise cur = self._cur({"rights_status": "declared"})
assert_rights_for_promotion(cur, 1, target) # no raise
def test_legacy_raises_legacy_code(self): def test_legacy_raises_legacy_code(self):
cur = self._cur({"rights_status": "legacy_unreviewed", "rights_declared_for_visibility": None}) cur = self._cur({"rights_status": "legacy_unreviewed"})
with pytest.raises(HTTPException) as exc: with pytest.raises(HTTPException) as exc:
assert_rights_for_promotion(cur, 1, "club") assert_rights_for_promotion(cur, 1, "club")
assert exc.value.status_code == 400 assert exc.value.status_code == 400
assert exc.value.detail["code"] == "LEGACY_REDECLARATION_REQUIRED" assert exc.value.detail["code"] == "LEGACY_REDECLARATION_REQUIRED"
def test_blocked_raises_403(self): def test_blocked_raises_403(self):
cur = self._cur({"rights_status": "blocked", "rights_declared_for_visibility": None}) cur = self._cur({"rights_status": "blocked"})
with pytest.raises(HTTPException) as exc: with pytest.raises(HTTPException) as exc:
assert_rights_for_promotion(cur, 1, "official") assert_rights_for_promotion(cur, 1, "official")
assert exc.value.status_code == 403 assert exc.value.status_code == 403
assert exc.value.detail["code"] == "RIGHTS_BLOCKED" assert exc.value.detail["code"] == "RIGHTS_BLOCKED"
def test_insufficient_raises_scope_code(self):
cur = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "private"})
with pytest.raises(HTTPException) as exc:
assert_rights_for_promotion(cur, 1, "official")
assert exc.value.status_code == 400
assert exc.value.detail["code"] == "RIGHTS_SCOPE_INSUFFICIENT"
# =========================================================================== # ===========================================================================
# 4. PATCH /api/media-assets/{id} P-06-Promotion via HTTP # 4. PATCH /api/media-assets/{id} P-06-Promotion via HTTP

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information # Shinkan Jinkendo Version Information
APP_VERSION = "0.8.80" APP_VERSION = "0.8.81"
BUILD_DATE = "2026-05-11" BUILD_DATE = "2026-05-11"
DB_SCHEMA_VERSION = "20260511048" DB_SCHEMA_VERSION = "20260511048"
@ -31,6 +31,13 @@ MODULE_VERSIONS = {
} }
CHANGELOG = [ CHANGELOG = [
{
"version": "0.8.81",
"date": "2026-05-11",
"changes": [
"Fix P-06: check_rights_coverage gibt bei rights_status='declared' immer 'ok' zurueck — Erklaerung gilt sichtbarkeitsunabhaengig (Inhalt aendert sich nicht durch Sichtbarkeits-Promotion). 'insufficient'-Pfad entfernt. Tests angepasst.",
],
},
{ {
"version": "0.8.80", "version": "0.8.80",
"date": "2026-05-11", "date": "2026-05-11",

View File

@ -1,6 +1,6 @@
// Shinkan Jinkendo Frontend Version // Shinkan Jinkendo Frontend Version
export const APP_VERSION = "0.8.80" export const APP_VERSION = "0.8.81"
export const BUILD_DATE = "2026-05-11" export const BUILD_DATE = "2026-05-11"
export const PAGE_VERSIONS = { export const PAGE_VERSIONS = {