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):