P1: Einheitliche klassische Pagination (Neuheiten, Planung, Audit-Log)
- Infinite Scroll ersetzt durch Zurück/Weiter + Seitenzahlen-Fenster (funktioniert ohne JavaScript) - Gemeinsames Makro seitennavigation.html (aria-current auf aktiver Seite, deaktivierte Buttons als span) - Neuheiten: Pagination bleibt beim HTMX-Filter-Swap innerhalb von #neuheiten-liste konsistent; Filter/Sortierung/Jahr bleiben in den Seiten-URLs erhalten - Audit-Log: gleiche Optik statt reiner Text-Links
This commit is contained in:
@@ -285,6 +285,7 @@ class AuditLogPlugin(BasePlugin):
|
|||||||
"ende": offset + len(eintraege),
|
"ende": offset + len(eintraege),
|
||||||
"url_zurueck": _seiten_url(seite - 1),
|
"url_zurueck": _seiten_url(seite - 1),
|
||||||
"url_weiter": _seiten_url(seite + 1),
|
"url_weiter": _seiten_url(seite + 1),
|
||||||
|
"seiten_url": _seiten_url,
|
||||||
"url_ohne_seite": f"/audit-log?{basis}" if basis else "/audit-log",
|
"url_ohne_seite": f"/audit-log?{basis}" if basis else "/audit-log",
|
||||||
"benutzer_auswahl": benutzer_auswahl,
|
"benutzer_auswahl": benutzer_auswahl,
|
||||||
"aktionen_auswahl": aktionen_auswahl,
|
"aktionen_auswahl": aktionen_auswahl,
|
||||||
|
|||||||
@@ -117,16 +117,7 @@
|
|||||||
0 Einträge
|
0 Einträge
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% if gesamt_seiten > 1 %}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
{% if seite > 1 %}
|
|
||||||
<a href="{{ url_zurueck }}" class="border border-slate-300 hover:bg-slate-50 rounded-lg px-3 py-1">← Zurück</a>
|
|
||||||
{% endif %}
|
|
||||||
<span>Seite {{ seite }} von {{ gesamt_seiten }}</span>
|
|
||||||
{% if seite < gesamt_seiten %}
|
|
||||||
<a href="{{ url_weiter }}" class="border border-slate-300 hover:bg-slate-50 rounded-lg px-3 py-1">Weiter →</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% from "seitennavigation.html" import seitennavigation %}
|
||||||
|
{{ seitennavigation(seiten_url, seite, gesamt_seiten) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from urllib.parse import quote
|
from math import ceil
|
||||||
|
from urllib.parse import quote, urlencode
|
||||||
|
|
||||||
from fastapi import Depends, Form, HTTPException, Request
|
from fastapi import Depends, Form, HTTPException, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
@@ -113,6 +114,7 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
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
|
pro_seite = 50
|
||||||
|
seite_aktuell = max(seite_nr, 1)
|
||||||
|
|
||||||
with self.context.session_factory() as db:
|
with self.context.session_factory() as db:
|
||||||
abfrage = select(Neuheit)
|
abfrage = select(Neuheit)
|
||||||
@@ -148,9 +150,11 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
) if j is not None
|
) if j is not None
|
||||||
]
|
]
|
||||||
gesamt = db.scalar(select(func.count()).select_from(abfrage.subquery())) or 0
|
gesamt = db.scalar(select(func.count()).select_from(abfrage.subquery())) or 0
|
||||||
|
gesamt_seiten = max(1, ceil(gesamt / pro_seite))
|
||||||
|
seite_aktuell = min(seite_aktuell, gesamt_seiten)
|
||||||
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)
|
.offset((seite_aktuell - 1) * pro_seite)
|
||||||
.limit(pro_seite)
|
.limit(pro_seite)
|
||||||
).all()
|
).all()
|
||||||
status_optionen = [
|
status_optionen = [
|
||||||
@@ -160,7 +164,18 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
quellen_zeilen = self._quellen_uebersicht(db)
|
quellen_zeilen = self._quellen_uebersicht(db)
|
||||||
hat_weitere = max(seite_nr, 1) * pro_seite < gesamt
|
|
||||||
|
def _seiten_url(nr: int) -> str:
|
||||||
|
parameter = {
|
||||||
|
"q": q,
|
||||||
|
"status": status,
|
||||||
|
"sort": sort if sort in SORTIERBAR else STANDARD_SORTIERUNG[0],
|
||||||
|
"richtung": "ab" if abwaerts else "auf",
|
||||||
|
"seite_nr": str(nr),
|
||||||
|
}
|
||||||
|
if jahr is not None:
|
||||||
|
parameter["jahr"] = str(jahr)
|
||||||
|
return f"/neuheiten?{urlencode(parameter)}"
|
||||||
kontext = {
|
kontext = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"titel": self.title,
|
"titel": self.title,
|
||||||
@@ -182,8 +197,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": gesamt,
|
"anzahl": gesamt,
|
||||||
"hat_weitere": hat_weitere,
|
"seite": seite_aktuell,
|
||||||
"naechste_seite": max(seite_nr, 1) + 1,
|
"gesamt_seiten": gesamt_seiten,
|
||||||
|
"seiten_url": _seiten_url,
|
||||||
"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()
|
||||||
|
|||||||
@@ -122,11 +122,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% from "seitennavigation.html" import seitennavigation %}
|
||||||
|
{{ seitennavigation(seiten_url, seite, gesamt_seiten) }}
|
||||||
</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 %}
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ der Eintrage-Vorgang bricht.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from urllib.parse import quote
|
from math import ceil
|
||||||
|
from urllib.parse import quote, urlencode
|
||||||
|
|
||||||
from fastapi import Depends, Form, Request
|
from fastapi import Depends, Form, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
@@ -299,13 +300,22 @@ class PlanungPlugin(BasePlugin):
|
|||||||
gesamt = db.scalar(
|
gesamt = db.scalar(
|
||||||
select(func.count()).select_from(abfrage.subquery())
|
select(func.count()).select_from(abfrage.subquery())
|
||||||
) or 0
|
) or 0
|
||||||
|
gesamt_seiten = max(1, ceil(gesamt / pro_seite))
|
||||||
|
seite_aktuell = min(max(seite_nr, 1), gesamt_seiten)
|
||||||
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)
|
.offset((seite_aktuell - 1) * pro_seite)
|
||||||
.limit(pro_seite)
|
.limit(pro_seite)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
def _seiten_url(nr: int) -> str:
|
||||||
|
parameter = {"seite_nr": str(nr)}
|
||||||
|
if status in GUELTIGE_STATUS:
|
||||||
|
parameter["status"] = status
|
||||||
|
return f"/planung?{urlencode(parameter)}"
|
||||||
|
|
||||||
namen = {
|
namen = {
|
||||||
benutzer.id: (benutzer.display_name or benutzer.username)
|
benutzer.id: (benutzer.display_name or benutzer.username)
|
||||||
for benutzer in db.scalars(select(User)).all()
|
for benutzer in db.scalars(select(User)).all()
|
||||||
@@ -326,8 +336,9 @@ class PlanungPlugin(BasePlugin):
|
|||||||
"meldung": meldung[:300],
|
"meldung": meldung[:300],
|
||||||
"fehler": fehler[:300],
|
"fehler": fehler[:300],
|
||||||
"anzahl": gesamt,
|
"anzahl": gesamt,
|
||||||
"hat_weitere": max(seite_nr, 1) * pro_seite < gesamt,
|
"seite": seite_aktuell,
|
||||||
"naechste_seite": max(seite_nr, 1) + 1,
|
"gesamt_seiten": gesamt_seiten,
|
||||||
|
"seiten_url": _seiten_url,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -217,13 +217,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if hat_weitere %}
|
{% from "seitennavigation.html" import seitennavigation %}
|
||||||
<div id="planung-lade-mehr"
|
{{ seitennavigation(seiten_url, seite, gesamt_seiten) }}
|
||||||
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 %}
|
|
||||||
<!-- Lösch-Bestätigungsdialog (Progressive Enhancement: ohne JavaScript
|
<!-- Lösch-Bestätigungsdialog (Progressive Enhancement: ohne JavaScript
|
||||||
sendet das Formular sofort, mit Alpine öffnet sich dieser Dialog). -->
|
sendet das Formular sofort, mit Alpine öffnet sich dieser Dialog). -->
|
||||||
<div x-show="loeschFormular !== null" x-cloak
|
<div x-show="loeschFormular !== null" x-cloak
|
||||||
|
|||||||
61
src/redaktionskern/templates/seitennavigation.html
Normal file
61
src/redaktionskern/templates/seitennavigation.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{# Klassische Seitennavigation: Zurück/Weiter + Seitenzahlen.
|
||||||
|
Parameter: seiten_url (Funktion nr → URL), seite, gesamt_seiten.
|
||||||
|
Funktioniert ohne JavaScript (einfache Links). #}
|
||||||
|
{% from "icons.html" import icon_chevron_links, icon_chevron_rechts %}
|
||||||
|
{% macro seitennavigation(seiten_url, seite, gesamt_seiten) %}
|
||||||
|
{% if gesamt_seiten > 1 %}
|
||||||
|
<nav class="flex flex-wrap items-center justify-between gap-3 mt-4" aria-label="Seitennavigation">
|
||||||
|
<div>
|
||||||
|
{% if seite > 1 %}
|
||||||
|
<a href="{{ seiten_url(seite - 1) }}" rel="prev"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-300 text-sm font-medium text-slate-700 hover:bg-slate-50">
|
||||||
|
{{ icon_chevron_links("h-4 w-4") }} Zurück
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span aria-disabled="true"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-200 text-sm font-medium text-slate-300 cursor-not-allowed">
|
||||||
|
{{ icon_chevron_links("h-4 w-4") }} Zurück
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
|
<span class="hidden sm:inline text-sm text-slate-500 mr-2">Seite {{ seite }} von {{ gesamt_seiten }}</span>
|
||||||
|
<span class="sm:hidden sr-only">Seite {{ seite }} von {{ gesamt_seiten }}</span>
|
||||||
|
{% set von = [seite - 2, 1] | max %}
|
||||||
|
{% set bis = [seite + 2, gesamt_seiten] | min %}
|
||||||
|
{% if von > 1 %}
|
||||||
|
<a href="{{ seiten_url(1) }}"
|
||||||
|
class="px-3 py-2 rounded-lg border border-slate-300 text-sm text-slate-700 hover:bg-slate-50">1</a>
|
||||||
|
{% if von > 2 %}<span class="px-1 text-slate-400" aria-hidden="true">…</span>{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% for nr in range(von, bis + 1) %}
|
||||||
|
{% if nr == seite %}
|
||||||
|
<span aria-current="page"
|
||||||
|
class="px-3 py-2 rounded-lg bg-emerald-600 text-white text-sm font-semibold">{{ nr }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ seiten_url(nr) }}"
|
||||||
|
class="px-3 py-2 rounded-lg border border-slate-300 text-sm text-slate-700 hover:bg-slate-50">{{ nr }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if bis < gesamt_seiten %}
|
||||||
|
{% if bis < gesamt_seiten - 1 %}<span class="px-1 text-slate-400" aria-hidden="true">…</span>{% endif %}
|
||||||
|
<a href="{{ seiten_url(gesamt_seiten) }}"
|
||||||
|
class="px-3 py-2 rounded-lg border border-slate-300 text-sm text-slate-700 hover:bg-slate-50">{{ gesamt_seiten }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if seite < gesamt_seiten %}
|
||||||
|
<a href="{{ seiten_url(seite + 1) }}" rel="next"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-300 text-sm font-medium text-slate-700 hover:bg-slate-50">
|
||||||
|
Weiter {{ icon_chevron_rechts("h-4 w-4") }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span aria-disabled="true"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-200 text-sm font-medium text-slate-300 cursor-not-allowed">
|
||||||
|
Weiter {{ icon_chevron_rechts("h-4 w-4") }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
Reference in New Issue
Block a user