# 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) 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)