Files
vergabe-teilnahme/vergabe_teilnahme/apps/core/views.py
tegwick 5a231223c0 feat(WP-0012): Querschnitt — Freigaben, Felder, Feedback, Suche, Tests
Implements all 8 tasks of the final cross-cutting workplan:

- T01: Generisches Freigabe-Modal (freigabe_modal, freigabe_erteilen views + templates)
- T02: Freigaben-Übersicht pro Ausschreibung (freigaben_uebersicht view + template)
- T03: EntityFieldConfig Admin-Interface (/felder/<entity_type>/ with HTMX toggle)
- T04: CustomAttribute-Panel (full CRUD with sort, lazy HTMX load)
- T05: Feedback-Backlog mit Statusverwaltung + feedback_success.html template
- T06: End-to-End-Tests in vergabe_teilnahme/tests/test_e2e.py (8 tests)
- T07: Globale Suche erweitert (Dokumente, Nachweise, Referenzen, Marktbegleiter)
- T08: Alle Migrationen sauber, 68/68 Tests grün, Ruff-Fehler in neuem Code behoben

Bugfix: URL-Namespace-Fehler in Abgabe-Templates (ausschreibungen:nachbetrachtung:abgabe → ausschreibungen:abgabe)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 17:54:38 +02:00

226 lines
8.8 KiB
Python

from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_GET, require_POST
from .models import CustomAttribute, EntityFieldConfig, Freigabe
ENTITY_TYPES = None # lazy-loaded to avoid circular imports
def _entity_types():
global ENTITY_TYPES
if ENTITY_TYPES is None:
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
from vergabe_teilnahme.apps.bibliothek.models import Nachweis, Referenz
from vergabe_teilnahme.apps.lose.models import Anforderung, Los
from vergabe_teilnahme.apps.partner.models import Subunternehmer
ENTITY_TYPES = {
'ausschreibung': Ausschreibung,
'los': Los,
'anforderung': Anforderung,
'aufgabe': Aufgabe,
'subunternehmer': Subunternehmer,
'nachweis': Nachweis,
'referenz': Referenz,
}
return ENTITY_TYPES
def custom_404(request, exception=None):
return render(request, 'errors/404.html', status=404)
def custom_500(request):
return render(request, 'errors/500.html', status=500)
# ── Freigaben ─────────────────────────────────────────────────────────────────
@require_GET
def freigabe_modal(request):
ctx = {
'content_type_id': request.GET.get('ct', ''),
'object_id': request.GET.get('oid', ''),
'freigabe_typ': request.GET.get('typ', ''),
'freigabe_typ_choices': Freigabe.TYP_CHOICES,
}
return render(request, 'partials/freigabe_modal.html', ctx)
@require_POST
def freigabe_erteilen(request):
ct_id = request.POST.get('content_type_id')
object_id = request.POST.get('object_id')
freigabe_typ = request.POST.get('freigabe_typ')
if not (ct_id and object_id and freigabe_typ):
return HttpResponseBadRequest()
ct = get_object_or_404(ContentType, pk=ct_id)
Freigabe.objects.create(
content_type=ct,
object_id=object_id,
freigabe_typ=freigabe_typ,
freigebende_person=request.user,
status='erteilt',
kommentar=request.POST.get('kommentar', ''),
)
return render(request, 'partials/freigabe_success.html', {})
# ── EntityFieldConfig ─────────────────────────────────────────────────────────
def feld_konfiguration_liste(request, entity_type):
types = _entity_types()
if entity_type not in types:
from django.http import Http404
raise Http404
model = types[entity_type]
raw_felder = [f for f in model._meta.get_fields()
if hasattr(f, 'column') and not f.name.startswith('_')]
konfigurationen = {
cfg.field_name: cfg
for cfg in EntityFieldConfig.objects.filter(entity_type=entity_type)
}
felder = [
{'feld': f, 'cfg': konfigurationen.get(f.name)}
for f in raw_felder
]
return render(request, 'core/feld_konfiguration.html', {
'entity_type': entity_type,
'entity_types': list(_entity_types().keys()),
'felder': felder,
})
@require_POST
def feld_konfiguration_toggle(request, entity_type, field_name):
cfg, _ = EntityFieldConfig.objects.get_or_create(
entity_type=entity_type, field_name=field_name
)
cfg.is_hidden = request.POST.get('is_hidden') == 'true'
cfg.display_label = request.POST.get('display_label', '')
cfg.save()
return render(request, 'core/partials/feld_zeile.html', {
'cfg': cfg,
'field_name': field_name,
'entity_type': entity_type,
})
# ── CustomAttributes ──────────────────────────────────────────────────────────
def custom_attributes_panel(request, content_type_id, object_id):
ct = get_object_or_404(ContentType, pk=content_type_id)
attrs = CustomAttribute.objects.filter(content_type=ct, object_id=object_id)
return render(request, 'core/partials/custom_attributes.html', {
'attrs': attrs,
'ct_id': content_type_id,
'oid': object_id,
})
@require_POST
def custom_attribute_neu(request, content_type_id, object_id):
from django.utils.text import slugify
ct = get_object_or_404(ContentType, pk=content_type_id)
label = request.POST.get('label', '').strip()
if label:
CustomAttribute.objects.create(
content_type=ct,
object_id=object_id,
key=slugify(label),
label=label,
value=request.POST.get('value', ''),
data_type=request.POST.get('data_type', 'text'),
)
attrs = CustomAttribute.objects.filter(content_type=ct, object_id=object_id)
return render(request, 'core/partials/custom_attributes.html', {
'attrs': attrs,
'ct_id': content_type_id,
'oid': object_id,
})
@require_POST
def custom_attribute_bearbeiten(request, content_type_id, object_id, attr_pk):
attr = get_object_or_404(CustomAttribute, pk=attr_pk, object_id=object_id)
attr.label = request.POST.get('label', attr.label).strip() or attr.label
attr.value = request.POST.get('value', attr.value)
attr.data_type = request.POST.get('data_type', attr.data_type)
attr.save()
ct = ContentType.objects.get(pk=content_type_id)
attrs = CustomAttribute.objects.filter(content_type=ct, object_id=object_id)
return render(request, 'core/partials/custom_attributes.html', {
'attrs': attrs,
'ct_id': content_type_id,
'oid': object_id,
})
@require_POST
def custom_attribute_loeschen(request, content_type_id, object_id, attr_pk):
attr = get_object_or_404(CustomAttribute, pk=attr_pk, object_id=object_id)
attr.delete()
ct = ContentType.objects.get(pk=content_type_id)
attrs = CustomAttribute.objects.filter(content_type=ct, object_id=object_id)
return render(request, 'core/partials/custom_attributes.html', {
'attrs': attrs,
'ct_id': content_type_id,
'oid': object_id,
})
@require_POST
def custom_attribute_sort(request, content_type_id, object_id, attr_pk):
direction = request.POST.get('direction', 'down')
attr = get_object_or_404(CustomAttribute, pk=attr_pk, object_id=object_id)
ct = ContentType.objects.get(pk=content_type_id)
attrs = list(CustomAttribute.objects.filter(content_type=ct, object_id=object_id))
idx = next((i for i, a in enumerate(attrs) if a.pk == attr.pk), None)
if idx is not None:
swap = idx - 1 if direction == 'up' else idx + 1
if 0 <= swap < len(attrs):
old, new = attrs[swap].sort_order, attrs[idx].sort_order
attrs[idx].sort_order, attrs[swap].sort_order = old, new
attrs[idx].save(update_fields=['sort_order'])
attrs[swap].save(update_fields=['sort_order'])
attrs = CustomAttribute.objects.filter(content_type=ct, object_id=object_id)
return render(request, 'core/partials/custom_attributes.html', {
'attrs': attrs,
'ct_id': content_type_id,
'oid': object_id,
})
# ── Globale Suche ─────────────────────────────────────────────────────────────
@require_GET
def suche(request):
q = request.GET.get('q', '').strip()
if not q or len(q) < 2:
return HttpResponse('')
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
from vergabe_teilnahme.apps.bibliothek.models import Nachweis, Referenz
from vergabe_teilnahme.apps.dokumente.models import Dokument
from vergabe_teilnahme.apps.marktbegleiter.models import Marktbegleiter
ausschreibungen = Ausschreibung.objects.filter(titel__icontains=q)[:5]
dokumente = Dokument.objects.filter(dateiname__icontains=q).select_related('ausschreibung')[:5]
nachweise = Nachweis.objects.filter(titel__icontains=q)[:5]
referenzen = Referenz.objects.filter(referenztitel__icontains=q)[:5]
marktbegleiter = Marktbegleiter.objects.filter(name__icontains=q)[:5]
has_results = any([ausschreibungen, dokumente, nachweise, referenzen, marktbegleiter])
if not has_results:
return render(request, 'partials/search_results.html', {'q': q, 'empty': True})
return render(request, 'partials/search_results.html', {
'q': q,
'ausschreibungen': ausschreibungen,
'dokumente': dokumente,
'nachweise': nachweise,
'referenzen': referenzen,
'marktbegleiter': marktbegleiter,
})