Web-Quellen: An/Aus-Schalter pro Quelle im UI + Einzel-Quellen-Sync

This commit is contained in:
Flo Hartmann
2026-08-25 01:10:36 +00:00
parent 8b38bc6c89
commit 667196a4ba
5 changed files with 176 additions and 14 deletions

View File

@@ -29,7 +29,7 @@ import threading
from datetime import datetime, timedelta, timezone
from urllib.parse import quote
from fastapi import Depends, Form, Request
from fastapi import Depends, Form, HTTPException, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import func, or_, select
@@ -192,12 +192,41 @@ class NeuheitenPlugin(BasePlugin):
@self.router.post("/neuheiten/quellen-sync")
def quellen_jetzt_synchronisieren(
user: User = Depends(require_roles(Role.ADMIN.value, Role.REDAKTEUR.value)),
quelle: str = Form(""),
):
zeilen = self._quellen_sync_ausfuehren()
text = " · ".join(z.als_text() for z in zeilen)
ziel = f"/neuheiten?meldung={quote(f'Web-Quellen-Sync abgeschlossen: {text}')}"
"""Gesamt-Sync oder Einzel-Quellen-Sync (`quelle` = Quellname)."""
if quelle:
zeilen = self._quellen_sync_ausfuehren(nur_quellen={quelle})
ziel = f"/neuheiten?meldung={quote('Web-Quelle „' + quelle + '“ synchronisiert: ' + ' · '.join(z.als_text() for z in zeilen))}"
else:
zeilen = self._quellen_sync_ausfuehren()
text = " · ".join(z.als_text() for z in zeilen)
ziel = f"/neuheiten?meldung={quote(f'Web-Quellen-Sync abgeschlossen: {text}')}"
return RedirectResponse(ziel[:2000], status_code=303)
@self.router.post("/neuheiten/quellen/{quellen_name}/schalter")
def quellen_schalter(
quellen_name: str,
user: User = Depends(require_roles(Role.ADMIN.value, Role.REDAKTEUR.value)),
aktiv: str = Form(...),
):
"""An/Aus-Schalter einer Web-Quelle (übersteuert den Env-Standard)."""
if quellen_name not in {k.name for k in ADAPTER_KLASSEN}:
raise HTTPException(status_code=404, detail="Unbekannte Quelle")
assert self.context is not None and self.context.session_factory is not None
with self.context.session_factory() as db:
eintrag = db.scalar(
select(QuellenStatus).where(QuellenStatus.quelle == quellen_name)
)
if eintrag is None:
eintrag = QuellenStatus(quelle=quellen_name, url="")
db.add(eintrag)
eintrag.aktiv_schalter = aktiv == "1"
db.commit()
zustand = "eingeschaltet" if aktiv == "1" else "ausgeschaltet"
ziel = f"/neuheiten?meldung={quote(f'Quelle „{quellen_name}{zustand}.')}"
return RedirectResponse(ziel, status_code=303)
def _quellen_uebersicht(self, db) -> list[dict]:
"""Zeilen der Sync-Übersicht: Adapter + letzter Lauf pro Quelle."""
status_nach_quelle = {
@@ -207,12 +236,18 @@ class NeuheitenPlugin(BasePlugin):
ansichten = []
for klasse in ADAPTER_KLASSEN:
status = status_nach_quelle.get(klasse.name)
schalter = status.aktiv_schalter if status is not None else None
aktiv = (
schalter
if schalter is not None
else quelle_aktiv(klasse.name)
)
ansichten.append(
{
"name": klasse.name,
"anzeigename": klasse.anzeigename,
"url": klasse.start_urls[0] if klasse.start_urls else "",
"aktiv": quelle_aktiv(klasse.name),
"aktiv": aktiv,
"letzte_laufzeit": status.letzte_laufzeit if status else None,
"dauer_sekunden": status.dauer_sekunden if status else None,
"neu": status.anzahl_neu if status else 0,
@@ -301,12 +336,33 @@ class NeuheitenPlugin(BasePlugin):
"ALTER TABLE neuheiten ADD COLUMN quellen_url VARCHAR(500)"
)
def aktiv_schalter_spalte(conn) -> None:
"""Migration 0006: UI-Schalter pro Web-Quelle (nullable Boolean)."""
info = conn.exec_driver_sql(
"PRAGMA table_info(neuheiten_quellen_status)"
).fetchall()
vorhanden = any(zeile[1] == "aktiv_schalter" for zeile in info)
if conn.dialect.name == "sqlite" and vorhanden:
return
if conn.dialect.name != "sqlite":
existiert = conn.exec_driver_sql(
"SELECT column_name FROM information_schema.columns "
"WHERE table_name = 'neuheiten_quellen_status' "
"AND column_name = 'aktiv_schalter'"
).scalar()
if existiert is not None:
return
conn.exec_driver_sql(
"ALTER TABLE neuheiten_quellen_status ADD COLUMN aktiv_schalter BOOLEAN"
)
return [
Migration(version="0001_neuheiten_tabelle", up=neuheiten_tabelle),
Migration(version="0002_bgg_id_nullable", up=bgg_id_nullable),
Migration(version="0003_quellen_status", up=quellen_status_tabelle),
Migration(version="0004_bild_url", up=bild_url_spalte),
Migration(version="0005_quellen_url", up=quellen_url_spalte),
Migration(version="0006_aktiv_schalter", up=aktiv_schalter_spalte),
]
def navigation(self) -> list[NavEntry]:
@@ -386,8 +442,14 @@ class NeuheitenPlugin(BasePlugin):
finally:
self._sync_sperre.release()
def _quellen_sync_ausfuehren(self) -> list[QuellenLaufZeile]:
"""Ein Lauf über alle aktiven Web-Quellen (Fehler pro Quelle isoliert)."""
def _quellen_sync_ausfuehren(
self, nur_quellen: set[str] | None = None
) -> list[QuellenLaufZeile]:
"""Ein Lauf über alle aktiven Web-Quellen (Fehler pro Quelle isoliert).
`nur_quellen`: optional nur diese Quellnamen (manueller Einzellauf,
ignoriert den An/Aus-Schalter der Quelle).
"""
registry = (
self.context.registry
if self.context is not None and self.context.registry is not None
@@ -398,7 +460,7 @@ class NeuheitenPlugin(BasePlugin):
service = WebQuellenSyncService(
self.context.session_factory, dedup=dedup, audit=audit
)
return service.laufe()
return service.laufe(nur_quellen=nur_quellen)
def _quellen_sync_job(self) -> None:
"""Hintergrund-Job: holt regelmäßig die Web-Quellen ab."""