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>
This commit is contained in:
tegwick 2026-05-11 17:54:38 +02:00
parent c5ccbd665d
commit 5a231223c0
23 changed files with 828 additions and 37 deletions

View file

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}Feldkonfiguration — {{ entity_type }}{% endblock %}
{% block content %}
<div class="flex items-center justify-between mb-5">
<h1 class="page-title">Feldkonfiguration: {{ entity_type }}</h1>
</div>
<div class="flex gap-2 mb-5 flex-wrap">
{% for et in entity_types %}
<a href="/felder/{{ et }}/"
class="px-3 py-1 rounded text-sm {% if et == entity_type %}bg-brand-600 text-white{% else %}bg-slate-100 text-slate-700 hover:bg-slate-200{% endif %}">
{{ et }}
</a>
{% endfor %}
</div>
<div class="card">
<table class="w-full text-sm">
<thead>
<tr class="text-left border-b border-slate-200">
<th class="pb-2 font-medium text-slate-600">Feldname</th>
<th class="pb-2 font-medium text-slate-600">Anzeige-Label</th>
<th class="pb-2 font-medium text-slate-600 text-center">Ausgeblendet</th>
</tr>
</thead>
<tbody>
{% for eintrag in felder %}
{% include "core/partials/feld_zeile.html" with cfg=eintrag.cfg field_name=eintrag.feld.name entity_type=entity_type %}
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -0,0 +1,67 @@
<div x-data="{ formOpen: false }">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-medium text-slate-700">Zusatzfelder</h3>
<button @click="formOpen = !formOpen" type="button" class="text-xs text-brand-600 hover:underline">
+ Hinzufügen
</button>
</div>
<div x-show="formOpen" x-cloak class="mb-4 bg-slate-50 border border-slate-200 rounded-lg p-3">
<form hx-post="/core/attrs/{{ ct_id }}/{{ oid }}/neu/"
hx-target="closest div[x-data]"
hx-swap="outerHTML"
class="space-y-2">
{% csrf_token %}
<div class="grid grid-cols-3 gap-2">
<input type="text" name="label" placeholder="Bezeichnung" class="form-input text-sm" required>
<input type="text" name="value" placeholder="Wert" class="form-input text-sm">
<select name="data_type" class="form-input text-sm">
<option value="text">Text</option>
<option value="number">Zahl</option>
<option value="date">Datum</option>
<option value="boolean">Ja/Nein</option>
<option value="url">URL</option>
<option value="email">E-Mail</option>
</select>
</div>
<div class="flex gap-2">
<button type="submit" class="btn-primary text-xs py-1 px-3">Speichern</button>
<button type="button" @click="formOpen = false" class="btn-secondary text-xs py-1 px-3">Abbrechen</button>
</div>
</form>
</div>
{% if attrs %}
<div class="space-y-1">
{% for attr in attrs %}
<div class="flex items-center gap-2 text-sm border-b border-slate-100 py-1.5">
<span class="text-xs text-slate-500 w-32 shrink-0">{{ attr.label }}</span>
<span class="text-slate-700 flex-1">{{ attr.value|default:"—" }}</span>
<span class="text-xs text-slate-400 bg-slate-100 rounded px-1.5">{{ attr.data_type }}</span>
<div class="flex gap-1">
<form hx-post="/core/attrs/{{ ct_id }}/{{ oid }}/{{ attr.pk }}/sort/"
hx-target="closest div[x-data]" hx-swap="outerHTML" class="inline">
{% csrf_token %}
<input type="hidden" name="direction" value="up">
<button type="submit" class="text-slate-300 hover:text-slate-600 text-xs"></button>
</form>
<form hx-post="/core/attrs/{{ ct_id }}/{{ oid }}/{{ attr.pk }}/sort/"
hx-target="closest div[x-data]" hx-swap="outerHTML" class="inline">
{% csrf_token %}
<input type="hidden" name="direction" value="down">
<button type="submit" class="text-slate-300 hover:text-slate-600 text-xs"></button>
</form>
<form hx-post="/core/attrs/{{ ct_id }}/{{ oid }}/{{ attr.pk }}/loeschen/"
hx-target="closest div[x-data]" hx-swap="outerHTML" class="inline"
onsubmit="return confirm('Löschen?')">
{% csrf_token %}
<button type="submit" class="text-red-400 hover:text-red-600 text-xs">×</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-xs text-slate-400">Keine Zusatzfelder vorhanden.</p>
{% endif %}
</div>

View file

@ -0,0 +1,29 @@
<tr id="feld-{{ entity_type }}-{{ field_name }}" class="border-b border-slate-100 hover:bg-slate-50">
<td class="py-2 font-mono text-xs text-slate-700">{{ field_name }}</td>
<td class="py-2">
<form hx-post="/felder/{{ entity_type }}/{{ field_name }}/toggle/"
hx-target="#feld-{{ entity_type }}-{{ field_name }}"
hx-swap="outerHTML"
class="flex items-center gap-2">
{% csrf_token %}
<input type="hidden" name="is_hidden" value="{% if cfg.is_hidden %}true{% else %}false{% endif %}">
<input type="text" name="display_label"
value="{{ cfg.display_label|default:'' }}"
class="form-input w-48 text-sm" placeholder="{{ field_name }}">
<button type="submit" class="text-xs text-brand-600 hover:underline">Speichern</button>
</form>
</td>
<td class="py-2 text-center">
<form hx-post="/felder/{{ entity_type }}/{{ field_name }}/toggle/"
hx-target="#feld-{{ entity_type }}-{{ field_name }}"
hx-swap="outerHTML">
{% csrf_token %}
<input type="hidden" name="display_label" value="{{ cfg.display_label|default:'' }}">
<input type="hidden" name="is_hidden" value="{% if cfg.is_hidden %}false{% else %}true{% endif %}">
<button type="submit"
class="text-sm {% if cfg.is_hidden %}text-red-600 font-medium{% else %}text-slate-400{% endif %}">
{% if cfg.is_hidden %}Ja{% else %}Nein{% endif %}
</button>
</form>
</td>
</tr>