Files
stiftung-management-system/app/stiftung/models/dokumente.py
SysAdmin Agent a79a0989d6 Phase 3: Django-natives DMS – Paperless-NGX durch DokumentDatei ersetzt
- Neues Modell DokumentDatei mit PostgreSQL FTS (SearchVectorField, GinIndex)
- Upload-Pfad: dokumente/YYYY/MM/<uuid>/dateiname
- 7 DMS-Views: list, detail, download, upload (HTMX Drag&Drop), delete, edit, search_api
- Templates: list, detail, edit, upload mit Drag&Drop-Zone, Partials
- URLs: /dms/ komplett verdrahtet
- Sidebar: DMS als Primäreintrag, Paperless als Legacy
- Migrationsskript: manage.py migrate_paperless_dokumente (DokumentLink → DokumentDatei)
- compose.yml: paperless-Dienst deaktiviert (Legacy-Kommentarblock)
- Migration 0048 angewendet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:10:08 +00:00

181 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# models/dokumente.py
# Phase 3: Django-natives DMS ersetzt Paperless-NGX-Integration
import uuid
import os
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVector, SearchVectorField
from django.db import models
from django.utils import timezone
def dokument_upload_path(instance, filename):
"""Speichert Dateien in MEDIA_ROOT/dokumente/YYYY/MM/<uuid>/<original_filename>"""
ext = os.path.splitext(filename)[1].lower()
safe_name = os.path.basename(filename)[:100]
return f"dokumente/{timezone.now().strftime('%Y/%m')}/{instance.id}/{safe_name}"
class DokumentDatei(models.Model):
"""Nativ gespeicherte Datei im Django-DMS ersetzt Paperless-Referenzen."""
KONTEXT_CHOICES = [
("pachtvertrag", "Pachtvertrag"),
("antrag", "Antrag / Förderantrag"),
("verwendungsnachweis", "Verwendungsnachweis"),
("studiennachweis", "Studiennachweis"),
("rechnung", "Rechnung"),
("vertrag", "Vertrag"),
("bericht", "Bericht"),
("landkarte", "Landkarte / Kataster"),
("korrespondenz", "Korrespondenz / Brief"),
("bescheid", "Bescheid / Behörde"),
("anderes", "Sonstiges"),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
titel = models.CharField(max_length=255, verbose_name="Titel")
beschreibung = models.TextField(blank=True, verbose_name="Beschreibung")
kontext = models.CharField(
max_length=30,
choices=KONTEXT_CHOICES,
default="anderes",
verbose_name="Dokumententyp",
)
datei = models.FileField(
upload_to=dokument_upload_path,
verbose_name="Datei",
)
dateiname_original = models.CharField(
max_length=255, blank=True, verbose_name="Originaldateiname"
)
dateityp = models.CharField(
max_length=100, blank=True, verbose_name="MIME-Typ"
)
dateigroesse = models.PositiveIntegerField(
default=0, verbose_name="Dateigröße (Bytes)"
)
# Volltext-Index (PostgreSQL FTS, befüllt per Signal)
inhaltstext = models.TextField(
blank=True,
verbose_name="Extrahierter Textinhalt",
help_text="Automatisch befüllt für PDF/TXT-Dateien. Basis für Volltextsuche.",
)
suchvektor = SearchVectorField(
null=True, blank=True, verbose_name="Such-Vektor (FTS)"
)
# Zuordnungsfelder optional, ein Dokument kann mehreren Entitäten gehören
land = models.ForeignKey(
"Land",
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="dms_dokumente",
verbose_name="Länderei",
)
paechter = models.ForeignKey(
"Paechter",
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="dms_dokumente",
verbose_name="Pächter",
)
verpachtung = models.ForeignKey(
"LandVerpachtung",
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="dms_dokumente",
verbose_name="Verpachtung",
)
destinataer = models.ForeignKey(
"Destinataer",
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="dms_dokumente",
verbose_name="Destinatär",
)
foerderung = models.ForeignKey(
"Foerderung",
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="dms_dokumente",
verbose_name="Förderung",
)
rentmeister = models.ForeignKey(
"Rentmeister",
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="dms_dokumente",
verbose_name="Rentmeister",
)
# Herkunft (optional: Verweis auf altes Paperless-Dokument zur Rückverfolgung)
paperless_dokument_id = models.IntegerField(
null=True, blank=True,
verbose_name="Paperless-ID (Migration)",
help_text="Wird nach vollständiger Migration entfernt.",
)
# Audit
erstellt_von = models.ForeignKey(
"auth.User",
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="hochgeladene_dokumente",
verbose_name="Erstellt von",
)
erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
aktualisiert_am = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
class Meta:
verbose_name = "Dokument"
verbose_name_plural = "Dokumente (DMS)"
ordering = ["-erstellt_am"]
indexes = [
# PostgreSQL GIN-Index für Volltextsuche
GinIndex(fields=["suchvektor"], name="dms_suchvektor_gin_idx"),
models.Index(fields=["kontext"]),
models.Index(fields=["destinataer", "kontext"]),
models.Index(fields=["land", "kontext"]),
models.Index(fields=["paechter", "kontext"]),
]
def __str__(self):
return self.titel or self.dateiname_original or str(self.id)
def save(self, *args, **kwargs):
# Originaldateiname aus FileField ableiten
if self.datei and not self.dateiname_original:
self.dateiname_original = os.path.basename(self.datei.name)
super().save(*args, **kwargs)
def update_suchvektor(self):
"""Aktualisiert den Such-Vektor aus Titel, Beschreibung und Inhaltstext."""
DokumentDatei.objects.filter(pk=self.pk).update(
suchvektor=SearchVector("titel", weight="A")
+ SearchVector("beschreibung", weight="B")
+ SearchVector("inhaltstext", weight="C"),
)
def get_datei_url(self):
"""Gibt die Download-URL zurück."""
if self.datei:
return self.datei.url
return None
def is_pdf(self):
return self.dateityp == "application/pdf" or (
self.dateiname_original and self.dateiname_original.lower().endswith(".pdf")
)
def get_human_size(self):
"""Gibt die Dateigröße leserlich zurück."""
size = self.dateigroesse
if size < 1024:
return f"{size} B"
elif size < 1024 * 1024:
return f"{size / 1024:.1f} KB"
else:
return f"{size / (1024 * 1024):.1f} MB"