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],
|
richtung: str = STANDARD_SORTIERUNG[1],
|
||||||
meldung: str = "",
|
meldung: str = "",
|
||||||
fehler: str = "",
|
fehler: str = "",
|
||||||
|
seite_nr: int = 1,
|
||||||
):
|
):
|
||||||
spalte = SORTIERBAR.get(sort, SORTIERBAR[STANDARD_SORTIERUNG[0]])
|
spalte = SORTIERBAR.get(sort, SORTIERBAR[STANDARD_SORTIERUNG[0]])
|
||||||
abwaerts = richtung == "ab" if sort in SORTIERBAR else False
|
abwaerts = richtung == "ab" if sort in SORTIERBAR else False
|
||||||
spalte_sortiert = spalte.desc() if abwaerts else spalte.asc()
|
spalte_sortiert = spalte.desc() if abwaerts else spalte.asc()
|
||||||
|
pro_seite = 50
|
||||||
|
|
||||||
with self.context.session_factory() as db:
|
with self.context.session_factory() as db:
|
||||||
abfrage = select(Neuheit)
|
abfrage = select(Neuheit)
|
||||||
@@ -116,8 +118,11 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
)
|
)
|
||||||
if status.strip():
|
if status.strip():
|
||||||
abfrage = abfrage.where(Neuheit.status == status.strip())
|
abfrage = abfrage.where(Neuheit.status == status.strip())
|
||||||
|
gesamt = db.scalar(select(func.count()).select_from(abfrage.subquery())) or 0
|
||||||
eintraege = db.scalars(
|
eintraege = db.scalars(
|
||||||
abfrage.order_by(spalte_sortiert, Neuheit.id.asc())
|
abfrage.order_by(spalte_sortiert, Neuheit.id.asc())
|
||||||
|
.offset((max(seite_nr, 1) - 1) * pro_seite)
|
||||||
|
.limit(pro_seite)
|
||||||
).all()
|
).all()
|
||||||
status_optionen = [
|
status_optionen = [
|
||||||
zeile for zeile in db.scalars(
|
zeile for zeile in db.scalars(
|
||||||
@@ -126,6 +131,7 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
quellen_zeilen = self._quellen_uebersicht(db)
|
quellen_zeilen = self._quellen_uebersicht(db)
|
||||||
|
hat_weitere = max(seite_nr, 1) * pro_seite < gesamt
|
||||||
kontext = {
|
kontext = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"titel": self.title,
|
"titel": self.title,
|
||||||
@@ -143,7 +149,9 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
),
|
),
|
||||||
"suchbegriffe": ", ".join(self._suchbegriffe) or "—",
|
"suchbegriffe": ", ".join(self._suchbegriffe) or "—",
|
||||||
"sync_aktiv": self._scheduler is not None and self._scheduler.running,
|
"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_zeilen": quellen_zeilen,
|
||||||
"quellen_sync_aktiv": (
|
"quellen_sync_aktiv": (
|
||||||
os.environ.get("SPIELE_NEUHEITEN_QUELLEN_SYNC_AKTIV", "1").strip()
|
os.environ.get("SPIELE_NEUHEITEN_QUELLEN_SYNC_AKTIV", "1").strip()
|
||||||
|
|||||||
@@ -119,3 +119,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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 import Depends, Form, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from sqlalchemy import select, update
|
from sqlalchemy import func, select, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from redaktionskern.auth.deps import AccessDenied, get_db, require_user
|
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)"
|
"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 [
|
return [
|
||||||
Migration(version="0001_planungsliste", up=_planungs_tabelle),
|
Migration(version="0001_planungsliste", up=_planungs_tabelle),
|
||||||
Migration(version="0002_bild_url", up=bild_url_spalte),
|
Migration(version="0002_bild_url", up=bild_url_spalte),
|
||||||
|
Migration(version="0003_quellen_url", up=quellen_url_spalte),
|
||||||
]
|
]
|
||||||
|
|
||||||
def navigation(self) -> list[NavEntry]:
|
def navigation(self) -> list[NavEntry]:
|
||||||
@@ -220,15 +240,22 @@ class PlanungPlugin(BasePlugin):
|
|||||||
status: str = "",
|
status: str = "",
|
||||||
meldung: str = "",
|
meldung: str = "",
|
||||||
fehler: 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)
|
abfrage = select(Planungseintrag)
|
||||||
if status in GUELTIGE_STATUS:
|
if status in GUELTIGE_STATUS:
|
||||||
abfrage = abfrage.where(Planungseintrag.status == status)
|
abfrage = abfrage.where(Planungseintrag.status == status)
|
||||||
|
gesamt = db.scalar(
|
||||||
|
select(func.count()).select_from(abfrage.subquery())
|
||||||
|
) or 0
|
||||||
eintraege = db.scalars(
|
eintraege = db.scalars(
|
||||||
abfrage.order_by(
|
abfrage.order_by(
|
||||||
Planungseintrag.aktualisiert_am.desc(), Planungseintrag.id.desc()
|
Planungseintrag.aktualisiert_am.desc(), Planungseintrag.id.desc()
|
||||||
)
|
)
|
||||||
|
.offset((max(seite_nr, 1) - 1) * pro_seite)
|
||||||
|
.limit(pro_seite)
|
||||||
).all()
|
).all()
|
||||||
namen = {
|
namen = {
|
||||||
benutzer.id: (benutzer.display_name or benutzer.username)
|
benutzer.id: (benutzer.display_name or benutzer.username)
|
||||||
@@ -249,7 +276,9 @@ class PlanungPlugin(BasePlugin):
|
|||||||
"ist_redaktion": user.role in REDAKTION,
|
"ist_redaktion": user.role in REDAKTION,
|
||||||
"meldung": meldung[:300],
|
"meldung": meldung[:300],
|
||||||
"fehler": fehler[: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,
|
autor=zeile.autor,
|
||||||
bgg_id=zeile.bgg_id,
|
bgg_id=zeile.bgg_id,
|
||||||
bild_url=zeile.bild_url,
|
bild_url=zeile.bild_url,
|
||||||
|
quellen_url=zeile.quellen_url,
|
||||||
ausgabe="",
|
ausgabe="",
|
||||||
rezensent_id=user.id,
|
rezensent_id=user.id,
|
||||||
status="offen",
|
status="offen",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class Planungseintrag(Base):
|
|||||||
bgg_id: Mapped[int | None] = mapped_column(Integer)
|
bgg_id: Mapped[int | None] = mapped_column(Integer)
|
||||||
#: URL des Coverbildes (aus der Neuheitenliste übernommen oder leer).
|
#: URL des Coverbildes (aus der Neuheitenliste übernommen oder leer).
|
||||||
bild_url: Mapped[str | None] = mapped_column(String(500))
|
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).
|
#: Magazin-Ausgabe, z. B. „3/2025“ (mehrere Ausgaben parallel möglich).
|
||||||
ausgabe: Mapped[str] = mapped_column(String(100), default="")
|
ausgabe: Mapped[str] = mapped_column(String(100), default="")
|
||||||
rezensent_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
rezensent_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||||
|
|||||||
@@ -76,6 +76,10 @@
|
|||||||
{% if eintrag.bgg_id %}
|
{% if eintrag.bgg_id %}
|
||||||
<a href="https://boardgamegeek.com/boardgame/{{ eintrag.bgg_id }}" target="_blank" rel="noopener"
|
<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>
|
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 %}
|
{% endif %}
|
||||||
{% if eintrag.pruefung %}
|
{% 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"
|
<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 %}
|
{% if eintrag.bgg_id %}
|
||||||
<a href="https://boardgamegeek.com/boardgame/{{ eintrag.bgg_id }}" target="_blank" rel="noopener"
|
<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>
|
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 %}
|
{% endif %}
|
||||||
{% if eintrag.pruefung %}
|
{% if eintrag.pruefung %}
|
||||||
<span class="ml-1 inline-block px-2 py-0.5 rounded-full bg-amber-100 text-amber-800 text-xs"
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -174,8 +174,10 @@ def test_migration_legt_planungsliste_an(app):
|
|||||||
text("SELECT version FROM schema_migrations WHERE plugin='planung'")
|
text("SELECT version FROM schema_migrations WHERE plugin='planung'")
|
||||||
).fetchall()
|
).fetchall()
|
||||||
assert "planungsliste" in tabellen
|
assert "planungsliste" in tabellen
|
||||||
# 0002_bild_url: Coverbild-Spalte (seit der Cover-Erweiterung).
|
# 0002_bild_url + 0003_quellen_url: Coverbild- und Quellen-Link-Spalten.
|
||||||
assert [z[0] for z in protokoll] == ["0001_planungsliste", "0002_bild_url"]
|
assert [z[0] for z in protokoll] == [
|
||||||
|
"0001_planungsliste", "0002_bild_url", "0003_quellen_url",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_anonym_wird_zum_login_umgeleitet(client):
|
def test_anonym_wird_zum_login_umgeleitet(client):
|
||||||
|
|||||||
Reference in New Issue
Block a user