From aea52d1563ca624f5e2b65bf587082eeb986be52 Mon Sep 17 00:00:00 2001 From: Flo Hartmann Date: Mon, 24 Aug 2026 22:44:18 +0000 Subject: [PATCH] Neuheiten+Planung: Pagination (50/Seite, Infinite Scroll) + quellen_url in Planung mit Quellen-Link --- plugins/neuheiten/__init__.py | 10 +++++- .../neuheiten/templates/neuheiten/_liste.html | 7 ++++ plugins/planung/__init__.py | 36 +++++++++++++++++-- plugins/planung/models.py | 1 + plugins/planung/templates/planung/index.html | 15 ++++++++ tests/test_planung.py | 6 ++-- 6 files changed, 69 insertions(+), 6 deletions(-) diff --git a/plugins/neuheiten/__init__.py b/plugins/neuheiten/__init__.py index 297b28a..2976e0c 100644 --- a/plugins/neuheiten/__init__.py +++ b/plugins/neuheiten/__init__.py @@ -98,10 +98,12 @@ class NeuheitenPlugin(BasePlugin): richtung: str = STANDARD_SORTIERUNG[1], meldung: str = "", fehler: str = "", + seite_nr: int = 1, ): spalte = SORTIERBAR.get(sort, SORTIERBAR[STANDARD_SORTIERUNG[0]]) abwaerts = richtung == "ab" if sort in SORTIERBAR else False spalte_sortiert = spalte.desc() if abwaerts else spalte.asc() + pro_seite = 50 with self.context.session_factory() as db: abfrage = select(Neuheit) @@ -116,8 +118,11 @@ class NeuheitenPlugin(BasePlugin): ) if status.strip(): abfrage = abfrage.where(Neuheit.status == status.strip()) + gesamt = db.scalar(select(func.count()).select_from(abfrage.subquery())) or 0 eintraege = db.scalars( abfrage.order_by(spalte_sortiert, Neuheit.id.asc()) + .offset((max(seite_nr, 1) - 1) * pro_seite) + .limit(pro_seite) ).all() status_optionen = [ zeile for zeile in db.scalars( @@ -126,6 +131,7 @@ class NeuheitenPlugin(BasePlugin): ] quellen_zeilen = self._quellen_uebersicht(db) + hat_weitere = max(seite_nr, 1) * pro_seite < gesamt kontext = { "user": user, "titel": self.title, @@ -143,7 +149,9 @@ class NeuheitenPlugin(BasePlugin): ), "suchbegriffe": ", ".join(self._suchbegriffe) or "—", "sync_aktiv": self._scheduler is not None and self._scheduler.running, - "anzahl": len(eintraege), + "anzahl": gesamt, + "hat_weitere": hat_weitere, + "naechste_seite": max(seite_nr, 1) + 1, "quellen_zeilen": quellen_zeilen, "quellen_sync_aktiv": ( os.environ.get("SPIELE_NEUHEITEN_QUELLEN_SYNC_AKTIV", "1").strip() diff --git a/plugins/neuheiten/templates/neuheiten/_liste.html b/plugins/neuheiten/templates/neuheiten/_liste.html index 29d8dac..3156407 100644 --- a/plugins/neuheiten/templates/neuheiten/_liste.html +++ b/plugins/neuheiten/templates/neuheiten/_liste.html @@ -119,3 +119,10 @@ +{% if hat_weitere %} +
+
Lade weitere Einträge …
+
+{% endif %} diff --git a/plugins/planung/__init__.py b/plugins/planung/__init__.py index 986769b..1c376e6 100644 --- a/plugins/planung/__init__.py +++ b/plugins/planung/__init__.py @@ -28,7 +28,7 @@ from urllib.parse import quote from fastapi import Depends, Form, Request from fastapi.responses import RedirectResponse -from sqlalchemy import select, update +from sqlalchemy import func, select, update from sqlalchemy.orm import Session from redaktionskern.auth.deps import AccessDenied, get_db, require_user @@ -106,9 +106,29 @@ class PlanungPlugin(BasePlugin): "ALTER TABLE planungsliste ADD COLUMN bild_url VARCHAR(500)" ) + def quellen_url_spalte(conn) -> None: + """Migration 0003: Spalte quellen_url (Link zur Herkunftsquelle).""" + if conn.dialect.name == "sqlite": + info = conn.exec_driver_sql( + "PRAGMA table_info(planungsliste)" + ).fetchall() + if any(zeile[1] == "quellen_url" for zeile in info): + return + else: + existiert = conn.exec_driver_sql( + "SELECT column_name FROM information_schema.columns " + "WHERE table_name = 'planungsliste' AND column_name = 'quellen_url'" + ).scalar() + if existiert is not None: + return + conn.exec_driver_sql( + "ALTER TABLE planungsliste ADD COLUMN quellen_url VARCHAR(500)" + ) + return [ Migration(version="0001_planungsliste", up=_planungs_tabelle), Migration(version="0002_bild_url", up=bild_url_spalte), + Migration(version="0003_quellen_url", up=quellen_url_spalte), ] def navigation(self) -> list[NavEntry]: @@ -220,15 +240,22 @@ class PlanungPlugin(BasePlugin): status: str = "", meldung: str = "", fehler: str = "", + seite_nr: int = 1, ): - """Planungsliste: alle Einträge mit Zuordnung und Statuswechsel.""" + """Planungsliste: Einträge paginiert (50/Seite) mit Zuordnung.""" + pro_seite = 50 abfrage = select(Planungseintrag) if status in GUELTIGE_STATUS: abfrage = abfrage.where(Planungseintrag.status == status) + gesamt = db.scalar( + select(func.count()).select_from(abfrage.subquery()) + ) or 0 eintraege = db.scalars( abfrage.order_by( Planungseintrag.aktualisiert_am.desc(), Planungseintrag.id.desc() ) + .offset((max(seite_nr, 1) - 1) * pro_seite) + .limit(pro_seite) ).all() namen = { benutzer.id: (benutzer.display_name or benutzer.username) @@ -249,7 +276,9 @@ class PlanungPlugin(BasePlugin): "ist_redaktion": user.role in REDAKTION, "meldung": meldung[:300], "fehler": fehler[:300], - "anzahl": len(eintraege), + "anzahl": gesamt, + "hat_weitere": max(seite_nr, 1) * pro_seite < gesamt, + "naechste_seite": max(seite_nr, 1) + 1, }, ) @@ -392,6 +421,7 @@ class PlanungPlugin(BasePlugin): autor=zeile.autor, bgg_id=zeile.bgg_id, bild_url=zeile.bild_url, + quellen_url=zeile.quellen_url, ausgabe="", rezensent_id=user.id, status="offen", diff --git a/plugins/planung/models.py b/plugins/planung/models.py index 4051aaa..f4fb931 100644 --- a/plugins/planung/models.py +++ b/plugins/planung/models.py @@ -50,6 +50,7 @@ class Planungseintrag(Base): bgg_id: Mapped[int | None] = mapped_column(Integer) #: URL des Coverbildes (aus der Neuheitenliste übernommen oder leer). bild_url: Mapped[str | None] = mapped_column(String(500)) + quellen_url: Mapped[str | None] = mapped_column(String(500)) #: Magazin-Ausgabe, z. B. „3/2025“ (mehrere Ausgaben parallel möglich). ausgabe: Mapped[str] = mapped_column(String(100), default="") rezensent_id: Mapped[int] = mapped_column(ForeignKey("users.id")) diff --git a/plugins/planung/templates/planung/index.html b/plugins/planung/templates/planung/index.html index 69c8637..4466106 100644 --- a/plugins/planung/templates/planung/index.html +++ b/plugins/planung/templates/planung/index.html @@ -76,6 +76,10 @@ {% if eintrag.bgg_id %} BGG {{ eintrag.bgg_id }} + {% elif eintrag.quellen_url %} + {{ eintrag.quelle }} ↗ {% endif %} {% if eintrag.pruefung %} BGG {{ eintrag.bgg_id }} + {% elif eintrag.quellen_url %} + {{ eintrag.quelle }} ↗ {% endif %} {% if eintrag.pruefung %} +{% if hat_weitere %} +
+
Lade weitere Einträge …
+
+{% endif %} {% endblock %} diff --git a/tests/test_planung.py b/tests/test_planung.py index 7b94f19..e47b64a 100644 --- a/tests/test_planung.py +++ b/tests/test_planung.py @@ -174,8 +174,10 @@ def test_migration_legt_planungsliste_an(app): text("SELECT version FROM schema_migrations WHERE plugin='planung'") ).fetchall() assert "planungsliste" in tabellen - # 0002_bild_url: Coverbild-Spalte (seit der Cover-Erweiterung). - assert [z[0] for z in protokoll] == ["0001_planungsliste", "0002_bild_url"] + # 0002_bild_url + 0003_quellen_url: Coverbild- und Quellen-Link-Spalten. + assert [z[0] for z in protokoll] == [ + "0001_planungsliste", "0002_bild_url", "0003_quellen_url", + ] def test_anonym_wird_zum_login_umgeleitet(client):