- Adapter-Pattern mit drei Kanaelen: E-Mail (SMTP per Env, TLS starttls/ssl, Dev-Fallback: Protokoll), Telegram (Bot-API, Token per Env, Chat-ID pro Benutzer), In-App (persistente Nachrichten mit Unread-Counter und 'Alle als gelesen markieren') - Pro Benutzer Kanal-Praeferenzen (Einstellungsseite, mehrere Kanaele gleichzeitig) plus eigene Kontakt-Tabelle (E-Mail-Adresse, Chat-ID); Kern und users-Tabelle unveraendert - Oeffentliche Plugin-API: await send_notification(user, titel, text, kategorie) - andere Plugins holen das Plugin ueber app.state.registry - Eigene Migration (0001_tabellen), eigene Routen/Templates, deutsche UI - Tests: Adapter-Auswahl nach Praeferenz, In-App-Persistenz, Dev-Log- Adapter, SMTP-Versand (gemockt), Telegram-API-Aufruf, Einstellungsseite - README: Plugin-Doku + neue Env-Variablen; docker-compose: Platzhalter
81 lines
3.2 KiB
Python
81 lines
3.2 KiB
Python
"""Datenmodelle des Plugins „benachrichtigung“.
|
|
|
|
Alle Tabellen tragen den Präfix `benachrichtigung_` und nutzen portable
|
|
Spaltentypen (SQLite ↔ Postgres). Der Kern bleibt unverändert — auch die
|
|
Benutzer-Tabelle wird nicht angetastet; Kanal-Kontaktdaten (E-Mail-Adresse,
|
|
Telegram-Chat-ID) liegen in der eigenen Kontakt-Tabelle.
|
|
|
|
`extend_existing` erlaubt das erneute Ausführen des Moduls durch den
|
|
Plugin-Loader (jeder App-Start lädt das Plugin frisch bzw. Tests können es
|
|
über einen zweiten Pfad importieren), ohne dass die gemeinsame Metadata
|
|
doppelt definierte Tabellen meldet. Die Modelle definieren bewusst keine
|
|
ORM-Indizes: Bei erneuter Ausführung würde ein deklarativ definierter Index
|
|
doppelt im Table-Objekt landen und die Migration würde ihn zweimal anlegen.
|
|
Die Datenmengen sind klein — auf Indizes wird verzichtet.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from redaktionskern.db import Base
|
|
|
|
#: Gültige Kanal-Schlüssel (Reihenfolge = Anzeigereihenfolge im Formular).
|
|
KANAELLE: dict[str, str] = {
|
|
"email": "E-Mail",
|
|
"telegram": "Telegram",
|
|
"inapp": "In-App",
|
|
}
|
|
STANDARD_KANAELLE = "inapp"
|
|
|
|
|
|
class Benachrichtigung(Base):
|
|
"""Eine In-App-Nachricht im Portal (Posteingang des Benutzers)."""
|
|
|
|
__tablename__ = "benachrichtigung"
|
|
__table_args__ = {"extend_existing": True}
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
|
titel: Mapped[str] = mapped_column(String(300))
|
|
text: Mapped[str] = mapped_column(Text)
|
|
kategorie: Mapped[str] = mapped_column(String(100), default="allgemein")
|
|
gelesen: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
|
|
|
|
class Praeferenz(Base):
|
|
"""Kanal-Präferenzen eines Benutzers (mehrere Kanäle gleichzeitig möglich)."""
|
|
|
|
__tablename__ = "benachrichtigung_praeferenz"
|
|
__table_args__ = {"extend_existing": True}
|
|
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), primary_key=True)
|
|
#: Komma-separierte Kanal-Schlüssel, z. B. "email,inapp".
|
|
kanaele: Mapped[str] = mapped_column(String(200), default=STANDARD_KANAELLE)
|
|
aktualisiert_at: Mapped[datetime] = mapped_column(
|
|
DateTime, server_default=func.now()
|
|
)
|
|
|
|
def kanaele_als_liste(self) -> list[str]:
|
|
gueltig = [k for k in self.kanaele.split(",") if k in KANAELLE]
|
|
return gueltige_reihenfolge(gueltig)
|
|
|
|
|
|
def gueltige_reihenfolge(auswahl: list[str]) -> list[str]:
|
|
"""Sortiert eine Kanal-Auswahl in die definierte Anzeigereihenfolge."""
|
|
return [k for k in KANAELLE if k in auswahl]
|
|
|
|
|
|
class Kontakt(Base):
|
|
"""Kanal-spezifische Kontaktdaten eines Benutzers (pro Benutzer höchstens eine Zeile)."""
|
|
|
|
__tablename__ = "benachrichtigung_kontakt"
|
|
__table_args__ = {"extend_existing": True}
|
|
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), primary_key=True)
|
|
email: Mapped[str | None] = mapped_column(String(320), nullable=True)
|
|
telegram_chat_id: Mapped[str | None] = mapped_column(String(50), nullable=True)
|