Forum: Anpinnen pro Thema/global + Admin-Berechtigung (v1303)
- Anpinnen-Scope: pin_scope ('global' | 'kategorie'). Global haelt oben in
jeder Ansicht; Themen-Pin nur in der gefilterten Kategorie (nicht in 'Alle').
- Bugfix Berechtigung: Forum pruefte nur is_moderator -> Admins ohne das Flag
wurden ausgesperrt. Neuer Helper _can_moderate() = rolle in (admin,moderator)
ODER is_moderator, an allen 7 Forum-Checks + beiden Frontend-isMod-Gates.
- Thread-Detail-Toolbar (nur Admin/Mod): 'Global anpinnen' / 'Im Thema anpinnen'
/ 'Loesen' + Status- und Badge-Anzeige nach Scope.
- DB-Migration forum_threads.pin_scope (idempotent, Default 'global').
- Tests: tests/test_forum_pinning.py (Berechtigung + Scope-Sortierung).
This commit is contained in:
parent
901df5468c
commit
ac0814e687
9 changed files with 175 additions and 36 deletions
|
|
@ -42,6 +42,7 @@ class PostCreate(BaseModel):
|
|||
class ThreadPatch(BaseModel):
|
||||
is_pinned: Optional[int] = None
|
||||
is_locked: Optional[int] = None
|
||||
pin_scope: Optional[str] = None # 'global' (überall oben) | 'kategorie' (nur im Thema oben)
|
||||
|
||||
class ThreadUpdate(BaseModel):
|
||||
titel: Optional[str] = Field(None, max_length=200)
|
||||
|
|
@ -71,6 +72,15 @@ class ResolveReport(BaseModel):
|
|||
resolved: int = 1
|
||||
|
||||
|
||||
def _can_moderate(user) -> bool:
|
||||
"""Admin ODER Moderator dürfen moderieren (pin/lock/löschen).
|
||||
Wichtig: Admins haben nicht zwingend das is_moderator-Flag gesetzt —
|
||||
daher zusätzlich die Rolle prüfen (analog auth.require_moderator)."""
|
||||
if not user:
|
||||
return False
|
||||
return user.get('rolle') in ('admin', 'moderator') or bool(user.get('is_moderator'))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -126,12 +136,13 @@ async def list_threads(
|
|||
user=Depends(get_current_user_optional),
|
||||
):
|
||||
uid = user['id'] if user else None
|
||||
has_cat = bool(kategorie and kategorie != 'alle')
|
||||
with db() as conn:
|
||||
q = """
|
||||
SELECT t.id, t.kategorie, t.titel,
|
||||
SUBSTR(t.text, 1, 120) AS text_preview,
|
||||
t.antworten, t.likes, t.views,
|
||||
t.is_pinned, t.is_locked, t.foto_urls,
|
||||
t.is_pinned, t.pin_scope, t.is_locked, t.foto_urls,
|
||||
t.created_at, t.user_id,
|
||||
u.name AS autor_name, u.founder_number AS autor_founder_number
|
||||
FROM forum_threads t
|
||||
|
|
@ -139,13 +150,18 @@ async def list_threads(
|
|||
WHERE t.is_deleted = 0
|
||||
"""
|
||||
params = []
|
||||
if kategorie and kategorie != 'alle':
|
||||
if has_cat:
|
||||
q += " AND t.kategorie = ?"
|
||||
params.append(kategorie)
|
||||
if search:
|
||||
q += " AND (t.titel LIKE ? OR t.text LIKE ?)"
|
||||
params.extend([f'%{search}%', f'%{search}%'])
|
||||
q += " ORDER BY t.is_pinned DESC, t.created_at DESC LIMIT ? OFFSET ?"
|
||||
# Kategorie-Ansicht: globale UND Themen-Pins steigen nach oben.
|
||||
# "Alle"-Ansicht: nur globale Pins oben — Themen-Pins bleiben in ihrem Thema.
|
||||
if has_cat:
|
||||
q += " ORDER BY t.is_pinned DESC, t.created_at DESC LIMIT ? OFFSET ?"
|
||||
else:
|
||||
q += " ORDER BY (t.is_pinned = 1 AND t.pin_scope = 'global') DESC, t.created_at DESC LIMIT ? OFFSET ?"
|
||||
params.extend([limit, offset])
|
||||
rows = conn.execute(q, params).fetchall()
|
||||
|
||||
|
|
@ -323,7 +339,7 @@ async def delete_thread(thread_id: int, user=Depends(get_current_user)):
|
|||
).fetchone()
|
||||
if not thread:
|
||||
raise HTTPException(404, "Thread nicht gefunden.")
|
||||
if thread['user_id'] != user['id'] and not user.get('is_moderator'):
|
||||
if thread['user_id'] != user['id'] and not _can_moderate(user):
|
||||
raise HTTPException(403, "Keine Berechtigung.")
|
||||
conn.execute(
|
||||
"UPDATE forum_threads SET is_deleted = 1 WHERE id = ?", (thread_id,)
|
||||
|
|
@ -335,7 +351,7 @@ async def delete_thread(thread_id: int, user=Depends(get_current_user)):
|
|||
# ------------------------------------------------------------------
|
||||
@router.patch("/threads/{thread_id}")
|
||||
async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_current_user)):
|
||||
if not user.get('is_moderator'):
|
||||
if not _can_moderate(user):
|
||||
raise HTTPException(403, "Nur Moderatoren können Threads bearbeiten.")
|
||||
with db() as conn:
|
||||
thread = conn.execute(
|
||||
|
|
@ -345,6 +361,8 @@ async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_curre
|
|||
raise HTTPException(404, "Thread nicht gefunden.")
|
||||
|
||||
updates = data.model_dump(exclude_none=True)
|
||||
if 'pin_scope' in updates and updates['pin_scope'] not in ('global', 'kategorie'):
|
||||
raise HTTPException(400, "Ungültiger pin_scope (erlaubt: 'global', 'kategorie').")
|
||||
if updates:
|
||||
cols = ', '.join(f"{k} = ?" for k in updates)
|
||||
conn.execute(
|
||||
|
|
@ -476,7 +494,7 @@ async def delete_post(post_id: int, user=Depends(get_current_user)):
|
|||
).fetchone()
|
||||
if not post:
|
||||
raise HTTPException(404, "Beitrag nicht gefunden.")
|
||||
if post['user_id'] != user['id'] and not user.get('is_moderator'):
|
||||
if post['user_id'] != user['id'] and not _can_moderate(user):
|
||||
raise HTTPException(403, "Keine Berechtigung.")
|
||||
conn.execute(
|
||||
"UPDATE forum_posts SET is_deleted = 1 WHERE id = ?", (post_id,)
|
||||
|
|
@ -504,7 +522,7 @@ async def upload_thread_foto(
|
|||
).fetchone()
|
||||
if not thread:
|
||||
raise HTTPException(404, "Thread nicht gefunden.")
|
||||
if thread['user_id'] != user['id'] and not user.get('is_moderator'):
|
||||
if thread['user_id'] != user['id'] and not _can_moderate(user):
|
||||
raise HTTPException(403, "Keine Berechtigung.")
|
||||
|
||||
existing = _parse_foto_urls(thread['foto_urls'])
|
||||
|
|
@ -537,7 +555,7 @@ async def upload_post_foto(
|
|||
).fetchone()
|
||||
if not post:
|
||||
raise HTTPException(404, "Beitrag nicht gefunden.")
|
||||
if post['user_id'] != user['id'] and not user.get('is_moderator'):
|
||||
if post['user_id'] != user['id'] and not _can_moderate(user):
|
||||
raise HTTPException(403, "Keine Berechtigung.")
|
||||
|
||||
existing = _parse_foto_urls(post['foto_urls'])
|
||||
|
|
@ -642,7 +660,7 @@ async def report_content(data: ReportBody, user=Depends(get_current_user)):
|
|||
# ------------------------------------------------------------------
|
||||
@router.get("/reports")
|
||||
async def list_reports(user=Depends(get_current_user)):
|
||||
if not user.get('is_moderator'):
|
||||
if not _can_moderate(user):
|
||||
raise HTTPException(403, "Nur Moderatoren.")
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
|
|
@ -660,7 +678,7 @@ async def list_reports(user=Depends(get_current_user)):
|
|||
# ------------------------------------------------------------------
|
||||
@router.patch("/reports/{report_id}")
|
||||
async def resolve_report(report_id: int, data: ResolveReport, user=Depends(get_current_user)):
|
||||
if not user.get('is_moderator'):
|
||||
if not _can_moderate(user):
|
||||
raise HTTPException(403, "Nur Moderatoren.")
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue