Established workplans
This commit is contained in:
parent
026f7c0db1
commit
315143a6fc
16 changed files with 5492 additions and 0 deletions
492
workplans/WP-0004-dashboard-ausschreibungen.md
Normal file
492
workplans/WP-0004-dashboard-ausschreibungen.md
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
---
|
||||
id: WP-0004
|
||||
title: Dashboard und Ausschreibungen-CRUD
|
||||
status: todo
|
||||
phase: 4-of-12
|
||||
created: "2026-05-08"
|
||||
depends_on: WP-0003
|
||||
---
|
||||
|
||||
# WP-0004 — Dashboard und Ausschreibungen-CRUD
|
||||
|
||||
Vollständige Implementierung des Dashboards und aller Ausschreibungs-Views:
|
||||
Liste, Suche/Filter, Anlegen, Detailseite, Status-Wechsel (HTMX), Teilnahmeentscheidung,
|
||||
Entscheidungsregel-Auswertung, Archivierung und historische Erfassung.
|
||||
|
||||
**Referenz:** UseCaseCatalog UC-OV-01 bis UC-OV-03, UC-AS-01 bis UC-AS-07.
|
||||
|
||||
---
|
||||
|
||||
```task
|
||||
id: WP-0004-T01
|
||||
title: Dashboard-View mit Kacheln und Fristenliste
|
||||
status: todo
|
||||
|
||||
`ausschreibungen/views.py` — Dashboard-View:
|
||||
```python
|
||||
def dashboard(request):
|
||||
from vergabe_teilnahme.apps.core.services import get_deadline_warnings
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
from datetime import date, timedelta
|
||||
|
||||
heute = date.today()
|
||||
in_14_tagen = heute + timedelta(days=14)
|
||||
|
||||
ctx = {
|
||||
'kritische_fristen': Ausschreibung.objects.filter(
|
||||
abgabe_bis__date__lte=in_14_tagen,
|
||||
abgabe_bis__date__gte=heute,
|
||||
status__lt=10
|
||||
).order_by('abgabe_bis')[:10],
|
||||
|
||||
'ohne_entscheidung': Ausschreibung.objects.filter(
|
||||
status__in=[1, 2],
|
||||
erstellt_am__lte=timezone.now() - timedelta(days=3)
|
||||
).order_by('erstellt_am')[:10],
|
||||
|
||||
'ueberfaellige_aufgaben': Aufgabe.objects.filter(
|
||||
frist__lt=heute,
|
||||
status__in=['offen', 'in_bearbeitung', 'wartend_intern', 'wartend_sub', 'wartend_ausschreiber']
|
||||
).select_related('ausschreibung', 'verantwortlicher').order_by('frist')[:15],
|
||||
|
||||
'laufende_ausschreibungen': Ausschreibung.objects.filter(
|
||||
status__range=(3, 9)
|
||||
).order_by('-geaendert_am')[:10],
|
||||
|
||||
'breadcrumbs': [{'label': 'Übersicht', 'url': None}],
|
||||
}
|
||||
return render(request, 'ausschreibungen/dashboard.html', ctx)
|
||||
```
|
||||
|
||||
`ausschreibungen/dashboard.html` zeigt vier Kacheln-Zeilen:
|
||||
Jede Kachel: Überschrift, Anzahl-Badge, Liste der Einträge mit Direktlinks.
|
||||
Nutze `.card`-Klasse, `status_badge`-Tag und relative Fristangaben (z. B. "in 3 Tagen").
|
||||
|
||||
Ablaufende Nachweise: Nachweis-Modell aus Bibliothek mit `gueltig_bis ≤ heute + 60 Tage`.
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T02
|
||||
title: Ausschreibungsliste mit Filter und HTMX-Suche
|
||||
status: todo
|
||||
|
||||
`ausschreibungen/views.py` — ListView:
|
||||
```python
|
||||
def ausschreibung_liste(request):
|
||||
qs = Ausschreibung.objects.all()
|
||||
# Filter-Parameter
|
||||
status = request.GET.get('status')
|
||||
if status:
|
||||
qs = qs.filter(status=status)
|
||||
archiviert = request.GET.get('archiviert', '0') == '1'
|
||||
qs = qs.filter(archiviert=archiviert)
|
||||
verantwortlicher = request.GET.get('verantwortlicher')
|
||||
if verantwortlicher:
|
||||
qs = qs.filter(hauptverantwortung=verantwortlicher)
|
||||
|
||||
qs = qs.select_related('hauptverantwortung').order_by('-geaendert_am')
|
||||
ctx = {
|
||||
'ausschreibungen': qs,
|
||||
'status_choices': Ausschreibung.STATUS_CHOICES,
|
||||
'mitarbeiter': Mitarbeiter.objects.all(),
|
||||
'breadcrumbs': [{'label': 'Ausschreibungen', 'url': None}],
|
||||
}
|
||||
template = 'ausschreibungen/liste_partial.html' if request.htmx else 'ausschreibungen/liste.html'
|
||||
return render(request, template, ctx)
|
||||
```
|
||||
|
||||
`liste.html` — vollständige Seite mit Filterleiste oben und eingebetteter Tabelle.
|
||||
`liste_partial.html` — nur die Tabellen-Rows (für HTMX-Filter-Update).
|
||||
|
||||
Filterleiste: Dropdowns für Status, Verantwortlicher, Checkbox "Archivierte anzeigen".
|
||||
Alle Filter-Änderungen: `hx-get="/ausschreibungen/" hx-target="#ausschreibungen-table" hx-push-url="true"`.
|
||||
|
||||
Tabelle: Titel, Ausschreiber, Status (status_badge), Abgabefrist (farbig wenn < 14 Tage),
|
||||
Verantwortlicher, Link zum Detail.
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T03
|
||||
title: Ausschreibung anlegen — Form und View (UC-AS-01)
|
||||
status: todo
|
||||
|
||||
`ausschreibungen/forms.py`:
|
||||
```python
|
||||
from django import forms
|
||||
|
||||
class AusschreibungForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Ausschreibung
|
||||
fields = ['titel', 'ausschreiber', 'plattform', 'plattform_link',
|
||||
'ansprechpartner', 'hauptverantwortung', 'beschreibung',
|
||||
'strategische_relevanz', 'bieterfragen_bis', 'abgabe_bis',
|
||||
'zuschlag_bis', 'produktiv_bis']
|
||||
widgets = {
|
||||
'titel': forms.TextInput(attrs={'class': 'form-input', 'autofocus': True}),
|
||||
'ausschreiber': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'abgabe_bis': forms.DateTimeInput(attrs={'class': 'form-input', 'type': 'datetime-local'}),
|
||||
'bieterfragen_bis': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
|
||||
# alle Datums-Widgets als type="date"
|
||||
}
|
||||
```
|
||||
|
||||
`ausschreibungen/views.py` — CreateView (function-based):
|
||||
```python
|
||||
def ausschreibung_neu(request):
|
||||
if request.method == 'POST':
|
||||
form = AusschreibungForm(request.POST)
|
||||
if form.is_valid():
|
||||
a = form.save()
|
||||
return redirect('ausschreibungen:detail', pk=a.pk)
|
||||
else:
|
||||
form = AusschreibungForm()
|
||||
return render(request, 'ausschreibungen/form.html', {
|
||||
'form': form,
|
||||
'titel': 'Neue Ausschreibung',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': 'Neu', 'url': None}
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
`ausschreibungen/form.html` — einfaches, gut gelayoutetes Formular.
|
||||
Sections: Stammdaten, Fristen. Alle Felder nutzen `form-input` und `form-label`.
|
||||
Submit: "Speichern" (btn-primary), "Abbrechen" (btn-ghost, zurück zur Liste).
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T04
|
||||
title: Ausschreibung-Detailseite (Phase 1 — Stammdaten)
|
||||
status: todo
|
||||
|
||||
`ausschreibungen/views.py` — Detailview:
|
||||
```python
|
||||
def ausschreibung_detail(request, pk):
|
||||
a = get_object_or_404(Ausschreibung, pk=pk)
|
||||
from vergabe_teilnahme.apps.core.services import get_deadline_warnings, build_phase_nav
|
||||
ctx = {
|
||||
'ausschreibung': a,
|
||||
'ausschreibung_id': pk, # für Context-Processor / Phase-Navigator
|
||||
'warnungen': get_deadline_warnings(a),
|
||||
'freigaben': a.freigabe_set.all() if hasattr(a, 'freigabe_set') else [],
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': a.titel, 'url': None}
|
||||
],
|
||||
}
|
||||
return render(request, 'ausschreibungen/detail.html', ctx)
|
||||
```
|
||||
|
||||
`ausschreibungen/detail.html`:
|
||||
- Seitentitel: Ausschreibungstitel + Status-Badge + Edit-Button
|
||||
- Warnungs-Banner (gelb/rot) falls `warnungen` nicht leer
|
||||
- Abschnitt "Stammdaten": nutze `{% render_field ausschreibung "ausschreiber" "Ausschreiber" %}` für alle Felder
|
||||
- Abschnitt "Fristen": alle Datums-Felder mit Restlaufzeit-Anzeige
|
||||
- Abschnitt "Freigaben": kompakte Liste (typ, person, datum)
|
||||
- Tab-Navigation zu Unterseiten (Lose, Anforderungen, Aufgaben, Bieterfragen, Preise, Abgabe, Nachbetrachtung)
|
||||
als horizontale Link-Leiste unterhalb des Titels
|
||||
- "Weitere Attribute" CustomAttribute-Panel (HTMX lazy-load, Implementierung in WP-0012)
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T05
|
||||
title: Ausschreibung bearbeiten (Edit-View) und Status inline wechseln
|
||||
status: todo
|
||||
|
||||
`ausschreibungen/views.py`:
|
||||
|
||||
**Edit-View** (gleiche Form wie Neu, aber mit `instance=`):
|
||||
```python
|
||||
def ausschreibung_bearbeiten(request, pk):
|
||||
a = get_object_or_404(Ausschreibung, pk=pk)
|
||||
form = AusschreibungForm(request.POST or None, instance=a)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
return redirect('ausschreibungen:detail', pk=pk)
|
||||
return render(request, 'ausschreibungen/form.html', {'form': form, 'titel': 'Bearbeiten', ...})
|
||||
```
|
||||
|
||||
**Status-Wechsel HTMX-Endpunkt:**
|
||||
```python
|
||||
def ausschreibung_status(request, pk):
|
||||
a = get_object_or_404(Ausschreibung, pk=pk)
|
||||
if request.method == 'POST':
|
||||
neuer_status = int(request.POST.get('status', a.status))
|
||||
a.status = neuer_status
|
||||
a.save(update_fields=['status', 'geaendert_am'])
|
||||
return render(request, 'ausschreibungen/partials/status_widget.html', {'ausschreibung': a})
|
||||
```
|
||||
|
||||
`ausschreibungen/partials/status_widget.html`:
|
||||
```html
|
||||
<div id="status-widget-{{ ausschreibung.pk }}"
|
||||
hx-target="#status-widget-{{ ausschreibung.pk }}" hx-swap="outerHTML">
|
||||
{% status_badge ausschreibung.get_status_display ausschreibung.status %}
|
||||
<select name="status" hx-post="{% url 'ausschreibungen:status' ausschreibung.pk %}"
|
||||
hx-trigger="change" class="form-input ml-2 w-auto">
|
||||
{% for val, label in ausschreibung.STATUS_CHOICES %}
|
||||
<option value="{{ val }}" {% if val == ausschreibung.status %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
```
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T06
|
||||
title: Teilnahmeentscheidung-Seite (Phase 2, UC-AS-04)
|
||||
status: todo
|
||||
|
||||
`ausschreibungen/views.py` — Teilnahmeentscheidungs-View:
|
||||
```python
|
||||
def ausschreibung_entscheidung(request, pk):
|
||||
a = get_object_or_404(Ausschreibung, pk=pk)
|
||||
if request.method == 'POST':
|
||||
a.teilnahmeentscheidung = request.POST.get('teilnahmeentscheidung', 'offen')
|
||||
a.beschreibung = request.POST.get('begruendung', a.beschreibung)
|
||||
if a.teilnahmeentscheidung in ['teilnahme', 'ablehnung']:
|
||||
a.status = max(a.status, 3)
|
||||
a.save()
|
||||
return redirect('ausschreibungen:detail', pk=pk)
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.services import entscheidungsregel_auswertung
|
||||
ctx = {
|
||||
'ausschreibung': a,
|
||||
'regelergebnis': entscheidungsregel_auswertung(a),
|
||||
'ausschlusskriterien_nicht_erfuellbar': a.anforderung_set.filter(
|
||||
ausschlusskriterium=True, erfuellungsstatus='nicht_erfuellbar'
|
||||
) if hasattr(a, 'anforderung_set') else [],
|
||||
'breadcrumbs': [...],
|
||||
}
|
||||
return render(request, 'ausschreibungen/entscheidung.html', ctx)
|
||||
```
|
||||
|
||||
`ausschreibungen/entscheidung.html`:
|
||||
- Zeigt offene Ausschlusskriterien als rote Warnmeldungen (wenn vorhanden)
|
||||
- Zeigt Regelergebnis aus dem Katalog als strukturierte Liste
|
||||
- Formular: Radio-Buttons für Teilnahme/Nichtteilnahme/Weitere Prüfung, Begründungsfeld
|
||||
- "Freigabe erteilen"-Button (öffnet Freigabe-Modal, Implementierung in WP-0012)
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T07
|
||||
title: Entscheidungsregel-Auswertungs-Service
|
||||
status: todo
|
||||
|
||||
`vergabe_teilnahme/apps/ausschreibungen/services.py`:
|
||||
|
||||
```python
|
||||
def entscheidungsregel_auswertung(ausschreibung):
|
||||
"""
|
||||
Wendet alle aktiven Entscheidungsregeln auf eine Ausschreibung an.
|
||||
Gibt Liste von Ergebnis-Dicts zurück.
|
||||
"""
|
||||
from vergabe_teilnahme.apps.bibliothek.models import Entscheidungsregel
|
||||
regeln = Entscheidungsregel.objects.filter(aktiv=True).order_by('-gewichtung')
|
||||
ergebnisse = []
|
||||
for regel in regeln:
|
||||
ergebnis = _wende_regel_an(regel, ausschreibung)
|
||||
ergebnisse.append({
|
||||
'regel': regel,
|
||||
'empfehlung': ergebnis['empfehlung'],
|
||||
'begruendung': ergebnis['begruendung'],
|
||||
'warnung': ergebnis['empfehlung'] == 'nicht_teilnehmen',
|
||||
})
|
||||
return ergebnisse
|
||||
|
||||
def _wende_regel_an(regel, ausschreibung):
|
||||
"""
|
||||
Einfache Heuristik für v1: Überprüft bekannte Regel-Kategorien.
|
||||
Für unbekannte Kategorien: gibt neutrale Empfehlung zurück.
|
||||
"""
|
||||
kat = regel.kategorie
|
||||
if kat == 'ausschlusskriterium' and hasattr(ausschreibung, 'anforderung_set'):
|
||||
hat_ausschluss = ausschreibung.anforderung_set.filter(
|
||||
ausschlusskriterium=True, erfuellungsstatus='nicht_erfuellbar'
|
||||
).exists()
|
||||
if hat_ausschluss:
|
||||
return {'empfehlung': 'nicht_teilnehmen',
|
||||
'begruendung': 'Nicht erfüllbares Ausschlusskriterium vorhanden.'}
|
||||
if kat == 'frist' and ausschreibung.abgabe_bis:
|
||||
from datetime import date
|
||||
delta = (ausschreibung.abgabe_bis.date() - date.today()).days
|
||||
if regel.schwellenwert and delta < regel.schwellenwert:
|
||||
return {'empfehlung': 'nicht_teilnehmen',
|
||||
'begruendung': f'Restlaufzeit {delta} Tage unter Schwellenwert.'}
|
||||
return {'empfehlung': 'pruefen', 'begruendung': regel.begruendung or '—'}
|
||||
```
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T08
|
||||
title: Ausschreibung archivieren und historisch erfassen (UC-AS-06, UC-AS-07)
|
||||
status: todo
|
||||
|
||||
**Archivieren:**
|
||||
```python
|
||||
def ausschreibung_archivieren(request, pk):
|
||||
a = get_object_or_404(Ausschreibung, pk=pk)
|
||||
if request.method == 'POST':
|
||||
a.archiviert = True
|
||||
a.status = 13
|
||||
a.save(update_fields=['archiviert', 'status', 'geaendert_am'])
|
||||
return redirect('ausschreibungen:liste')
|
||||
return render(request, 'ausschreibungen/archivieren_confirm.html', {'ausschreibung': a})
|
||||
```
|
||||
|
||||
`archivieren_confirm.html`: Einfacher Bestätigungsdialog (Alpine.js Modal oder eigene Seite).
|
||||
|
||||
**Historisch erfassen:**
|
||||
Ergänze `AusschreibungForm` um ein BooleanField `historisch_erfassen` (Widget: HiddenInput).
|
||||
Bei `historisch_erfassen=True` zeigt das Formular zusätzlich die Felder:
|
||||
`ergebnis`, `teilnahmeentscheidung` — direkt befüllbar ohne Phasenreihenfolge.
|
||||
Die Detailseite wird nach dem Speichern sofort mit allen Unterseiten (Preise, Nachbetrachtung etc.)
|
||||
zugänglich — keine Einschränkung.
|
||||
|
||||
URL für historische Erfassung: `/ausschreibungen/neu/?historisch=1`
|
||||
Die View prüft diesen Parameter und setzt `historisch_erfassen` im initialen Form-Context.
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T09
|
||||
title: Globale Suchleiste — HTMX-Endpunkt und Ergebnis-Template
|
||||
status: todo
|
||||
|
||||
`core/views.py`:
|
||||
```python
|
||||
def global_search(request):
|
||||
q = request.GET.get('q', '').strip()
|
||||
if len(q) < 2:
|
||||
return HttpResponse('')
|
||||
ctx = {
|
||||
'q': q,
|
||||
'ausschreibungen': Ausschreibung.objects.filter(
|
||||
Q(titel__icontains=q) | Q(ausschreiber__icontains=q)
|
||||
)[:5],
|
||||
'aufgaben': Aufgabe.objects.filter(titel__icontains=q)[:5],
|
||||
'subunternehmer': Subunternehmer.objects.filter(name__icontains=q)[:5],
|
||||
'marktbegleiter': Marktbegleiter.objects.filter(name__icontains=q)[:3],
|
||||
}
|
||||
return render(request, 'partials/search_results.html', ctx)
|
||||
```
|
||||
|
||||
`partials/search_results.html`:
|
||||
```html
|
||||
{% if ausschreibungen or aufgaben or subunternehmer %}
|
||||
<div class="p-3 space-y-3">
|
||||
{% if ausschreibungen %}
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-slate-500 uppercase mb-1">Ausschreibungen</p>
|
||||
{% for a in ausschreibungen %}
|
||||
<a href="/ausschreibungen/{{ a.pk }}/" class="block px-2 py-1 rounded hover:bg-slate-50 text-sm">
|
||||
{{ a.titel }} <span class="text-slate-400">— {{ a.ausschreiber }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- analog für andere Kategorien -->
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
URL: `path('suche/', core_views.global_search, name='global_search')`
|
||||
Topbar-Formular (aus WP-0003-T02) zeigt Ergebnisse in `#search-results`.
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T10
|
||||
title: Ausschreibungen-URL-Verkabelung und App-Namespace
|
||||
status: todo
|
||||
|
||||
`vergabe_teilnahme/apps/ausschreibungen/urls.py`:
|
||||
```python
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'ausschreibungen'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.ausschreibung_liste, name='liste'),
|
||||
path('neu/', views.ausschreibung_neu, name='neu'),
|
||||
path('<int:pk>/', views.ausschreibung_detail, name='detail'),
|
||||
path('<int:pk>/bearbeiten/', views.ausschreibung_bearbeiten, name='bearbeiten'),
|
||||
path('<int:pk>/status/', views.ausschreibung_status, name='status'),
|
||||
path('<int:pk>/entscheidung/', views.ausschreibung_entscheidung, name='entscheidung'),
|
||||
path('<int:pk>/archivieren/', views.ausschreibung_archivieren, name='archivieren'),
|
||||
# Unterseiten-URLs (Platzhalter für spätere Workplans):
|
||||
path('<int:ausschreibung_id>/lose/', include('vergabe_teilnahme.apps.lose.urls')),
|
||||
path('<int:ausschreibung_id>/aufgaben/', include('vergabe_teilnahme.apps.aufgaben.urls')),
|
||||
path('<int:ausschreibung_id>/bieterfragen/', include('vergabe_teilnahme.apps.aufgaben.bieterfragen_urls')),
|
||||
path('<int:ausschreibung_id>/dokumente/', include('vergabe_teilnahme.apps.dokumente.urls')),
|
||||
path('<int:ausschreibung_id>/preise/', include('vergabe_teilnahme.apps.preise.urls')),
|
||||
path('<int:ausschreibung_id>/abgabe/', include('vergabe_teilnahme.apps.nachbetrachtung.abgabe_urls')),
|
||||
path('<int:ausschreibung_id>/nachbetrachtung/', include('vergabe_teilnahme.apps.nachbetrachtung.urls')),
|
||||
path('<int:ausschreibung_id>/marktbegleiter/', include('vergabe_teilnahme.apps.marktbegleiter.passagen_urls')),
|
||||
]
|
||||
```
|
||||
|
||||
Jede referenzierte App-URL-Datei wird hier als leere Stub-Datei angelegt
|
||||
(`urlpatterns = []`) damit die includes nicht zu ImportErrors führen.
|
||||
|
||||
Prüfe: `uv run manage.py check --deploy` → keine URL-Fehler.
|
||||
Smoke-Test: alle Hauptseiten (/ausschreibungen/, /ausschreibungen/neu/) laden ohne 500.
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T11
|
||||
title: Ausschreibungs-Tests (Models und Views)
|
||||
status: todo
|
||||
|
||||
Erstelle `vergabe_teilnahme/apps/ausschreibungen/tests/`:
|
||||
|
||||
`test_models.py`:
|
||||
- Test: Ausschreibung `__str__` gibt Titel zurück
|
||||
- Test: `ist_aktiv` property für Status 1-9 (True) und 10-13 (False)
|
||||
- Test: `naechste_frist` gibt das frühere von bieterfragen_bis/abgabe_bis zurück
|
||||
|
||||
`test_views.py` (nutze `pytest-django` + `client` fixture):
|
||||
- Test: GET /ausschreibungen/ → 200
|
||||
- Test: GET /ausschreibungen/neu/ → 200
|
||||
- Test: POST /ausschreibungen/neu/ mit validen Daten → Redirect zur Detailseite
|
||||
- Test: GET /ausschreibungen/<pk>/ → 200
|
||||
- Test: POST /ausschreibungen/<pk>/status/ mit status=4 → 200, Ausschreibung hat status=4
|
||||
- Test: Status-Wechsel mit HTMX-Header → partial template response
|
||||
|
||||
Nutze `factory_boy` für Factories:
|
||||
```python
|
||||
import factory
|
||||
class AusschreibungFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Ausschreibung
|
||||
titel = factory.Sequence(lambda n: f"Ausschreibung {n}")
|
||||
ausschreiber = "Testausschreiber GmbH"
|
||||
status = 1
|
||||
```
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0004-T12
|
||||
title: Seed-Daten prüfen und Dashboard-Kacheln verifizieren
|
||||
status: todo
|
||||
|
||||
Führe die gesamte Integrations-Smoke-Test-Sequenz durch:
|
||||
|
||||
1. `make db` → PostgreSQL läuft
|
||||
2. `uv run manage.py migrate` → alle Migrationen sauber
|
||||
3. `uv run manage.py seed_dev` → Seed-Daten angelegt
|
||||
4. `make dev` → Server läuft
|
||||
5. Browser öffnen: `http://localhost:8000/`
|
||||
→ Dashboard zeigt Kacheln (auch wenn leer)
|
||||
→ Sidebar zeigt alle globalen Navpunkte
|
||||
→ Topbar mit Suchleiste sichtbar
|
||||
6. `http://localhost:8000/ausschreibungen/`
|
||||
→ Liste zeigt die Seed-Ausschreibung
|
||||
7. Ausschreibung öffnen → Detail-Seite rendert mit Stammdaten
|
||||
8. Status-Dropdown wechseln → HTMX aktualisiert Status inline
|
||||
9. `http://localhost:8000/ausschreibungen/neu/` → Formular funktioniert
|
||||
10. `uv run pytest vergabe_teilnahme/apps/ausschreibungen/` → alle Tests grün
|
||||
|
||||
Erst wenn alle 10 Punkte erfüllt sind: Task als done markieren.
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue