Medien: offizielle Assets nur Superadmin verwaltbar; Lesemodus; Upload-Verein vorausfüllen
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 28s
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 28s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f354bd9f77
commit
d3055f6f2f
|
|
@ -27,20 +27,26 @@ HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS",
|
||||||
def assert_can_manage_media_asset_lifecycle(cur: Any, tenant: Any, asset: dict) -> None:
|
def assert_can_manage_media_asset_lifecycle(cur: Any, tenant: Any, asset: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Papierkorb Stufe 2 / Recovery / Reaktivierung — nicht für trash_soft (siehe assert_can_trash_soft).
|
Papierkorb Stufe 2 / Recovery / Reaktivierung — nicht für trash_soft (siehe assert_can_trash_soft).
|
||||||
§5.2: official nur Plattform-Admin; club Vereinsorga; privat nur Uploader.
|
§5.2: official nur Superadmin; club Vereinsorga; privat nur Uploader (Plattform-Admin sonst wie bisher).
|
||||||
"""
|
"""
|
||||||
profile_id = tenant.profile_id
|
profile_id = tenant.profile_id
|
||||||
role = (tenant.global_role or "").strip().lower()
|
role = (tenant.global_role or "").strip().lower()
|
||||||
|
vis = (asset.get("visibility") or "private").strip().lower()
|
||||||
|
if vis == "official":
|
||||||
|
if not is_superadmin(role):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Offizielle Medien dürfen nur von Superadmins geändert oder gelöscht werden",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if is_platform_admin(role):
|
if is_platform_admin(role):
|
||||||
return
|
return
|
||||||
vis = (asset.get("visibility") or "private").strip().lower()
|
|
||||||
uid = asset.get("uploaded_by_profile_id")
|
uid = asset.get("uploaded_by_profile_id")
|
||||||
if vis == "private":
|
if vis == "private":
|
||||||
if uid is not None and int(uid) == int(profile_id):
|
if uid is not None and int(uid) == int(profile_id):
|
||||||
return
|
return
|
||||||
raise HTTPException(status_code=403, detail="Keine Berechtigung für dieses Medium")
|
raise HTTPException(status_code=403, detail="Keine Berechtigung für dieses Medium")
|
||||||
if vis == "official":
|
|
||||||
raise HTTPException(status_code=403, detail="Nur Plattform-Admin")
|
|
||||||
if vis == "club":
|
if vis == "club":
|
||||||
cid = asset.get("club_id")
|
cid = asset.get("club_id")
|
||||||
if cid is None:
|
if cid is None:
|
||||||
|
|
@ -54,15 +60,20 @@ def assert_can_manage_media_asset_lifecycle(cur: Any, tenant: Any, asset: dict)
|
||||||
def assert_can_trash_soft(cur: Any, tenant: Any, asset: dict) -> None:
|
def assert_can_trash_soft(cur: Any, tenant: Any, asset: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Aktiv → Papierkorb (Stufe 1). Trainer: nur eigene private Uploads.
|
Aktiv → Papierkorb (Stufe 1). Trainer: nur eigene private Uploads.
|
||||||
Vereinsmedien: Vereinsorga; official: Plattform-Admin; Superadmin: immer.
|
Vereinsmedien: Vereinsorga; official: nur Superadmin; Plattform-Admin: sonst wie Verein/privat.
|
||||||
"""
|
"""
|
||||||
role_raw = tenant.global_role
|
role_raw = tenant.global_role
|
||||||
role = (role_raw or "").strip().lower()
|
role = (role_raw or "").strip().lower()
|
||||||
if is_superadmin(role):
|
if is_superadmin(role):
|
||||||
return
|
return
|
||||||
|
vis = (asset.get("visibility") or "private").strip().lower()
|
||||||
|
if vis == "official":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Offizielle Medien dürfen nur von Superadmins in den Papierkorb gelegt werden",
|
||||||
|
)
|
||||||
if is_platform_admin(role):
|
if is_platform_admin(role):
|
||||||
return
|
return
|
||||||
vis = (asset.get("visibility") or "private").strip().lower()
|
|
||||||
uid = asset.get("uploaded_by_profile_id")
|
uid = asset.get("uploaded_by_profile_id")
|
||||||
cid = asset.get("club_id")
|
cid = asset.get("club_id")
|
||||||
pid = int(tenant.profile_id)
|
pid = int(tenant.profile_id)
|
||||||
|
|
@ -80,8 +91,6 @@ def assert_can_trash_soft(cur: Any, tenant: Any, asset: dict) -> None:
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Nur Vereinsorganisation darf Vereinsmedien in den Papierkorb legen",
|
detail="Nur Vereinsorganisation darf Vereinsmedien in den Papierkorb legen",
|
||||||
)
|
)
|
||||||
if vis == "official":
|
|
||||||
raise HTTPException(status_code=403, detail="Nur Plattform-Admin")
|
|
||||||
raise HTTPException(status_code=403, detail="Keine Berechtigung für dieses Medium")
|
raise HTTPException(status_code=403, detail="Keine Berechtigung für dieses Medium")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -427,25 +427,25 @@ def _item_permissions(row: dict, tenant: TenantContext, admin_club_ids: set[int]
|
||||||
is_owner = uid is not None and int(uid) == pid
|
is_owner = uid is not None and int(uid) == pid
|
||||||
club_mgr = plat or (cid is not None and int(cid) in admin_club_ids)
|
club_mgr = plat or (cid is not None and int(cid) in admin_club_ids)
|
||||||
|
|
||||||
edit_metadata = (
|
if vis == "official":
|
||||||
sup
|
edit_metadata = sup
|
||||||
or (vis == "official" and plat)
|
trash_soft = lc == "active" and sup
|
||||||
or (vis == "club" and club_mgr)
|
can_manage_adv = sup
|
||||||
or (vis == "private" and is_owner)
|
else:
|
||||||
)
|
edit_metadata = (
|
||||||
|
sup
|
||||||
trash_soft = lc == "active" and (
|
or (vis == "club" and club_mgr)
|
||||||
sup or plat or (vis == "private" and is_owner) or (vis == "club" and club_mgr)
|
or (vis == "private" and is_owner)
|
||||||
)
|
)
|
||||||
if vis == "official" and not (sup or plat):
|
trash_soft = lc == "active" and (
|
||||||
trash_soft = False
|
sup or plat or (vis == "private" and is_owner) or (vis == "club" and club_mgr)
|
||||||
|
)
|
||||||
can_manage_adv = (
|
can_manage_adv = (
|
||||||
sup
|
sup
|
||||||
or plat
|
or plat
|
||||||
or (vis == "private" and is_owner)
|
or (vis == "private" and is_owner)
|
||||||
or (vis == "club" and club_mgr)
|
or (vis == "club" and club_mgr)
|
||||||
)
|
)
|
||||||
|
|
||||||
trash_hidden = lc in ("active", "trash_soft") and can_manage_adv
|
trash_hidden = lc in ("active", "trash_soft") and can_manage_adv
|
||||||
recover_from_hidden = lc == "trash_hidden" and can_manage_adv
|
recover_from_hidden = lc == "trash_hidden" and can_manage_adv
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.57"
|
APP_VERSION = "0.8.58"
|
||||||
BUILD_DATE = "2026-05-07"
|
BUILD_DATE = "2026-05-07"
|
||||||
DB_SCHEMA_VERSION = "20260508049"
|
DB_SCHEMA_VERSION = "20260508049"
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ MODULE_VERSIONS = {
|
||||||
"club_join_requests": "1.0.1", # Depends(get_tenant_context)
|
"club_join_requests": "1.0.1", # Depends(get_tenant_context)
|
||||||
"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_assets": "1.12.0", # Privat: Plattform-Admin muss club_id; private Ablage wie Verein (.u{pid} im Dateinamen)
|
"media_assets": "1.12.1", # official: nur Superadmin Lifecycle/PATCH; UI Lesemodus; Superadmin Upload-Verein = aktiv
|
||||||
"groups": "0.1.0",
|
"groups": "0.1.0",
|
||||||
"skills": "0.1.0",
|
"skills": "0.1.0",
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
|
|
@ -29,6 +29,13 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.58",
|
||||||
|
"date": "2026-05-07",
|
||||||
|
"changes": [
|
||||||
|
"Medienbibliothek offiziell: Ändern/Lifecycle nur Superadmin (nicht Plattform-Admin); Bearbeiten-Dialog für andere nur Lesemodus; Superadmin: Vereinsauswahl beim Archiv-Upload folgt aktiv/gesetzt effective_club_id",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.57",
|
"version": "0.8.57",
|
||||||
"date": "2026-05-07",
|
"date": "2026-05-07",
|
||||||
|
|
|
||||||
|
|
@ -240,18 +240,6 @@ export default function MediaLibraryPage() {
|
||||||
[isSuperadmin],
|
[isSuperadmin],
|
||||||
)
|
)
|
||||||
|
|
||||||
const modalVisibilityOptions = useMemo(() => {
|
|
||||||
if (!modalDraft) return archiveVisOptions
|
|
||||||
const o = [...archiveVisOptions]
|
|
||||||
if (!o.some((x) => x.value === modalDraft.visibility)) {
|
|
||||||
o.push({
|
|
||||||
value: modalDraft.visibility,
|
|
||||||
label: visibilityUiLabel(modalDraft.visibility),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}, [archiveVisOptions, modalDraft])
|
|
||||||
|
|
||||||
const [lifecycle, setLifecycle] = useState('active')
|
const [lifecycle, setLifecycle] = useState('active')
|
||||||
const [q, setQ] = useState('')
|
const [q, setQ] = useState('')
|
||||||
const [items, setItems] = useState([])
|
const [items, setItems] = useState([])
|
||||||
|
|
@ -282,6 +270,25 @@ export default function MediaLibraryPage() {
|
||||||
const mediaListFetchSeqRef = useRef(0)
|
const mediaListFetchSeqRef = useRef(0)
|
||||||
const gridTopAnchorRef = useRef(null)
|
const gridTopAnchorRef = useRef(null)
|
||||||
|
|
||||||
|
const modalVisibilityOptions = useMemo(() => {
|
||||||
|
if (!modalDraft) return archiveVisOptions
|
||||||
|
const o = [...archiveVisOptions]
|
||||||
|
if (!o.some((x) => x.value === modalDraft.visibility)) {
|
||||||
|
o.push({
|
||||||
|
value: modalDraft.visibility,
|
||||||
|
label: visibilityUiLabel(modalDraft.visibility),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}, [archiveVisOptions, modalDraft])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSuperadmin) return
|
||||||
|
const cid = user?.effective_club_id ?? user?.active_club_id
|
||||||
|
if (cid == null || cid === '') return
|
||||||
|
setUploadClubId(String(cid))
|
||||||
|
}, [isSuperadmin, user?.effective_club_id, user?.active_club_id])
|
||||||
|
|
||||||
const loadClubs = useCallback(async () => {
|
const loadClubs = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const c = await api.listClubs()
|
const c = await api.listClubs()
|
||||||
|
|
@ -1049,13 +1056,20 @@ export default function MediaLibraryPage() {
|
||||||
<div className="media-library__overlay" role="dialog" aria-modal="true" aria-labelledby="edit-media-title">
|
<div className="media-library__overlay" role="dialog" aria-modal="true" aria-labelledby="edit-media-title">
|
||||||
<div className="media-library__modal">
|
<div className="media-library__modal">
|
||||||
<div className="media-library__modal-head">
|
<div className="media-library__modal-head">
|
||||||
<h2 id="edit-media-title">Medium #{modal.id}</h2>
|
<h2 id="edit-media-title">
|
||||||
|
Medium #{modal.id}
|
||||||
|
{(modal.visibility || '').toLowerCase() === 'official' && !isSuperadmin
|
||||||
|
? ' · Nur Lesen'
|
||||||
|
: ''}
|
||||||
|
</h2>
|
||||||
<button type="button" className="media-library__icon-btn" onClick={closeModal} aria-label="Schließen">
|
<button type="button" className="media-library__icon-btn" onClick={closeModal} aria-label="Schließen">
|
||||||
<X size={22} />
|
<X size={22} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-library__modal-body">
|
<div className="media-library__modal-body">
|
||||||
{(viewer?.show_club_meta || viewer?.show_uploader_meta) && (
|
{(viewer?.show_club_meta ||
|
||||||
|
viewer?.show_uploader_meta ||
|
||||||
|
(modal.visibility || '').toLowerCase() === 'official') && (
|
||||||
<div className="media-library__meta-block">
|
<div className="media-library__meta-block">
|
||||||
{viewer?.show_club_meta ? (
|
{viewer?.show_club_meta ? (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1102,6 +1116,18 @@ export default function MediaLibraryPage() {
|
||||||
placeholder="z. B. Technik, Wurf"
|
placeholder="z. B. Technik, Wurf"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
) : (modal.visibility || '').toLowerCase() === 'official' ? (
|
||||||
|
<>
|
||||||
|
<p className="media-library__hint">
|
||||||
|
Offizielle Medien sind für alle sichtbar. Bearbeiten, Sichtbarkeit und Löschung nur als Superadmin.
|
||||||
|
</p>
|
||||||
|
<label className="form-label">Bezeichnung</label>
|
||||||
|
<input className="form-input" readOnly value={modalDraft.display_name} />
|
||||||
|
<label className="form-label">Copyright</label>
|
||||||
|
<textarea className="form-input" readOnly rows={3} value={modalDraft.copyright_notice} />
|
||||||
|
<label className="form-label">Schlagwörter</label>
|
||||||
|
<input className="form-input" readOnly value={modalDraft.tags_input} />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="media-library__hint">Keine Berechtigung für Metadaten — nur Verwaltende dieser Stufe.</p>
|
<p className="media-library__hint">Keine Berechtigung für Metadaten — nur Verwaltende dieser Stufe.</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1155,9 +1181,11 @@ export default function MediaLibraryPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="media-library__modal-actions">
|
<div className="media-library__modal-actions">
|
||||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={saveModal}>
|
{modal.permissions?.edit_metadata ? (
|
||||||
Speichern
|
<button type="button" className="btn btn-secondary" disabled={busy} onClick={saveModal}>
|
||||||
</button>
|
Speichern
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="media-library__lc-block">
|
<div className="media-library__lc-block">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user