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>
This commit is contained in:
252
app/stiftung/views/dms.py
Normal file
252
app/stiftung/views/dms.py
Normal file
@@ -0,0 +1,252 @@
|
||||
# views/dms.py
|
||||
# Phase 3: Django-natives DMS – Dokumentenverwaltung ohne Paperless-NGX
|
||||
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import FileResponse, Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from stiftung.models import (
|
||||
Destinataer, DokumentDatei, Foerderung, Land, LandVerpachtung, Paechter, Rentmeister
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _save_upload(request, instance: DokumentDatei):
|
||||
"""Speichert Upload-Metadaten (Dateityp, Größe, FTS)."""
|
||||
if instance.datei:
|
||||
f = instance.datei
|
||||
instance.dateiname_original = os.path.basename(f.name)
|
||||
instance.dateityp = getattr(f, "content_type", "") or ""
|
||||
instance.dateigroesse = f.size if hasattr(f, "size") else 0
|
||||
instance.erstellt_von = request.user
|
||||
instance.save()
|
||||
instance.update_suchvektor()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DMS Hauptseiten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@login_required
|
||||
def dms_list(request):
|
||||
"""Dokumenten-Übersicht mit Filter und Suche."""
|
||||
q = request.GET.get("q", "").strip()
|
||||
kontext_filter = request.GET.get("kontext", "")
|
||||
entity_filter = request.GET.get("entity", "") # z.B. "destinataer"
|
||||
entity_id = request.GET.get("entity_id", "")
|
||||
|
||||
qs = DokumentDatei.objects.select_related(
|
||||
"destinataer", "land", "paechter", "verpachtung", "erstellt_von"
|
||||
)
|
||||
|
||||
# Volltextsuche
|
||||
if q:
|
||||
search_query = SearchQuery(q, config="german")
|
||||
qs = qs.annotate(rank=SearchRank("suchvektor", search_query)).filter(
|
||||
rank__gt=0.01
|
||||
).order_by("-rank")
|
||||
else:
|
||||
qs = qs.order_by("-erstellt_am")
|
||||
|
||||
if kontext_filter:
|
||||
qs = qs.filter(kontext=kontext_filter)
|
||||
|
||||
if entity_filter == "destinataer" and entity_id:
|
||||
qs = qs.filter(destinataer_id=entity_id)
|
||||
elif entity_filter == "land" and entity_id:
|
||||
qs = qs.filter(land_id=entity_id)
|
||||
elif entity_filter == "paechter" and entity_id:
|
||||
qs = qs.filter(paechter_id=entity_id)
|
||||
elif entity_filter == "verpachtung" and entity_id:
|
||||
qs = qs.filter(verpachtung_id=entity_id)
|
||||
|
||||
paginator = Paginator(qs, 25)
|
||||
page_obj = paginator.get_page(request.GET.get("page"))
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"q": q,
|
||||
"kontext_filter": kontext_filter,
|
||||
"kontext_choices": DokumentDatei.KONTEXT_CHOICES,
|
||||
"gesamt": qs.count() if not q else paginator.count,
|
||||
}
|
||||
return render(request, "stiftung/dms/list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def dms_detail(request, pk):
|
||||
"""Dokument-Detailseite mit Download-Link."""
|
||||
dok = get_object_or_404(DokumentDatei, pk=pk)
|
||||
context = {"dok": dok}
|
||||
return render(request, "stiftung/dms/detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def dms_download(request, pk):
|
||||
"""Direkter Datei-Download."""
|
||||
dok = get_object_or_404(DokumentDatei, pk=pk)
|
||||
if not dok.datei or not dok.datei.storage.exists(dok.datei.name):
|
||||
raise Http404("Datei nicht gefunden.")
|
||||
response = FileResponse(
|
||||
dok.datei.open("rb"),
|
||||
as_attachment=True,
|
||||
filename=dok.dateiname_original or os.path.basename(dok.datei.name),
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def dms_upload(request):
|
||||
"""HTMX-Drag&Drop-Upload – unterstützt normale POST-Anfragen und HTMX-Requests."""
|
||||
# Pre-fill entity links from GET params
|
||||
initial = {
|
||||
"destinataer_id": request.GET.get("destinataer", ""),
|
||||
"land_id": request.GET.get("land", ""),
|
||||
"paechter_id": request.GET.get("paechter", ""),
|
||||
"verpachtung_id": request.GET.get("verpachtung", ""),
|
||||
"kontext": request.GET.get("kontext", "anderes"),
|
||||
}
|
||||
|
||||
if request.method == "POST":
|
||||
datei = request.FILES.get("datei")
|
||||
titel = request.POST.get("titel", "").strip()
|
||||
beschreibung = request.POST.get("beschreibung", "").strip()
|
||||
kontext = request.POST.get("kontext", "anderes")
|
||||
|
||||
if not datei:
|
||||
if request.htmx:
|
||||
return JsonResponse({"error": "Keine Datei übermittelt."}, status=400)
|
||||
messages.error(request, "Bitte eine Datei auswählen.")
|
||||
else:
|
||||
if not titel:
|
||||
titel = os.path.splitext(datei.name)[0][:255]
|
||||
|
||||
dok = DokumentDatei(
|
||||
titel=titel,
|
||||
beschreibung=beschreibung,
|
||||
kontext=kontext,
|
||||
datei=datei,
|
||||
)
|
||||
|
||||
# Entity links
|
||||
dest_id = request.POST.get("destinataer_id", "").strip()
|
||||
land_id = request.POST.get("land_id", "").strip()
|
||||
paechter_id = request.POST.get("paechter_id", "").strip()
|
||||
verp_id = request.POST.get("verpachtung_id", "").strip()
|
||||
|
||||
if dest_id:
|
||||
try:
|
||||
dok.destinataer_id = dest_id
|
||||
except Exception:
|
||||
pass
|
||||
if land_id:
|
||||
try:
|
||||
dok.land_id = land_id
|
||||
except Exception:
|
||||
pass
|
||||
if paechter_id:
|
||||
try:
|
||||
dok.paechter_id = paechter_id
|
||||
except Exception:
|
||||
pass
|
||||
if verp_id:
|
||||
try:
|
||||
dok.verpachtung_id = verp_id
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_save_upload(request, dok)
|
||||
|
||||
if request.htmx:
|
||||
return render(request, "stiftung/dms/partials/upload_success.html", {"dok": dok})
|
||||
|
||||
messages.success(request, f'Dokument \u201e{dok.titel}\u201c erfolgreich hochgeladen.')
|
||||
return redirect("stiftung:dms_detail", pk=dok.pk)
|
||||
|
||||
# GET: zeige Upload-Formular
|
||||
destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||
laendereien = Land.objects.filter(aktiv=True).order_by("lfd_nr")
|
||||
paechter_qs = Paechter.objects.filter(aktiv=True).order_by("nachname")
|
||||
|
||||
context = {
|
||||
"initial": initial,
|
||||
"kontext_choices": DokumentDatei.KONTEXT_CHOICES,
|
||||
"destinataere": destinataere,
|
||||
"laendereien": laendereien,
|
||||
"paechter_qs": paechter_qs,
|
||||
}
|
||||
return render(request, "stiftung/dms/upload.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def dms_delete(request, pk):
|
||||
"""Löscht ein Dokument inkl. Datei."""
|
||||
dok = get_object_or_404(DokumentDatei, pk=pk)
|
||||
titel = dok.titel
|
||||
# Datei physisch löschen
|
||||
if dok.datei:
|
||||
try:
|
||||
dok.datei.delete(save=False)
|
||||
except Exception:
|
||||
pass
|
||||
dok.delete()
|
||||
messages.success(request, f'Dokument \u201e{titel}\u201c gel\u00f6scht.')
|
||||
|
||||
next_url = request.POST.get("next") or "stiftung:dms_list"
|
||||
if next_url.startswith("/"):
|
||||
return redirect(next_url)
|
||||
return redirect(next_url)
|
||||
|
||||
|
||||
@login_required
|
||||
def dms_search_api(request):
|
||||
"""HTMX-Suche: gibt gerendertes Partial mit Suchergebnissen zurück."""
|
||||
q = request.GET.get("q", "").strip()
|
||||
if not q:
|
||||
return render(request, "stiftung/dms/partials/search_results.html", {"results": []})
|
||||
|
||||
search_query = SearchQuery(q, config="german")
|
||||
results = (
|
||||
DokumentDatei.objects.annotate(rank=SearchRank("suchvektor", search_query))
|
||||
.filter(rank__gt=0.01)
|
||||
.select_related("destinataer", "land")
|
||||
.order_by("-rank")[:20]
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"stiftung/dms/partials/search_results.html",
|
||||
{"results": results, "q": q},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def dms_edit(request, pk):
|
||||
"""Bearbeite Metadaten eines Dokuments (kein Datei-Austausch)."""
|
||||
dok = get_object_or_404(DokumentDatei, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
dok.titel = request.POST.get("titel", dok.titel).strip()[:255]
|
||||
dok.beschreibung = request.POST.get("beschreibung", "").strip()
|
||||
dok.kontext = request.POST.get("kontext", dok.kontext)
|
||||
dok.save()
|
||||
dok.update_suchvektor()
|
||||
messages.success(request, "Metadaten gespeichert.")
|
||||
return redirect("stiftung:dms_detail", pk=dok.pk)
|
||||
|
||||
context = {
|
||||
"dok": dok,
|
||||
"kontext_choices": DokumentDatei.KONTEXT_CHOICES,
|
||||
}
|
||||
return render(request, "stiftung/dms/edit.html", context)
|
||||
Reference in New Issue
Block a user