- Ausgaben (Name + Redaktionsschluss-Datum) verwalten, mehrere parallel, Anlegen/Bearbeiten/Löschen nur für Admin, mit Audit-Log-Einträgen - Erinnerung 28 Tage vor dem Redaktionsschluss an alle aktiven Rezensenten mit offener (oder fehlender) Planung, Versand über das benachrichtigung- Plugin je Kanal-Präferenz, Audit als System/benachrichtigt - Täglicher APScheduler-Job (SPIELE_ERINNERUNG_JOB_UHRZEIT, abschaltbar) plus manuelle Sofort-Prüfung in der Admin-Ansicht - Doppelschutz: Protokolltabelle mit Unique (Ausgabe, Benutzer) - Admin-UI: Ausgabenliste mit Restzeit-Badges, Erinnerungsstatus (wer wurde wann über welche Kanäle erinnert), deutsche Templates - Tests mit eingefrorener Uhr (freezegun): Fenster-Grenzen, kein Versand vor Frist/nach Schluss, Empfänger-/Rollenregeln, keine Duplikate, Scheduler-Konfiguration; Integrationstest mit den echten Plugins (167 Tests grün)
65 lines
2.8 KiB
Python
65 lines
2.8 KiB
Python
"""Datenmodelle des Plugins „erinnerung“.
|
|
|
|
Zwei eigene Tabellen (SQLite ↔ Postgres portabel), der Kern bleibt unberührt:
|
|
|
|
- `erinnerung_ausgabe` — eine Magazin-Ausgabe mit Redaktionsschluss-Datum.
|
|
Mehrere Ausgaben laufen parallel; der Name ist frei wählbar (z. B. „3/2025“).
|
|
- `erinnerung_protokoll` — protokolliert jede versendete Erinnerung
|
|
(Ausgabe + Benutzer). Die Eindeutigkeit von (ausgabe_id, user_id) stellt
|
|
auf Datenbankebene sicher, dass niemand zweimal pro Ausgabe erinnert wird.
|
|
|
|
Keine ORM-Indizes: Bei erneuter Ausführung des Moduls würde ein deklarativ
|
|
definierter Index doppelt im Table-Objekt landen und die Migration würde ihn
|
|
zweimal anlegen (gleiche Begründung wie im benachrichtigung-Plugin).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
|
|
from sqlalchemy import Date, DateTime, ForeignKey, String, UniqueConstraint, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from redaktionskern.db import Base
|
|
|
|
|
|
class Ausgabe(Base):
|
|
"""Eine Magazin-Ausgabe mit ihrem Redaktionsschluss-Datum."""
|
|
|
|
__tablename__ = "erinnerung_ausgabe"
|
|
__table_args__ = {"extend_existing": True}
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
#: Name/Titel der Ausgabe, z. B. „3/2025“ oder „Sommer-Sonderheft“.
|
|
name: Mapped[str] = mapped_column(String(200))
|
|
#: Redaktionsschluss (Datum, ohne Uhrzeit).
|
|
redaktionsschluss: Mapped[date] = mapped_column(Date)
|
|
erstellt_am: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
aktualisiert_am: Mapped[datetime] = mapped_column(
|
|
DateTime, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
def __repr__(self) -> str: # pragma: no cover - Debug-Hilfe
|
|
return f"<Ausgabe {self.name!r} (Redaktionsschluss {self.redaktionsschluss})>"
|
|
|
|
|
|
class ErinnerungsProtokoll(Base):
|
|
"""Eine versendete Erinnerung: wer wurde zu welcher Ausgabe wann erinnert."""
|
|
|
|
__tablename__ = "erinnerung_protokoll"
|
|
__table_args__ = (
|
|
UniqueConstraint("ausgabe_id", "user_id", name="uq_erinnerung_ausgabe_user"),
|
|
{"extend_existing": True},
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
ausgabe_id: Mapped[int] = mapped_column(ForeignKey("erinnerung_ausgabe.id"))
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
|
#: Zugestellte Kanäle (Komma-liste, z. B. „email,inapp“) — wie vom
|
|
#: benachrichtigung-Plugin gemeldet; leer, falls kein Kanal zustellte.
|
|
kanaele: Mapped[str] = mapped_column(String(200), default="")
|
|
#: Wann wurde die Erinnerung ausgelöst.
|
|
erstellt_am: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
|
|
def __repr__(self) -> str: # pragma: no cover - Debug-Hilfe
|
|
return f"<ErinnerungsProtokoll Ausgabe={self.ausgabe_id} User={self.user_id}>"
|