"""Datenmodelle des Plugins „archiv“. Das Archiv folgt dem Sicherungsdatei-Prinzip: Beim Archivieren wird jede Zeile vollständig in eine eigene Archiv-Tabelle kopiert und aus der aktiven Liste entfernt — nichts wird verändert oder gelöscht, ein Eintrag lässt sich jederzeit unverändert zurück in die aktive Liste verschieben. Es gibt zwei Spiegel-Tabellen (eine pro aktiver Liste): - `archiv_neuheiten` ← Spiegel von `neuheiten` - `archiv_planung` ← Spiegel von `planungsliste` Bewusst ohne Fremdschlüssel: Archivierte Zeilen müssen auch dann erhalten bleiben, wenn z. B. der zugeordnete Rezensent gelöscht wird. Bei der Wiederherstellung wird die Gültigkeit geprüft und notfalls ein Ersatz- Rezensent zugeordnet. Bewusst keine zusätzlichen Indizes: Bei doppelt geladenem Modul (direkter Import im Test + Plugin-Loader-Namensraum) würden sie doppelt angelegt; die Nutzerzahl ist klein. Die Archiv-Metadaten (wer/wann/warum) stehen in jeder Zeile: `quell_id` (ursprüngliche ID in der aktiven Liste), `archiviert_am`, `archiviert_von`, `grund` und `referenz_am` (Zeitpunkt, an dem die 12-Monats-Regel angesetzt hat — Erscheinungstermin bzw. Eintragsdatum). """ from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, Integer, JSON, String, Text, func from sqlalchemy.orm import Mapped, mapped_column from redaktionskern.db import Base class _ArchivMeta: """Gemeinsame Archiv-Metadaten beider Spiegel-Tabellen (Mixin).""" #: ID der Zeile in der ursprünglichen aktiven Liste. quell_id: Mapped[int] = mapped_column(Integer) #: Zeitpunkt der Archivierung (naive UTC, wie überall in der App). archiviert_am: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) #: „System“ beim automatischen Lauf, sonst Benutzername der Wiederherstellung… # noqa archiviert_von: Mapped[str] = mapped_column(String(100), default="System") #: Menschlich lesbarer Grund, z. B. „älter als 12 Monate (Erscheinungsjahr 2023)“. grund: Mapped[str] = mapped_column(String(200), default="") #: Datum, an dem die 12-Monats-Frist angesetzt hat (Erscheinung bzw. Eintrag). referenz_am: Mapped[datetime | None] = mapped_column(DateTime) class ArchivNeuheit(_ArchivMeta, Base): """Archivierter Neuheiten-Eintrag (Spiegel der Tabelle `neuheiten`).""" __tablename__ = "archiv_neuheiten" # extend_existing: Das Plugin kann doppelt geladen werden (direkter Import # im Test + Plugin-Loader-Namensraum) und teilt sich dieselbe Metadata. __table_args__ = {"extend_existing": True} id: Mapped[int] = mapped_column(primary_key=True) titel: Mapped[str] = mapped_column(String(300)) verlag: Mapped[str | None] = mapped_column(String(300)) autor: Mapped[str | None] = mapped_column(String(300)) erscheinungsjahr: Mapped[int | None] = mapped_column(Integer) bgg_id: Mapped[int | None] = mapped_column(Integer) bild_url: Mapped[str | None] = mapped_column(String(500)) status: Mapped[str] = mapped_column(String(50), default="neuheit") quelle: Mapped[str] = mapped_column(String(100), default="boardgamegeek") erstellt_am: Mapped[datetime | None] = mapped_column(DateTime) aktualisiert_am: Mapped[datetime | None] = mapped_column(DateTime) def __repr__(self) -> str: # pragma: no cover - Debug-Hilfe return f"" class ArchivPlanung(_ArchivMeta, Base): """Archivierter Planungseintrag (Spiegel der Tabelle `planungsliste`).""" __tablename__ = "archiv_planung" __table_args__ = {"extend_existing": True} id: Mapped[int] = mapped_column(primary_key=True) titel: Mapped[str] = mapped_column(String(300)) verlag: Mapped[str | None] = mapped_column(String(300)) autor: Mapped[str | None] = mapped_column(String(300)) bgg_id: Mapped[int | None] = mapped_column(Integer) bild_url: Mapped[str | None] = mapped_column(String(500)) ausgabe: Mapped[str] = mapped_column(String(100), default="") #: Kein FK — Archiv-Zeilen überleben das Löschen des Benutzers. rezensent_id: Mapped[int | None] = mapped_column(Integer) status: Mapped[str] = mapped_column(String(50), default="offen") notizen: Mapped[str | None] = mapped_column(Text) quelle: Mapped[str] = mapped_column(String(50), default="manuell") pruefung: Mapped[list | None] = mapped_column(JSON) erstellt_am: Mapped[datetime | None] = mapped_column(DateTime) aktualisiert_am: Mapped[datetime | None] = mapped_column(DateTime) def __repr__(self) -> str: # pragma: no cover - Debug-Hilfe return f"" #: Deutsche Anzeigenamen der Statuswerte (bewusst als Kopie ohne #: Import-Abhängigkeit zu den Partner-Plugins, siehe planung/KONFLIKT_ANZEIGE). STATUS_ANZEIGE: dict[str, str] = { "neuheit": "Neuheit", "planung": "In Planung", "offen": "Offen", "in_bearbeitung": "In Bearbeitung", "abgeschlossen": "Abgeschlossen", }