generated from coulomb/repo-seed
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>
226 lines
8.8 KiB
Python
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,
|
|
})
|