LandVerpachtung model uses pachtbeginn, not vertragsbeginn. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
281 lines
9.8 KiB
Python
281 lines
9.8 KiB
Python
# 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", ""),
|
||
"foerderung_id": request.GET.get("foerderung", ""),
|
||
"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()
|
||
foerd_id = request.POST.get("foerderung_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
|
||
if foerd_id:
|
||
try:
|
||
dok.foerderung_id = foerd_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)
|
||
|
||
# Entity assignments
|
||
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()
|
||
|
||
dok.destinataer_id = int(dest_id) if dest_id else None
|
||
dok.land_id = int(land_id) if land_id else None
|
||
dok.paechter_id = int(paechter_id) if paechter_id else None
|
||
dok.verpachtung_id = verp_id if verp_id else None
|
||
|
||
dok.save()
|
||
dok.update_suchvektor()
|
||
messages.success(request, "Metadaten gespeichert.")
|
||
return redirect("stiftung:dms_detail", pk=dok.pk)
|
||
|
||
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")
|
||
verpachtungen = LandVerpachtung.objects.select_related("land", "paechter").order_by("-pachtbeginn")[:50]
|
||
|
||
context = {
|
||
"dok": dok,
|
||
"kontext_choices": DokumentDatei.KONTEXT_CHOICES,
|
||
"destinataere": destinataere,
|
||
"laendereien": laendereien,
|
||
"paechter_qs": paechter_qs,
|
||
"verpachtungen": verpachtungen,
|
||
}
|
||
return render(request, "stiftung/dms/edit.html", context)
|