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
102
tests/test_forum_pinning.py
Normal file
102
tests/test_forum_pinning.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Forum-Anpinnen: Berechtigung (Admin/Moderator) + Scope-Sortierung.
|
||||
|
||||
Deckt zwei neue Verhalten ab:
|
||||
1. Admins OHNE is_moderator-Flag dürfen anpinnen (Rolle zählt, nicht nur Flag).
|
||||
2. pin_scope='global' hält oben in JEDER Ansicht, pin_scope='kategorie' nur in
|
||||
der gefilterten Kategorie (nicht in der "Alle"-Liste).
|
||||
|
||||
Hinweise zu den Fixtures/Route-Constraints:
|
||||
- `admin`-Fixture setzt rolle='admin' direkt in der DB, OHNE is_moderator.
|
||||
- forum.create_thread hat 30s-Cooldown + 5-Threads/Stunde — daher gestaffelte
|
||||
client_time. Texte sind eindeutig (token_hex) gegen den Duplikat-Check.
|
||||
- Kategorie 'tauschboerse' isoliert die Sortier-Tests von anderen Suiten;
|
||||
Assertions vergleichen nur die RELATIVE Reihenfolge der eigenen Thread-IDs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
|
||||
def _mk_thread(client, headers, kategorie, titel, client_time):
|
||||
r = client.post(
|
||||
"/api/forum/threads",
|
||||
headers=headers,
|
||||
json={
|
||||
"kategorie": kategorie,
|
||||
"titel": titel,
|
||||
"text": f"{titel}: Hallo zusammen, ein Testbeitrag {secrets.token_hex(8)}.",
|
||||
"client_time": client_time,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 201, f"create_thread failed: {r.status_code} {r.text}"
|
||||
return r.json()
|
||||
|
||||
|
||||
class TestForumPinPermission:
|
||||
def test_admin_without_moderator_flag_can_pin(self, client, admin):
|
||||
"""rolle='admin' ohne is_moderator darf anpinnen (Kernursache-Fix)."""
|
||||
t = _mk_thread(client, admin["headers"], "tauschboerse", "Admin Pin", "2026-06-18T08:00:00")
|
||||
r = client.patch(
|
||||
f"/api/forum/threads/{t['id']}",
|
||||
headers=admin["headers"],
|
||||
json={"is_pinned": 1, "pin_scope": "global"},
|
||||
)
|
||||
assert r.status_code == 200, f"{r.status_code} {r.text}"
|
||||
body = r.json()
|
||||
assert body["is_pinned"] == 1
|
||||
assert body["pin_scope"] == "global"
|
||||
|
||||
def test_normal_user_cannot_pin(self, client, user):
|
||||
"""Normaler User → 403, Anpinnen bleibt Admin/Mod vorbehalten."""
|
||||
t = _mk_thread(client, user["headers"], "tauschboerse", "User Pin", "2026-06-18T08:00:00")
|
||||
r = client.patch(
|
||||
f"/api/forum/threads/{t['id']}",
|
||||
headers=user["headers"],
|
||||
json={"is_pinned": 1, "pin_scope": "global"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
def test_invalid_pin_scope_rejected(self, client, admin):
|
||||
"""Ungültiger pin_scope → 400."""
|
||||
t = _mk_thread(client, admin["headers"], "tauschboerse", "Bad Scope", "2026-06-18T08:00:00")
|
||||
r = client.patch(
|
||||
f"/api/forum/threads/{t['id']}",
|
||||
headers=admin["headers"],
|
||||
json={"is_pinned": 1, "pin_scope": "bogus"},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
class TestForumPinScopeSorting:
|
||||
def test_scope_controls_where_thread_floats(self, client, admin):
|
||||
h = admin["headers"]
|
||||
t1 = _mk_thread(client, h, "tauschboerse", "Erster", "2026-06-18T09:00:00")
|
||||
t2 = _mk_thread(client, h, "tauschboerse", "Zweiter", "2026-06-18T09:20:00")
|
||||
t3 = _mk_thread(client, h, "tauschboerse", "Dritter", "2026-06-18T09:40:00")
|
||||
ids = {t1["id"], t2["id"], t3["id"]}
|
||||
|
||||
def order(kategorie=None):
|
||||
url = "/api/forum/threads?limit=200"
|
||||
if kategorie:
|
||||
url += f"&kategorie={kategorie}"
|
||||
r = client.get(url, headers=h)
|
||||
assert r.status_code == 200
|
||||
return [x["id"] for x in r.json() if x["id"] in ids]
|
||||
|
||||
# Ohne Pin: neueste zuerst.
|
||||
assert order() == [t3["id"], t2["id"], t1["id"]]
|
||||
|
||||
# Ältesten Thread GLOBAL anpinnen → oben in "Alle" UND in der Kategorie.
|
||||
r = client.patch(f"/api/forum/threads/{t1['id']}", headers=h,
|
||||
json={"is_pinned": 1, "pin_scope": "global"})
|
||||
assert r.status_code == 200
|
||||
assert order()[0] == t1["id"]
|
||||
assert order("tauschboerse")[0] == t1["id"]
|
||||
|
||||
# Auf Themen-Pin umstellen → NUR in der Kategorie oben, in "Alle" wieder nach Datum.
|
||||
r = client.patch(f"/api/forum/threads/{t1['id']}", headers=h,
|
||||
json={"pin_scope": "kategorie"})
|
||||
assert r.status_code == 200
|
||||
assert order() == [t3["id"], t2["id"], t1["id"]] # "Alle": Themen-Pin zählt nicht
|
||||
assert order("tauschboerse")[0] == t1["id"] # Kategorie: oben
|
||||
Loading…
Add table
Add a link
Reference in a new issue