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:
SysAdmin Agent
2026-03-11 11:10:08 +00:00
parent ee2c827d85
commit a79a0989d6
16 changed files with 1219 additions and 35 deletions

View File

@@ -0,0 +1,164 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Dokument hochladen Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="fas fa-upload text-primary me-2"></i>
Dokument hochladen
</h1>
<a href="{% url 'stiftung:dms_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück
</a>
</div>
<form method="post" enctype="multipart/form-data" id="upload-form">
{% csrf_token %}
<!-- Drag & Drop Zone -->
<div class="card shadow mb-4">
<div class="card-body">
<div id="drop-zone"
class="border border-2 border-dashed rounded p-5 text-center"
style="border-color: #ccc !important; cursor: pointer; transition: all 0.2s;"
onclick="document.getElementById('datei-input').click()">
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3 d-block"></i>
<p class="mb-1 fw-semibold">Datei hierher ziehen oder klicken zum Auswählen</p>
<p class="small text-muted mb-0">PDF, Word, Excel, Bilder — max. 50 MB</p>
<div id="file-preview" class="mt-3 d-none">
<span class="badge bg-success fs-6 px-3 py-2">
<i class="fas fa-file me-2"></i><span id="file-name"></span>
</span>
</div>
</div>
<input type="file" name="datei" id="datei-input" class="d-none" required>
</div>
</div>
<!-- Metadaten -->
<div class="card shadow mb-4">
<div class="card-header bg-dark text-white py-2">
<span class="small fw-bold"><i class="fas fa-tag me-2"></i>Metadaten</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-semibold">Titel</label>
<input type="text" name="titel" class="form-control"
placeholder="Wird automatisch aus Dateiname abgeleitet wenn leer">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Typ / Kontext</label>
<select name="kontext" class="form-select">
{% for code, label in kontext_choices %}
<option value="{{ code }}" {% if code == initial.kontext %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Beschreibung <span class="text-muted fw-normal">(optional)</span></label>
<textarea name="beschreibung" class="form-control" rows="2"
placeholder="Kurze Beschreibung des Dokuments"></textarea>
</div>
</div>
</div>
<!-- Zuordnung -->
<div class="card shadow mb-4">
<div class="card-header bg-dark text-white py-2">
<span class="small fw-bold"><i class="fas fa-link me-2"></i>Zuordnung <span class="fw-normal opacity-75">(optional)</span></span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-semibold">Destinatär</label>
<select name="destinataer_id" class="form-select form-select-sm">
<option value=""> keine </option>
{% for d in destinataere %}
<option value="{{ d.pk }}" {% if d.pk|stringformat:'s' == initial.destinataer_id %}selected{% endif %}>
{{ d.get_full_name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Länderei</label>
<select name="land_id" class="form-select form-select-sm">
<option value=""> keine </option>
{% for l in laendereien %}
<option value="{{ l.pk }}" {% if l.pk|stringformat:'s' == initial.land_id %}selected{% endif %}>
{{ l.lfd_nr }}{% if l.gemeinde %} {{ l.gemeinde }}{% endif %}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold">Pächter</label>
<select name="paechter_id" class="form-select form-select-sm">
<option value=""> keine </option>
{% for p in paechter_qs %}
<option value="{{ p.pk }}" {% if p.pk|stringformat:'s' == initial.paechter_id %}selected{% endif %}>
{{ p.get_full_name }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2 justify-content-end">
<a href="{% url 'stiftung:dms_list' %}" class="btn btn-outline-secondary">Abbrechen</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload me-2"></i>Hochladen
</button>
</div>
</form>
</div>
</div>
<script>
(function () {
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('datei-input');
const filePreview = document.getElementById('file-preview');
const fileName = document.getElementById('file-name');
function showFile(file) {
fileName.textContent = file.name;
filePreview.classList.remove('d-none');
dropZone.style.borderColor = '#198754 !important';
dropZone.classList.add('border-success');
}
fileInput.addEventListener('change', function () {
if (this.files[0]) showFile(this.files[0]);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
this.style.backgroundColor = '#f0f7ff';
this.style.borderColor = '#0d6efd';
});
dropZone.addEventListener('dragleave', function () {
this.style.backgroundColor = '';
this.style.borderColor = '#ccc';
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
this.style.backgroundColor = '';
this.style.borderColor = '#ccc';
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
showFile(files[0]);
}
});
})();
</script>
{% endblock %}