Dokumentenmanagement
This commit is contained in:
parent
70ece97587
commit
c2c4ae3cbe
14 changed files with 759 additions and 13 deletions
82
vergabe_teilnahme/apps/dokumente/forms.py
Normal file
82
vergabe_teilnahme/apps/dokumente/forms.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from vergabe_teilnahme.apps.lose.models import Los
|
||||||
|
|
||||||
|
from .models import ALLOWED_EXTENSIONS, Dokument
|
||||||
|
|
||||||
|
|
||||||
|
class DokumentForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Dokument
|
||||||
|
fields = ['datei', 'kategorie', 'version', 'quelle', 'verantwortlicher', 'pruefer', 'los', 'beschreibung']
|
||||||
|
widgets = {
|
||||||
|
'datei': forms.FileInput(attrs={'class': 'form-input'}),
|
||||||
|
'kategorie': forms.Select(attrs={'class': 'form-input'}),
|
||||||
|
'version': forms.TextInput(attrs={'class': 'form-input'}),
|
||||||
|
'quelle': forms.TextInput(attrs={'class': 'form-input'}),
|
||||||
|
'verantwortlicher': forms.Select(attrs={'class': 'form-input'}),
|
||||||
|
'pruefer': forms.Select(attrs={'class': 'form-input'}),
|
||||||
|
'los': forms.Select(attrs={'class': 'form-input'}),
|
||||||
|
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, ausschreibung=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if ausschreibung is not None:
|
||||||
|
self.fields['los'].queryset = Los.objects.filter(ausschreibung=ausschreibung)
|
||||||
|
self.fields['quelle'].required = False
|
||||||
|
self.fields['verantwortlicher'].required = False
|
||||||
|
self.fields['pruefer'].required = False
|
||||||
|
self.fields['los'].required = False
|
||||||
|
self.fields['beschreibung'].required = False
|
||||||
|
|
||||||
|
def clean_datei(self):
|
||||||
|
datei = self.cleaned_data.get('datei')
|
||||||
|
if datei:
|
||||||
|
ext = os.path.splitext(datei.name)[1].lower()
|
||||||
|
if ext not in ALLOWED_EXTENSIONS:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
f'Dateityp "{ext}" nicht erlaubt. Erlaubt: {", ".join(sorted(ALLOWED_EXTENSIONS))}'
|
||||||
|
)
|
||||||
|
max_size = getattr(settings, 'MAX_UPLOAD_SIZE', 52428800)
|
||||||
|
if hasattr(datei, 'size') and datei.size > max_size:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
f'Datei zu groß. Maximum: {max_size // 1024 // 1024} MB'
|
||||||
|
)
|
||||||
|
return datei
|
||||||
|
|
||||||
|
|
||||||
|
class DokumentVersionForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Dokument
|
||||||
|
fields = ['datei', 'version']
|
||||||
|
widgets = {
|
||||||
|
'datei': forms.FileInput(attrs={'class': 'form-input'}),
|
||||||
|
'version': forms.TextInput(attrs={'class': 'form-input'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def clean_datei(self):
|
||||||
|
datei = self.cleaned_data.get('datei')
|
||||||
|
if datei:
|
||||||
|
ext = os.path.splitext(datei.name)[1].lower()
|
||||||
|
if ext not in ALLOWED_EXTENSIONS:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
f'Dateityp "{ext}" nicht erlaubt. Erlaubt: {", ".join(sorted(ALLOWED_EXTENSIONS))}'
|
||||||
|
)
|
||||||
|
max_size = getattr(settings, 'MAX_UPLOAD_SIZE', 52428800)
|
||||||
|
if hasattr(datei, 'size') and datei.size > max_size:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
f'Datei zu groß. Maximum: {max_size // 1024 // 1024} MB'
|
||||||
|
)
|
||||||
|
return datei
|
||||||
|
|
||||||
|
|
||||||
|
def naechste_version(alte_version: str) -> str:
|
||||||
|
try:
|
||||||
|
major = int(alte_version.split('.')[0])
|
||||||
|
return f'{major + 1}.0'
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return '2.0'
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 6.0.5 on 2026-05-08 16:27
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bibliothek', '0001_initial'),
|
||||||
|
('dokumente', '0002_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='dokument',
|
||||||
|
name='bibliothek_nachweis',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='verwendete_dokumente', to='bibliothek.nachweis'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='dokument',
|
||||||
|
name='quelle',
|
||||||
|
field=models.CharField(blank=True, max_length=300),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dokument',
|
||||||
|
name='datei',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='dokumente/%Y/%m/'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -39,8 +39,13 @@ class Dokument(FlexibleModel):
|
||||||
'lose.Los', on_delete=models.SET_NULL,
|
'lose.Los', on_delete=models.SET_NULL,
|
||||||
null=True, blank=True, related_name='dokumente'
|
null=True, blank=True, related_name='dokumente'
|
||||||
)
|
)
|
||||||
datei = models.FileField(upload_to='dokumente/%Y/%m/')
|
datei = models.FileField(upload_to='dokumente/%Y/%m/', null=True, blank=True)
|
||||||
dateiname = models.CharField(max_length=300, blank=True)
|
dateiname = models.CharField(max_length=300, blank=True)
|
||||||
|
quelle = models.CharField(max_length=300, blank=True)
|
||||||
|
bibliothek_nachweis = models.ForeignKey(
|
||||||
|
'bibliothek.Nachweis', on_delete=models.SET_NULL,
|
||||||
|
null=True, blank=True, related_name='verwendete_dokumente'
|
||||||
|
)
|
||||||
kategorie = models.CharField(max_length=30, choices=KATEGORIE_CHOICES, default='sonstiges')
|
kategorie = models.CharField(max_length=30, choices=KATEGORIE_CHOICES, default='sonstiges')
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='entwurf')
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='entwurf')
|
||||||
version = models.CharField(max_length=20, default='1.0')
|
version = models.CharField(max_length=20, default='1.0')
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,85 @@
|
||||||
from django.test import TestCase
|
import io
|
||||||
|
|
||||||
# Create your tests here.
|
import factory
|
||||||
|
import pytest
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
|
||||||
|
|
||||||
|
from .models import Dokument
|
||||||
|
|
||||||
|
|
||||||
|
class DokumentFactory(factory.django.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = Dokument
|
||||||
|
|
||||||
|
ausschreibung = factory.SubFactory(AusschreibungFactory)
|
||||||
|
dateiname = factory.Sequence(lambda n: f'dokument_{n}.pdf')
|
||||||
|
kategorie = 'intern'
|
||||||
|
status = 'entwurf'
|
||||||
|
version = '1.0'
|
||||||
|
|
||||||
|
|
||||||
|
def _pdf_file(name='test.pdf'):
|
||||||
|
return SimpleUploadedFile(name, b'%PDF-1.4 fake content', content_type='application/pdf')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_dokument_upload_valid(client, tmp_path, settings):
|
||||||
|
settings.MEDIA_ROOT = tmp_path
|
||||||
|
a = AusschreibungFactory()
|
||||||
|
url = reverse('ausschreibungen:dokumente:upload', kwargs={'ausschreibung_id': a.pk})
|
||||||
|
response = client.post(url, {'datei': _pdf_file(), 'kategorie': 'intern', 'version': '1.0'})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert Dokument.objects.filter(ausschreibung=a).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_dokument_upload_invalid_extension(client, tmp_path, settings):
|
||||||
|
settings.MEDIA_ROOT = tmp_path
|
||||||
|
a = AusschreibungFactory()
|
||||||
|
url = reverse('ausschreibungen:dokumente:upload', kwargs={'ausschreibung_id': a.pk})
|
||||||
|
bad_file = SimpleUploadedFile('malware.exe', b'MZ bad', content_type='application/octet-stream')
|
||||||
|
response = client.post(url, {'datei': bad_file, 'kategorie': 'intern', 'version': '1.0'})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert not Dokument.objects.filter(ausschreibung=a).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_dokument_upload_too_large(client, tmp_path, settings):
|
||||||
|
settings.MEDIA_ROOT = tmp_path
|
||||||
|
settings.MAX_UPLOAD_SIZE = 10
|
||||||
|
a = AusschreibungFactory()
|
||||||
|
url = reverse('ausschreibungen:dokumente:upload', kwargs={'ausschreibung_id': a.pk})
|
||||||
|
big_file = SimpleUploadedFile('big.pdf', b'%PDF' + b'x' * 100, content_type='application/pdf')
|
||||||
|
response = client.post(url, {'datei': big_file, 'kategorie': 'intern', 'version': '1.0'})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert not Dokument.objects.filter(ausschreibung=a).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_dokument_neue_version(client, tmp_path, settings):
|
||||||
|
settings.MEDIA_ROOT = tmp_path
|
||||||
|
altes_dok = DokumentFactory(version='1.0')
|
||||||
|
a = altes_dok.ausschreibung
|
||||||
|
url = reverse('ausschreibungen:dokumente:neue_version',
|
||||||
|
kwargs={'ausschreibung_id': a.pk, 'pk': altes_dok.pk})
|
||||||
|
response = client.post(url, {'datei': _pdf_file('v2.pdf'), 'version': '2.0'})
|
||||||
|
assert response.status_code == 302
|
||||||
|
altes_dok.refresh_from_db()
|
||||||
|
assert altes_dok.status == 'ersetzt'
|
||||||
|
assert Dokument.objects.filter(ausschreibung=a, version='2.0').exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_dokument_finale_version(client):
|
||||||
|
dok = DokumentFactory(status='freigegeben')
|
||||||
|
a = dok.ausschreibung
|
||||||
|
url = reverse('ausschreibungen:dokumente:finale_version',
|
||||||
|
kwargs={'ausschreibung_id': a.pk, 'pk': dok.pk})
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
dok.refresh_from_db()
|
||||||
|
assert dok.finale_abgabeversion is True
|
||||||
|
assert dok.status == 'final_abgegeben'
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,15 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
urlpatterns = []
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'dokumente'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.dokumente_liste, name='liste'),
|
||||||
|
path('hochladen/', views.dokument_upload, name='upload'),
|
||||||
|
path('<int:pk>/', views.dokument_detail, name='detail'),
|
||||||
|
path('<int:pk>/version/', views.dokument_neue_version, name='neue_version'),
|
||||||
|
path('<int:pk>/status/', views.dokument_status, name='status'),
|
||||||
|
path('<int:pk>/final/', views.dokument_finale_version, name='finale_version'),
|
||||||
|
path('<int:pk>/bibliothek/', views.dokument_bibliothek_zuordnen, name='bibliothek_zuordnen'),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,169 @@
|
||||||
from django.shortcuts import render
|
import os
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
# Create your views here.
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
|
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||||
|
from vergabe_teilnahme.apps.bibliothek.models import Nachweis
|
||||||
|
from vergabe_teilnahme.apps.core.models import Freigabe
|
||||||
|
|
||||||
|
from .forms import DokumentForm, DokumentVersionForm, naechste_version
|
||||||
|
from .models import Dokument
|
||||||
|
|
||||||
|
STATUS_WORKFLOW = [
|
||||||
|
'entwurf',
|
||||||
|
'in_pruefung',
|
||||||
|
'freigegeben',
|
||||||
|
'final_abgegeben',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ausschreibung(ausschreibung_id):
|
||||||
|
return get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||||
|
|
||||||
|
|
||||||
|
def dokumente_liste(request, ausschreibung_id):
|
||||||
|
ausschreibung = _get_ausschreibung(ausschreibung_id)
|
||||||
|
qs = Dokument.objects.filter(ausschreibung=ausschreibung).select_related('verantwortlicher', 'pruefer', 'los')
|
||||||
|
|
||||||
|
status_filter = request.GET.get('status', '')
|
||||||
|
kategorie_filter = request.GET.get('kategorie', '')
|
||||||
|
verantwortlicher_filter = request.GET.get('verantwortlicher', '')
|
||||||
|
|
||||||
|
if status_filter:
|
||||||
|
qs = qs.filter(status=status_filter)
|
||||||
|
if kategorie_filter:
|
||||||
|
qs = qs.filter(kategorie=kategorie_filter)
|
||||||
|
if verantwortlicher_filter:
|
||||||
|
qs = qs.filter(verantwortlicher_id=verantwortlicher_filter)
|
||||||
|
|
||||||
|
grouped = {}
|
||||||
|
for dok in qs:
|
||||||
|
grouped.setdefault(dok.get_kategorie_display(), []).append(dok)
|
||||||
|
|
||||||
|
return render(request, 'dokumente/liste.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'grouped_dokumente': grouped,
|
||||||
|
'status_choices': Dokument.STATUS_CHOICES,
|
||||||
|
'kategorie_choices': Dokument.KATEGORIE_CHOICES,
|
||||||
|
'current_status': status_filter,
|
||||||
|
'current_kategorie': kategorie_filter,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def dokument_upload(request, ausschreibung_id):
|
||||||
|
ausschreibung = _get_ausschreibung(ausschreibung_id)
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = DokumentForm(request.POST, request.FILES, ausschreibung=ausschreibung)
|
||||||
|
if form.is_valid():
|
||||||
|
dok = form.save(commit=False)
|
||||||
|
dok.ausschreibung = ausschreibung
|
||||||
|
if dok.datei:
|
||||||
|
dok.dateiname = os.path.basename(dok.datei.name)
|
||||||
|
dok.save()
|
||||||
|
return redirect('ausschreibungen:dokumente:liste', ausschreibung_id=ausschreibung_id)
|
||||||
|
else:
|
||||||
|
form = DokumentForm(ausschreibung=ausschreibung)
|
||||||
|
return render(request, 'dokumente/form.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'form': form,
|
||||||
|
'titel': 'Dokument hochladen',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def dokument_detail(request, ausschreibung_id, pk):
|
||||||
|
ausschreibung = _get_ausschreibung(ausschreibung_id)
|
||||||
|
dok = get_object_or_404(Dokument, pk=pk, ausschreibung=ausschreibung)
|
||||||
|
ct = ContentType.objects.get_for_model(Dokument)
|
||||||
|
freigaben = Freigabe.objects.filter(content_type=ct, object_id=dok.pk)
|
||||||
|
versionen = Dokument.objects.filter(
|
||||||
|
ausschreibung=ausschreibung,
|
||||||
|
kategorie=dok.kategorie,
|
||||||
|
dateiname=dok.dateiname,
|
||||||
|
).order_by('version')
|
||||||
|
return render(request, 'dokumente/detail.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'dokument': dok,
|
||||||
|
'freigaben': freigaben,
|
||||||
|
'versionen': versionen,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def dokument_neue_version(request, ausschreibung_id, pk):
|
||||||
|
ausschreibung = _get_ausschreibung(ausschreibung_id)
|
||||||
|
altes_dokument = get_object_or_404(Dokument, pk=pk, ausschreibung=ausschreibung)
|
||||||
|
initial_version = naechste_version(altes_dokument.version)
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = DokumentVersionForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
neues_dok = form.save(commit=False)
|
||||||
|
neues_dok.ausschreibung = altes_dokument.ausschreibung
|
||||||
|
neues_dok.los = altes_dokument.los
|
||||||
|
neues_dok.kategorie = altes_dokument.kategorie
|
||||||
|
neues_dok.verantwortlicher = altes_dokument.verantwortlicher
|
||||||
|
if neues_dok.datei:
|
||||||
|
neues_dok.dateiname = os.path.basename(neues_dok.datei.name)
|
||||||
|
neues_dok.save()
|
||||||
|
altes_dokument.status = 'ersetzt'
|
||||||
|
altes_dokument.save(update_fields=['status'])
|
||||||
|
return redirect('ausschreibungen:dokumente:detail', ausschreibung_id=ausschreibung_id, pk=neues_dok.pk)
|
||||||
|
else:
|
||||||
|
form = DokumentVersionForm(initial={'version': initial_version})
|
||||||
|
return render(request, 'dokumente/neue_version.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'form': form,
|
||||||
|
'dokument': altes_dokument,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def dokument_status(request, ausschreibung_id, pk):
|
||||||
|
ausschreibung = _get_ausschreibung(ausschreibung_id)
|
||||||
|
dok = get_object_or_404(Dokument, pk=pk, ausschreibung=ausschreibung)
|
||||||
|
if request.method == 'POST' and dok.status != 'final_abgegeben':
|
||||||
|
neuer_status = request.POST.get('status', '')
|
||||||
|
valid = [s for s, _ in Dokument.STATUS_CHOICES if s not in ('ersetzt', 'archiviert')]
|
||||||
|
if neuer_status in valid:
|
||||||
|
dok.status = neuer_status
|
||||||
|
if neuer_status == 'final_abgegeben':
|
||||||
|
dok.finale_abgabeversion = True
|
||||||
|
dok.save(update_fields=['status', 'finale_abgabeversion'])
|
||||||
|
return render(request, 'dokumente/partials/status_widget.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'dokument': dok,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def dokument_finale_version(request, ausschreibung_id, pk):
|
||||||
|
ausschreibung = _get_ausschreibung(ausschreibung_id)
|
||||||
|
dok = get_object_or_404(Dokument, pk=pk, ausschreibung=ausschreibung)
|
||||||
|
if request.method == 'POST':
|
||||||
|
dok.finale_abgabeversion = True
|
||||||
|
dok.status = 'final_abgegeben'
|
||||||
|
dok.save(update_fields=['finale_abgabeversion', 'status'])
|
||||||
|
return render(request, 'dokumente/partials/finaler_status_badge.html', {
|
||||||
|
'dokument': dok,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def dokument_bibliothek_zuordnen(request, ausschreibung_id, pk):
|
||||||
|
ausschreibung = _get_ausschreibung(ausschreibung_id)
|
||||||
|
dok = get_object_or_404(Dokument, pk=pk, ausschreibung=ausschreibung)
|
||||||
|
q = request.GET.get('q', '').strip()
|
||||||
|
nachweise = []
|
||||||
|
if q:
|
||||||
|
nachweise = Nachweis.objects.filter(titel__icontains=q)
|
||||||
|
if request.method == 'POST':
|
||||||
|
nachweis_id = request.POST.get('nachweis_id')
|
||||||
|
nachweis = get_object_or_404(Nachweis, pk=nachweis_id)
|
||||||
|
dok.bibliothek_nachweis = nachweis
|
||||||
|
dok.quelle = f'Bibliothek: {nachweis.titel}'
|
||||||
|
dok.dateiname = nachweis.titel
|
||||||
|
dok.save(update_fields=['bibliothek_nachweis', 'quelle', 'dateiname'])
|
||||||
|
return redirect('ausschreibungen:dokumente:detail', ausschreibung_id=ausschreibung_id, pk=dok.pk)
|
||||||
|
return render(request, 'dokumente/bibliothek_modal.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'dokument': dok,
|
||||||
|
'nachweise': nachweise,
|
||||||
|
'q': q,
|
||||||
|
})
|
||||||
|
|
|
||||||
46
vergabe_teilnahme/templates/dokumente/bibliothek_modal.html
Normal file
46
vergabe_teilnahme/templates/dokumente/bibliothek_modal.html
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Aus Bibliothek zuordnen{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<h1 class="page-title mb-2">Nachweis aus Bibliothek zuordnen</h1>
|
||||||
|
<p class="text-sm text-slate-500 mb-6">Dokument: <span class="font-medium">{{ dokument.dateiname|default:"—" }}</span></p>
|
||||||
|
|
||||||
|
<form method="get" class="card mb-4 flex gap-3 items-end">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="form-label">Suche</label>
|
||||||
|
<input type="text" name="q" value="{{ q }}" placeholder="Nachweis-Titel…" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Suchen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if nachweise %}
|
||||||
|
<div class="card divide-y divide-slate-100">
|
||||||
|
{% for nw in nachweise %}
|
||||||
|
<form method="post" class="flex items-start justify-between py-3 gap-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="nachweis_id" value="{{ nw.pk }}">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-slate-800">{{ nw.titel }}</p>
|
||||||
|
<p class="text-xs text-slate-500">{{ nw.kategorie }}{% if nw.version %} · v{{ nw.version }}{% endif %}{% if nw.gueltig_bis %} · Gültig bis {{ nw.gueltig_bis|date:"d.m.Y" }}{% endif %}</p>
|
||||||
|
{% if nw.ist_abgelaufen %}
|
||||||
|
<p class="text-xs text-red-600 font-medium mt-0.5">Abgelaufen</p>
|
||||||
|
{% elif nw.gueltig_bis %}
|
||||||
|
{% now "Y-m-d" as today_str %}
|
||||||
|
<p class="text-xs text-yellow-600 mt-0.5">Bald ablaufend</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-secondary text-xs shrink-0">Zuordnen</button>
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% elif q %}
|
||||||
|
<p class="text-sm text-slate-500 text-center py-6">Keine Nachweise für "{{ q }}" gefunden.</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-sm text-slate-500 text-center py-6">Suchbegriff eingeben, um Nachweise zu finden.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:detail' ausschreibung.pk dokument.pk %}" class="btn-ghost text-xs">← Zurück</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
122
vergabe_teilnahme/templates/dokumente/detail.html
Normal file
122
vergabe_teilnahme/templates/dokumente/detail.html
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load vergabe_tags %}
|
||||||
|
{% block title %}{{ dokument.dateiname }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h1 class="page-title">{{ dokument.dateiname|default:"Dokument" }}</h1>
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:liste' ausschreibung.pk %}" class="btn-ghost text-xs">← Übersicht</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div class="col-span-2 space-y-4">
|
||||||
|
|
||||||
|
<div class="card space-y-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{% status_badge dokument.status dokument.get_status_display %}
|
||||||
|
{% if dokument.finale_abgabeversion %}
|
||||||
|
<span class="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded font-medium">Finale Abgabeversion</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="text-xs text-slate-500">v{{ dokument.version }}</span>
|
||||||
|
</div>
|
||||||
|
{% if dokument.datei %}
|
||||||
|
<a href="{{ dokument.datei.url }}" target="_blank" class="btn-primary text-xs inline-block">↓ Herunterladen</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if dokument.beschreibung %}
|
||||||
|
<p class="text-sm text-slate-700 whitespace-pre-wrap">{{ dokument.beschreibung }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if versionen.count > 1 %}
|
||||||
|
<div class="card">
|
||||||
|
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Versionshistorie</p>
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-slate-200 text-left text-xs text-slate-500">
|
||||||
|
<th class="pb-2 pr-4">Version</th>
|
||||||
|
<th class="pb-2 pr-4">Status</th>
|
||||||
|
<th class="pb-2">Datum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
{% for v in versionen %}
|
||||||
|
<tr class="{% if v.pk == dokument.pk %}bg-blue-50{% else %}hover:bg-slate-50{% endif %}">
|
||||||
|
<td class="py-2 pr-4">
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:detail' ausschreibung.pk v.pk %}"
|
||||||
|
class="{% if v.pk == dokument.pk %}font-semibold text-blue-700{% else %}text-slate-700 hover:text-blue-600{% endif %}">
|
||||||
|
v{{ v.version }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 pr-4">{% status_badge v.status v.get_status_display %}</td>
|
||||||
|
<td class="py-2 text-xs text-slate-500">{{ v.upload_datum|date:"d.m.Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if freigaben %}
|
||||||
|
<div class="card">
|
||||||
|
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Freigaben</p>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{% for fg in freigaben %}
|
||||||
|
<li class="text-sm text-slate-700">
|
||||||
|
{% status_badge fg.status fg.get_status_display %}
|
||||||
|
<span class="ml-2 text-slate-500">{{ fg.freigebender }}</span>
|
||||||
|
{% if fg.kommentar %}<span class="ml-2 text-slate-400 text-xs">— {{ fg.kommentar }}</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<div class="card space-y-2">
|
||||||
|
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Details</p>
|
||||||
|
<p class="text-xs text-slate-600">Kategorie: <span class="font-medium">{{ dokument.get_kategorie_display }}</span></p>
|
||||||
|
<p class="text-xs text-slate-600">Hochgeladen: <span class="font-medium">{{ dokument.upload_datum|date:"d.m.Y H:i" }}</span></p>
|
||||||
|
{% if dokument.verantwortlicher %}
|
||||||
|
<p class="text-xs text-slate-600">Verantwortlicher: {{ dokument.verantwortlicher }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if dokument.pruefer %}
|
||||||
|
<p class="text-xs text-slate-600">Prüfer: {{ dokument.pruefer }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if dokument.los %}
|
||||||
|
<p class="text-xs text-slate-600">Los: {{ dokument.los }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if dokument.quelle %}
|
||||||
|
<p class="text-xs text-slate-600">Quelle: {{ dokument.quelle }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if dokument.bibliothek_nachweis %}
|
||||||
|
<div class="border-t border-slate-100 pt-2 mt-2">
|
||||||
|
<p class="text-xs text-slate-500 mb-1">Aus Bibliothek</p>
|
||||||
|
<p class="text-xs font-medium text-slate-700">{{ dokument.bibliothek_nachweis.titel }}</p>
|
||||||
|
{% if dokument.bibliothek_nachweis.ist_abgelaufen %}
|
||||||
|
<p class="text-xs text-red-600 mt-1">Nachweis abgelaufen!</p>
|
||||||
|
{% elif dokument.bibliothek_nachweis.gueltig_bis %}
|
||||||
|
<p class="text-xs text-slate-500">Gültig bis: {{ dokument.bibliothek_nachweis.gueltig_bis|date:"d.m.Y" }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card space-y-2">
|
||||||
|
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Aktionen</p>
|
||||||
|
{% if not dokument.finale_abgabeversion %}
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:neue_version' ausschreibung.pk dokument.pk %}" class="btn-secondary text-xs w-full block text-center">Neue Version hochladen</a>
|
||||||
|
<form method="post" action="{% url 'ausschreibungen:dokumente:finale_version' ausschreibung.pk dokument.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn-primary text-xs w-full mt-2">Als finale Abgabeversion kennzeichnen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:bibliothek_zuordnen' ausschreibung.pk dokument.pk %}" class="btn-ghost text-xs w-full block text-center mt-1">Aus Bibliothek zuordnen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
68
vergabe_teilnahme/templates/dokumente/form.html
Normal file
68
vergabe_teilnahme/templates/dokumente/form.html
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ titel }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<h1 class="page-title mb-6">{{ titel }}</h1>
|
||||||
|
<form method="post" enctype="multipart/form-data" class="space-y-4"
|
||||||
|
x-data="{ files: [] }">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Datei</label>
|
||||||
|
<input type="file" name="datei"
|
||||||
|
accept=".pdf,.docx,.xlsx,.zip,.png,.jpg,.jpeg"
|
||||||
|
class="form-input"
|
||||||
|
@change="files = Array.from($event.target.files).map(f => f.name)">
|
||||||
|
<template x-if="files.length">
|
||||||
|
<ul class="mt-1 space-y-0.5">
|
||||||
|
<template x-for="name in files" :key="name">
|
||||||
|
<li class="text-xs text-slate-600" x-text="name"></li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
{% if form.datei.errors %}<p class="text-xs text-red-600 mt-1">{{ form.datei.errors.0 }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Kategorie</label>
|
||||||
|
{{ form.kategorie }}
|
||||||
|
{% if form.kategorie.errors %}<p class="text-xs text-red-600 mt-1">{{ form.kategorie.errors.0 }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Version</label>
|
||||||
|
{{ form.version }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Quelle (optional)</label>
|
||||||
|
{{ form.quelle }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Beschreibung (optional)</label>
|
||||||
|
{{ form.beschreibung }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card space-y-4">
|
||||||
|
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Zuständigkeiten (optional)</p>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Verantwortlicher</label>
|
||||||
|
{{ form.verantwortlicher }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Prüfer</label>
|
||||||
|
{{ form.pruefer }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Los</label>
|
||||||
|
{{ form.los }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="submit" class="btn-primary">Hochladen</button>
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:liste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
85
vergabe_teilnahme/templates/dokumente/liste.html
Normal file
85
vergabe_teilnahme/templates/dokumente/liste.html
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load vergabe_tags %}
|
||||||
|
{% block title %}Dokumente — {{ ausschreibung.titel }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h1 class="page-title">Dokumente</h1>
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:upload' ausschreibung.pk %}" class="btn-primary text-xs">+ Hochladen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="card mb-4 flex flex-wrap gap-3 items-end">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Status</label>
|
||||||
|
<select name="status" class="form-input text-xs" onchange="this.form.submit()">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
{% for val, label in status_choices %}
|
||||||
|
<option value="{{ val }}"{% if current_status == val %} selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Kategorie</label>
|
||||||
|
<select name="kategorie" class="form-input text-xs" onchange="this.form.submit()">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
{% for val, label in kategorie_choices %}
|
||||||
|
<option value="{{ val }}"{% if current_kategorie == val %} selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if grouped_dokumente %}
|
||||||
|
{% for kategorie_label, dokumente in grouped_dokumente.items %}
|
||||||
|
<div class="mb-4" x-data="{ open: true }">
|
||||||
|
<button @click="open = !open"
|
||||||
|
class="w-full flex items-center justify-between card py-2 px-4 text-left text-sm font-semibold text-slate-700 hover:bg-slate-50">
|
||||||
|
<span>{{ kategorie_label }} <span class="text-xs font-normal text-slate-400">({{ dokumente|length }})</span></span>
|
||||||
|
<span x-text="open ? '▲' : '▼'" class="text-xs text-slate-400"></span>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" class="card mt-0 rounded-t-none border-t-0">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-slate-200 text-left text-xs text-slate-500">
|
||||||
|
<th class="pb-2 pr-4">Datei</th>
|
||||||
|
<th class="pb-2 pr-4">Version</th>
|
||||||
|
<th class="pb-2 pr-4">Status</th>
|
||||||
|
<th class="pb-2 pr-4">Verantwortlicher</th>
|
||||||
|
<th class="pb-2 pr-4">Prüfer</th>
|
||||||
|
<th class="pb-2">Datum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
{% for dok in dokumente %}
|
||||||
|
<tr class="hover:bg-slate-50{% if dok.finale_abgabeversion %} bg-green-50{% endif %}">
|
||||||
|
<td class="py-2 pr-4">
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:detail' ausschreibung.pk dok.pk %}"
|
||||||
|
class="font-medium text-slate-800 hover:text-blue-600">
|
||||||
|
{{ dok.dateiname|default:dok.pk }}
|
||||||
|
</a>
|
||||||
|
{% if dok.finale_abgabeversion %}
|
||||||
|
<span class="ml-1 inline-block text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded">Final</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if dok.quelle %}
|
||||||
|
<p class="text-xs text-slate-400">{{ dok.quelle }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 pr-4 text-xs text-slate-600">{{ dok.version }}</td>
|
||||||
|
<td class="py-2 pr-4" id="status-widget-{{ dok.pk }}">
|
||||||
|
{% include "dokumente/partials/status_widget.html" with dokument=dok %}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 pr-4 text-xs text-slate-600">{{ dok.verantwortlicher|default:"—" }}</td>
|
||||||
|
<td class="py-2 pr-4 text-xs text-slate-600">{{ dok.pruefer|default:"—" }}</td>
|
||||||
|
<td class="py-2 text-xs text-slate-500">{{ dok.upload_datum|date:"d.m.Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="card text-center py-10 text-slate-500 text-sm">Noch keine Dokumente hochgeladen.</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
26
vergabe_teilnahme/templates/dokumente/neue_version.html
Normal file
26
vergabe_teilnahme/templates/dokumente/neue_version.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Neue Version — {{ dokument.dateiname }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-xl mx-auto">
|
||||||
|
<h1 class="page-title mb-2">Neue Version hochladen</h1>
|
||||||
|
<p class="text-sm text-slate-500 mb-6">Ersetzt: <span class="font-medium">{{ dokument.dateiname }}</span> v{{ dokument.version }}</p>
|
||||||
|
<form method="post" enctype="multipart/form-data" class="space-y-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Neue Datei</label>
|
||||||
|
{{ form.datei }}
|
||||||
|
{% if form.datei.errors %}<p class="text-xs text-red-600 mt-1">{{ form.datei.errors.0 }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Versionsnummer</label>
|
||||||
|
{{ form.version }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="submit" class="btn-primary">Version hochladen</button>
|
||||||
|
<a href="{% url 'ausschreibungen:dokumente:detail' ausschreibung.pk dokument.pk %}" class="btn-ghost">Abbrechen</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% load vergabe_tags %}
|
||||||
|
<span class="inline-flex items-center gap-1">
|
||||||
|
{% status_badge dokument.status dokument.get_status_display %}
|
||||||
|
<span class="text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded font-medium">Final ✓</span>
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% load vergabe_tags %}
|
||||||
|
{% if dokument.status == 'final_abgegeben' or dokument.status == 'ersetzt' or dokument.status == 'archiviert' %}
|
||||||
|
{% status_badge dokument.status dokument.get_status_display %}
|
||||||
|
{% else %}
|
||||||
|
<select name="status"
|
||||||
|
hx-post="{% url 'ausschreibungen:dokumente:status' ausschreibung.pk dokument.pk %}"
|
||||||
|
hx-target="#status-widget-{{ dokument.pk }}"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="form-input text-xs">
|
||||||
|
{% for val, label in dokument.STATUS_CHOICES %}
|
||||||
|
{% if val not in 'ersetzt,archiviert' %}
|
||||||
|
<option value="{{ val }}"{% if val == dokument.status %} selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: WP-0007
|
id: WP-0007
|
||||||
title: Dokumentenmanagement
|
title: Dokumentenmanagement
|
||||||
status: todo
|
status: done
|
||||||
phase: 7-of-12
|
phase: 7-of-12
|
||||||
created: "2026-05-08"
|
created: "2026-05-08"
|
||||||
depends_on: WP-0006
|
depends_on: WP-0006
|
||||||
|
|
@ -17,7 +17,7 @@ für alle Dokumente. Referenz: UC-DO-01 bis UC-DO-05.
|
||||||
```task
|
```task
|
||||||
id: WP-0007-T01
|
id: WP-0007-T01
|
||||||
title: Dokument-Upload und Kategorisierung (UC-DO-01)
|
title: Dokument-Upload und Kategorisierung (UC-DO-01)
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`dokumente/views.py` — dokument_upload:
|
`dokumente/views.py` — dokument_upload:
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ gewählten Dateien (Dateinamen-Liste). Für jede Datei eigenes Formular-Submit
|
||||||
```task
|
```task
|
||||||
id: WP-0007-T02
|
id: WP-0007-T02
|
||||||
title: Dokumentenliste und Dokumentdetail
|
title: Dokumentenliste und Dokumentdetail
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`dokumente/views.py` — dokumente_liste:
|
`dokumente/views.py` — dokumente_liste:
|
||||||
Zeigt alle Dokumente einer Ausschreibung, gruppiert nach Kategorie.
|
Zeigt alle Dokumente einer Ausschreibung, gruppiert nach Kategorie.
|
||||||
|
|
@ -60,7 +60,7 @@ Template `dokumente/liste.html`:
|
||||||
```task
|
```task
|
||||||
id: WP-0007-T03
|
id: WP-0007-T03
|
||||||
title: Neue Dokumentversion hochladen (UC-DO-02)
|
title: Neue Dokumentversion hochladen (UC-DO-02)
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`dokument_neue_version (POST)`:
|
`dokument_neue_version (POST)`:
|
||||||
```python
|
```python
|
||||||
|
|
@ -87,7 +87,7 @@ Logik `naechste_version(alte_version_str)`: "1.0" → "2.0", "2.3" → "3.0" (Ma
|
||||||
```task
|
```task
|
||||||
id: WP-0007-T04
|
id: WP-0007-T04
|
||||||
title: Dokumentstatus-Workflow und finale Abgabeversion (UC-DO-03, UC-DO-04)
|
title: Dokumentstatus-Workflow und finale Abgabeversion (UC-DO-03, UC-DO-04)
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
**Status-Workflow** — HTMX-Widget analog zum Aufgaben-Status.
|
**Status-Workflow** — HTMX-Widget analog zum Aufgaben-Status.
|
||||||
Statusübergänge: hochgeladen → zu_pruefen → in_bearbeitung → geprueft → freigegeben → final_abgegeben.
|
Statusübergänge: hochgeladen → zu_pruefen → in_bearbeitung → geprueft → freigegeben → final_abgegeben.
|
||||||
|
|
@ -114,7 +114,7 @@ Nach Kennzeichnung erscheint grüner "Final" Badge; weitere Uploads zu dieser Ve
|
||||||
```task
|
```task
|
||||||
id: WP-0007-T05
|
id: WP-0007-T05
|
||||||
title: Standarddokument aus Bibliothek zuordnen (UC-DO-05)
|
title: Standarddokument aus Bibliothek zuordnen (UC-DO-05)
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`dokument_bibliothek_zuordnen`:
|
`dokument_bibliothek_zuordnen`:
|
||||||
HTMX-Modal mit Suchfeld. Suche in `bibliothek.Nachweis` und Bibliothek-Dokumente.
|
HTMX-Modal mit Suchfeld. Suche in `bibliothek.Nachweis` und Bibliothek-Dokumente.
|
||||||
|
|
@ -132,7 +132,7 @@ Ablaufende/abgelaufene Nachweise: Zeige Warnung in orange/rot.
|
||||||
```task
|
```task
|
||||||
id: WP-0007-T06
|
id: WP-0007-T06
|
||||||
title: Dokument-URL-Verkabelung und Tests
|
title: Dokument-URL-Verkabelung und Tests
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`dokumente/urls.py`:
|
`dokumente/urls.py`:
|
||||||
```python
|
```python
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue