fix(p06): declared-Status deckt alle Sichtbarkeiten ab (kein Level-Vergleich mehr)
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 58s

- check_rights_coverage: rights_status='declared' gibt immer 'ok' zurück
  (P-06-Erklärung gilt inhaltlich, nicht sichtbarkeitsabhängig)
- assert_rights_for_promotion: 'insufficient'-Pfad entfernt
- Tests: test_declared_private_insufficient_for_club → test_declared_covers_any_visibility

version: 0.8.81
module: media_rights

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-11 09:45:06 +02:00
parent 6586d3b68b
commit 56e952f084
4 changed files with 30 additions and 59 deletions

View File

@ -159,37 +159,30 @@ def check_rights_coverage(cur: Any, asset_id: int, target_visibility: str) -> st
Returns:
'ok' - vorhandene Erklaerung reicht aus
'insufficient' - Erklaerung vorhanden, aber fuer niedrigere Sichtbarkeit
'legacy' - Altmedium ohne Erklaerung
'legacy' - Altmedium ohne Erklaerung (legacy_unreviewed)
'blocked' - durch Admin gesperrt
'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(
"SELECT rights_status, rights_declared_for_visibility FROM media_assets WHERE id = %s",
"SELECT rights_status FROM media_assets WHERE id = %s",
(asset_id,),
)
row = cur.fetchone()
if not row:
return "no_declaration"
# psycopg2 RealDictCursor oder ähnlich
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
rs = (row[0] if not hasattr(row, "keys") else row["rights_status"] or "").strip().lower()
if rs == "blocked":
return "blocked"
if rs == "legacy_unreviewed":
return "legacy"
if rs == "declared":
if rights_covers_target(rdv, target_visibility):
return "ok"
return "insufficient"
return "ok"
return "no_declaration"
@ -210,19 +203,6 @@ def assert_rights_for_promotion(cur: Any, asset_id: int, target_visibility: str)
"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":
raise HTTPException(
status_code=403,

View File

@ -215,28 +215,18 @@ class TestCheckRightsCoverage:
assert check_rights_coverage(cur, 1, "private") == "no_declaration"
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"
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"
def test_declared_private_covers_private(self):
cur = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "private"})
assert check_rights_coverage(cur, 1, "private") == "ok"
def test_declared_private_insufficient_for_club(self):
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"
def test_declared_covers_any_visibility(self):
# Eine bestehende Erklaerung gilt sichtbarkeitsunabhaengig
for target in ("private", "club", "official"):
cur = self._cur({"rights_status": "declared"})
assert check_rights_coverage(cur, 1, target) == "ok", f"failed for target={target}"
# ===========================================================================
@ -250,31 +240,25 @@ class TestAssertRightsForPromotion:
cur.fetchone.return_value = row
return cur
def test_ok_passes(self):
cur = self._cur({"rights_status": "declared", "rights_declared_for_visibility": "official"})
assert_rights_for_promotion(cur, 1, "official") # no raise
def test_ok_passes_for_any_target(self):
for target in ("private", "club", "official"):
cur = self._cur({"rights_status": "declared"})
assert_rights_for_promotion(cur, 1, target) # no raise
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:
assert_rights_for_promotion(cur, 1, "club")
assert exc.value.status_code == 400
assert exc.value.detail["code"] == "LEGACY_REDECLARATION_REQUIRED"
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:
assert_rights_for_promotion(cur, 1, "official")
assert exc.value.status_code == 403
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

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.80"
APP_VERSION = "0.8.81"
BUILD_DATE = "2026-05-11"
DB_SCHEMA_VERSION = "20260511048"
@ -31,6 +31,13 @@ MODULE_VERSIONS = {
}
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",
"date": "2026-05-11",

View File

@ -1,6 +1,6 @@
// 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 PAGE_VERSIONS = {