BGG: Powered-by-Logo im Footer (Lizenzpflicht) + Erscheinungsjahr-Filter (Standard aktuelles Jahr+)
This commit is contained in:
1
.env
1
.env
@@ -1,2 +1,3 @@
|
|||||||
SPIELE_SESSION_SECRET=48ebf7ddae261a931652b5a55cff708aa21cf4936a8bdeb287eeac7cec4ee3e2
|
SPIELE_SESSION_SECRET=48ebf7ddae261a931652b5a55cff708aa21cf4936a8bdeb287eeac7cec4ee3e2
|
||||||
SPIELE_INITIAL_ADMIN_PASSWORD=3fb93f84014ff45406b36ec3
|
SPIELE_INITIAL_ADMIN_PASSWORD=3fb93f84014ff45406b36ec3
|
||||||
|
SPIELE_BGG_TOKEN=6fa6f9f6-32fd-443d-a5c3-ac2527f6a6fc
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
user: User = Depends(require_user),
|
user: User = Depends(require_user),
|
||||||
q: str = "",
|
q: str = "",
|
||||||
status: str = "",
|
status: str = "",
|
||||||
|
jahr: int | None = None,
|
||||||
sort: str = STANDARD_SORTIERUNG[0],
|
sort: str = STANDARD_SORTIERUNG[0],
|
||||||
richtung: str = STANDARD_SORTIERUNG[1],
|
richtung: str = STANDARD_SORTIERUNG[1],
|
||||||
meldung: str = "",
|
meldung: str = "",
|
||||||
@@ -118,6 +119,26 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
)
|
)
|
||||||
if status.strip():
|
if status.strip():
|
||||||
abfrage = abfrage.where(Neuheit.status == status.strip())
|
abfrage = abfrage.where(Neuheit.status == status.strip())
|
||||||
|
# Erscheinungsjahr-Filter: Standard „aktuelles Jahr und neuer“.
|
||||||
|
jahr_standard = datetime.now(timezone.utc).year
|
||||||
|
if jahr is None:
|
||||||
|
jahr_filter = jahr_standard
|
||||||
|
abfrage = abfrage.where(
|
||||||
|
or_(
|
||||||
|
Neuheit.erscheinungsjahr >= jahr_standard,
|
||||||
|
Neuheit.erscheinungsjahr.is_(None), # ohne Jahr nicht wegfiltern
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
jahr_filter = jahr
|
||||||
|
abfrage = abfrage.where(Neuheit.erscheinungsjahr >= jahr)
|
||||||
|
jahr_optionen = [
|
||||||
|
j for j in db.scalars(
|
||||||
|
select(Neuheit.erscheinungsjahr).distinct().order_by(
|
||||||
|
Neuheit.erscheinungsjahr.desc()
|
||||||
|
)
|
||||||
|
) 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
|
||||||
eintraege = db.scalars(
|
eintraege = db.scalars(
|
||||||
abfrage.order_by(spalte_sortiert, Neuheit.id.asc())
|
abfrage.order_by(spalte_sortiert, Neuheit.id.asc())
|
||||||
@@ -138,6 +159,9 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
"eintraege": eintraege,
|
"eintraege": eintraege,
|
||||||
"q": q,
|
"q": q,
|
||||||
"status_filter": status,
|
"status_filter": status,
|
||||||
|
"jahr_filter": jahr_filter,
|
||||||
|
"jahr_standard": jahr_standard,
|
||||||
|
"jahr_optionen": jahr_optionen,
|
||||||
"sort": sort if sort in SORTIERBAR else STANDARD_SORTIERUNG[0],
|
"sort": sort if sort in SORTIERBAR else STANDARD_SORTIERUNG[0],
|
||||||
"richtung": "ab" if abwaerts else "auf",
|
"richtung": "ab" if abwaerts else "auf",
|
||||||
"status_optionen": status_optionen,
|
"status_optionen": status_optionen,
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
<form method="get" action="/neuheiten"
|
<form method="get" action="/neuheiten"
|
||||||
hx-get="/neuheiten" hx-target="#neuheiten-liste" hx-swap="outerHTML"
|
hx-get="/neuheiten" hx-target="#neuheiten-liste" hx-swap="outerHTML"
|
||||||
hx-trigger="input changed delay:300ms from:find input[name='q'], change from:find select[name='status']"
|
hx-trigger="input changed delay:300ms from:find input[name='q'], change from:find select[name='status'], change from:find select[name='jahr']"
|
||||||
class="grid grid-cols-1 sm:flex sm:flex-wrap gap-3 mb-4">
|
class="grid grid-cols-1 sm:flex sm:flex-wrap gap-3 mb-4">
|
||||||
<input type="text" name="q" value="{{ q }}" placeholder="Suche in Titel, Verlag, Autor …"
|
<input type="text" name="q" value="{{ q }}" placeholder="Suche in Titel, Verlag, Autor …"
|
||||||
class="w-full min-h-[44px] border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 sm:w-80">
|
class="w-full min-h-[44px] border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 sm:w-80">
|
||||||
@@ -131,6 +131,13 @@
|
|||||||
<option value="{{ option }}" {% if option == status_filter %}selected{% endif %}>{{ option }}</option>
|
<option value="{{ option }}" {% if option == status_filter %}selected{% endif %}>{{ option }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<select name="jahr" class="w-full sm:w-auto min-h-[44px] md:min-h-0 border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white">
|
||||||
|
<option value="{{ jahr_standard }}">ab {{ jahr_standard }}</option>
|
||||||
|
{% for jahr in jahr_optionen %}
|
||||||
|
<option value="{{ jahr }}" {% if jahr|string == jahr_filter|string %}selected{% endif %}>{{ jahr }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
<option value="" {% if not jahr_filter %}selected{% endif %}>Alle Jahre</option>
|
||||||
|
</select>
|
||||||
<noscript>
|
<noscript>
|
||||||
<button type="submit" class="bg-slate-200 hover:bg-slate-300 px-4 py-2 rounded-lg text-sm min-h-[44px] md:min-h-0">Filtern</button>
|
<button type="submit" class="bg-slate-200 hover:bg-slate-300 px-4 py-2 rounded-lg text-sm min-h-[44px] md:min-h-0">Filtern</button>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, select_autoescape
|
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, select_autoescape
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -109,6 +110,10 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
openapi_url=None,
|
openapi_url=None,
|
||||||
)
|
)
|
||||||
|
# Statische Kern-Dateien (z. B. „Powered by BGG“-Logo, Pflicht für
|
||||||
|
# öffentliche BGG-XML-API-Anwendungen).
|
||||||
|
statisch = Path(__file__).parent / "static"
|
||||||
|
app.mount("/static", StaticFiles(directory=statisch), name="statisch")
|
||||||
app.state.settings = settings
|
app.state.settings = settings
|
||||||
app.state.engine = engine
|
app.state.engine = engine
|
||||||
app.state.session_factory = session_factory
|
app.state.session_factory = session_factory
|
||||||
|
|||||||
BIN
src/redaktionskern/static/powered-by-bgg.png
Normal file
BIN
src/redaktionskern/static/powered-by-bgg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -73,6 +73,12 @@
|
|||||||
</main>
|
</main>
|
||||||
<footer class="text-center text-xs text-slate-400 py-4">
|
<footer class="text-center text-xs text-slate-400 py-4">
|
||||||
{{ app_name }} · Plugin-Anzahl: {{ geladene_plugins|length }}
|
{{ app_name }} · Plugin-Anzahl: {{ geladene_plugins|length }}
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="https://boardgamegeek.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img src="/static/powered-by-bgg.png" alt="Powered by BoardGameGeek"
|
||||||
|
width="120" height="35" class="inline-block opacity-80 hover:opacity-100">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -435,7 +435,8 @@ def test_neuheitenliste_rendert_platzhalter_ohne_bild(app, client):
|
|||||||
melde_an(client)
|
melde_an(client)
|
||||||
antwort = client.get("/neuheiten")
|
antwort = client.get("/neuheiten")
|
||||||
assert antwort.status_code == 200
|
assert antwort.status_code == 200
|
||||||
assert "<img" not in antwort.text
|
liste = antwort.text.split('<div id="neuheiten-liste"')[1].split("</main>")[0]
|
||||||
|
assert "<img" not in liste
|
||||||
assert "🎲" in antwort.text # Spielicon-Zeichen im Platzhalter-Div
|
assert "🎲" in antwort.text # Spielicon-Zeichen im Platzhalter-Div
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ Alle BGG-Antworten sind gemockt (FakeBggClient), kein echter Netzwerkverkehr.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from redaktionskern.app import create_app
|
from redaktionskern.app import create_app
|
||||||
@@ -77,7 +79,7 @@ def test_liste_zeigt_eintraege_und_deutsche_beschriftung(app, client):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
melde_an(client)
|
melde_an(client)
|
||||||
antwort = client.get("/neuheiten")
|
antwort = client.get("/neuheiten?jahr=1990")
|
||||||
|
|
||||||
assert antwort.status_code == 200
|
assert antwort.status_code == 200
|
||||||
for text in (
|
for text in (
|
||||||
@@ -135,12 +137,31 @@ def test_sortierung_nach_jahr_abwaerts(app, client):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
melde_an(client)
|
melde_an(client)
|
||||||
antwort = client.get("/neuheiten?sort=jahr&richtung=ab")
|
antwort = client.get("/neuheiten?sort=jahr&richtung=ab&jahr=1990")
|
||||||
reihenfolge = [zeile for zeile in ("Neu", "Mittel", "Alt") if zeile]
|
reihenfolge = [zeile for zeile in ("Neu", "Mittel", "Alt") if zeile]
|
||||||
position = [antwort.text.index(titel) for titel in reihenfolge]
|
position = [antwort.text.index(titel) for titel in reihenfolge]
|
||||||
assert position == sorted(position)
|
assert position == sorted(position)
|
||||||
|
|
||||||
|
|
||||||
|
def test_standard_jahr_filter_zeigt_nur_aktuelles_jahr_und_neuer(app, client):
|
||||||
|
"""Ohne jahr-Parameter: nur aktuelles Jahr und neuer (plus ohne Jahr)."""
|
||||||
|
with app.state.session_factory() as db:
|
||||||
|
db.add_all(
|
||||||
|
[
|
||||||
|
Neuheit(titel="Alt", bgg_id=1, erscheinungsjahr=1990),
|
||||||
|
Neuheit(titel="Aktuell", bgg_id=2, erscheinungsjahr=datetime.now(timezone.utc).year),
|
||||||
|
Neuheit(titel="Ohne Jahr", bgg_id=3),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
melde_an(client)
|
||||||
|
antwort = client.get("/neuheiten")
|
||||||
|
assert "Aktuell" in antwort.text
|
||||||
|
assert "Ohne Jahr" in antwort.text
|
||||||
|
assert ">Alt<" not in antwort.text
|
||||||
|
|
||||||
|
|
||||||
def test_unbekannte_sortierung_faellt_auf_standard_zurueck(client):
|
def test_unbekannte_sortierung_faellt_auf_standard_zurueck(client):
|
||||||
melde_an(client)
|
melde_an(client)
|
||||||
antwort = client.get("/neuheiten?sort=hack&richtung=auf")
|
antwort = client.get("/neuheiten?sort=hack&richtung=auf")
|
||||||
|
|||||||
Reference in New Issue
Block a user