Neuheiten+Planung: Pagination (50/Seite, Infinite Scroll) + quellen_url in Planung mit Quellen-Link
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -119,3 +119,10 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% if hat_weitere %}
|
||||
<div id="lade-mehr"
|
||||
hx-get="/neuheiten?seite_nr={{ naechste_seite }}&q={{ q }}&status={{ status_filter }}&sort={{ sort }}&richtung={{ richtung }}"
|
||||
hx-trigger="revealed delay:150ms" hx-swap="outerHTML">
|
||||
<div class="py-6 text-center text-sm text-slate-400">Lade weitere Einträge …</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -76,6 +76,10 @@
|
||||
{% if eintrag.bgg_id %}
|
||||
<a href="https://boardgamegeek.com/boardgame/{{ eintrag.bgg_id }}" target="_blank" rel="noopener"
|
||||
class="ml-0.5 text-[10px] text-slate-400 hover:text-emerald-700 align-super">BGG {{ eintrag.bgg_id }}</a>
|
||||
{% elif eintrag.quellen_url %}
|
||||
<a href="{{ eintrag.quellen_url }}" target="_blank" rel="noopener"
|
||||
title="Zur Quelle ({{ eintrag.quelle }})"
|
||||
class="ml-0.5 text-[10px] uppercase tracking-wide text-sky-600/70 hover:text-sky-800 align-super">{{ eintrag.quelle }} ↗</a>
|
||||
{% endif %}
|
||||
{% if eintrag.pruefung %}
|
||||
<span class="ml-0.5 inline-block px-2 py-0.5 rounded-full bg-amber-100 text-amber-800 text-xs align-middle"
|
||||
@@ -159,6 +163,10 @@
|
||||
{% if eintrag.bgg_id %}
|
||||
<a href="https://boardgamegeek.com/boardgame/{{ eintrag.bgg_id }}" target="_blank" rel="noopener"
|
||||
class="ml-1 text-[10px] text-slate-400 hover:text-emerald-700 align-super">BGG {{ eintrag.bgg_id }}</a>
|
||||
{% elif eintrag.quellen_url %}
|
||||
<a href="{{ eintrag.quellen_url }}" target="_blank" rel="noopener"
|
||||
title="Zur Quelle ({{ eintrag.quelle }})"
|
||||
class="ml-1 text-[10px] uppercase tracking-wide text-sky-600/70 hover:text-sky-800 align-super">{{ eintrag.quelle }} ↗</a>
|
||||
{% endif %}
|
||||
{% if eintrag.pruefung %}
|
||||
<span class="ml-1 inline-block px-2 py-0.5 rounded-full bg-amber-100 text-amber-800 text-xs"
|
||||
@@ -216,4 +224,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if hat_weitere %}
|
||||
<div id="planung-lade-mehr"
|
||||
hx-get="/planung?seite_nr={{ naechste_seite }}&status={{ status_filter }}"
|
||||
hx-trigger="revealed delay:150ms" hx-swap="outerHTML">
|
||||
<div class="py-6 text-center text-sm text-slate-400">Lade weitere Einträge …</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user