Complete dashboard loose ends and reconcile blocked pilot work
All checks were successful
All checks were successful
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e394-2a69-7df0-8980-22a0ebe55216
This commit is contained in:
parent
387b7e9782
commit
35df9bb772
13 changed files with 325 additions and 55 deletions
|
|
@ -9,7 +9,7 @@
|
|||
| Kind | ID | Status | Lane | Source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| workplan | VERGABE-WP-0018 | blocked | — | workplans/VERGABE-WP-0018-customer-factory-delivery.md |
|
||||
| workplan | VERGABE-WP-0019 | active | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| workplan | VERGABE-WP-0019 | blocked | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| workplan | VT-WP-0001 | finished | — | workplans/VT-WP-0001-projektgeruest.md |
|
||||
| workplan | VT-WP-0002 | finished | — | workplans/VT-WP-0002-fachmodelle.md |
|
||||
| workplan | VT-WP-0003 | finished | — | workplans/VT-WP-0003-basis-ui.md |
|
||||
|
|
@ -32,10 +32,10 @@
|
|||
| task | VERGABE-WP-0018-T03 | wait | — | workplans/VERGABE-WP-0018-customer-factory-delivery.md |
|
||||
| task | VERGABE-WP-0019-T01 | done | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VERGABE-WP-0019-T02 | done | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VERGABE-WP-0019-T03 | progress | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VERGABE-WP-0019-T03 | wait | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VERGABE-WP-0019-T04 | wait | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VERGABE-WP-0019-T05 | done | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VERGABE-WP-0019-T06 | progress | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VERGABE-WP-0019-T06 | wait | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
|
||||
| task | VT-WP-0001-T03 | done | — | workplans/VT-WP-0001-projektgeruest.md |
|
||||
| task | VT-WP-0001-T05 | done | — | workplans/VT-WP-0001-projektgeruest.md |
|
||||
| task | VT-WP-0001-T07 | done | — | workplans/VT-WP-0001-projektgeruest.md |
|
||||
|
|
@ -45,6 +45,18 @@
|
|||
| task | VT-WP-0002-T13 | done | — | workplans/VT-WP-0002-fachmodelle.md |
|
||||
| task | VT-WP-0003-T01 | done | — | workplans/VT-WP-0003-basis-ui.md |
|
||||
| task | VT-WP-0003-T10 | done | — | workplans/VT-WP-0003-basis-ui.md |
|
||||
| task | VT-WP-0004-T01 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T02 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T03 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T04 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T05 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T06 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T07 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T08 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T09 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T10 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T11 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0004-T12 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
|
||||
| task | VT-WP-0005-T07 | done | — | workplans/VT-WP-0005-lose-anforderungen.md |
|
||||
| task | VT-WP-0012-T06 | done | — | workplans/VT-WP-0012-querschnitt.md |
|
||||
| task | VT-WP-0015-T03 | done | — | workplans/VT-WP-0015-aufgaben-verknuepfungen-frist-issuefacade.md |
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ class AusschreibungForm(forms.ModelForm):
|
|||
'entscheidungsbegruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, historisch=False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['teilnahmeentscheidung'].required = False
|
||||
if historisch:
|
||||
from vergabe_teilnahme.apps.nachbetrachtung.models import Nachbetrachtung
|
||||
|
||||
self.fields['ergebnis'] = forms.ChoiceField(
|
||||
choices=Nachbetrachtung.ERGEBNIS_CHOICES, initial='offen',
|
||||
widget=forms.Select(attrs={'class': 'form-input'}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -98,3 +98,93 @@ def test_status_htmx_returns_partial(admitted_client):
|
|||
response = admitted_client.post(url, {"status": "3"}, HTTP_HX_REQUEST="true")
|
||||
assert response.status_code == 200
|
||||
assert b"status-widget" in response.content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_expiry_boundary(admitted_client):
|
||||
from datetime import date, timedelta
|
||||
|
||||
from vergabe_teilnahme.apps.bibliothek.models import Nachweis
|
||||
|
||||
for title, days in [('Expired', -1), ('Boundary', 60), ('Later', 61)]:
|
||||
Nachweis.objects.create(titel=title, gueltig_bis=date.today() + timedelta(days=days))
|
||||
Nachweis.objects.create(titel='Undated')
|
||||
response = admitted_client.get(reverse('ausschreibungen:dashboard'))
|
||||
assert response.status_code == 200
|
||||
assert [n.titel for n in response.context['ablaufende_nachweise']] == ['Expired', 'Boundary']
|
||||
assert 'Boundary' in response.content.decode()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_search_combines_filters_and_htmx(admitted_client):
|
||||
matching = AusschreibungFactory(titel='Network upgrade', ausschreiber='City', status=4)
|
||||
AusschreibungFactory(titel='Other', ausschreiber='City', status=3)
|
||||
AusschreibungFactory(titel='Archived', ausschreiber='City', status=4, archiviert=True)
|
||||
for query in ['Network', 'City']:
|
||||
response = admitted_client.get(reverse('ausschreibungen:liste'),
|
||||
{'q': query, 'status': '4'}, HTTP_HX_REQUEST='true')
|
||||
assert list(response.context['ausschreibungen']) == [matching]
|
||||
assert 'ausschreibungen/liste_partial.html' in [t.name for t in response.templates]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_historical_result_validation_and_save(admitted_client):
|
||||
url = reverse('ausschreibungen:neu')
|
||||
response = admitted_client.get(url, {'historisch': '1'})
|
||||
assert 'name="ergebnis"' in response.content.decode()
|
||||
data = {'titel': 'Historical award', 'ausschreiber': 'City',
|
||||
'historisch_erfassen': '1', 'teilnahmeentscheidung': 'teilnahme',
|
||||
'ergebnis': 'invalid'}
|
||||
response = admitted_client.post(url, data)
|
||||
assert response.status_code == 200
|
||||
assert response.context['form'].errors['ergebnis']
|
||||
assert not Ausschreibung.objects.filter(titel=data['titel']).exists()
|
||||
data['ergebnis'] = 'gewonnen'
|
||||
response = admitted_client.post(url, data)
|
||||
assert response.status_code == 302
|
||||
tender = Ausschreibung.objects.get(titel=data['titel'])
|
||||
assert tender.nachbetrachtung.ergebnis == 'gewonnen'
|
||||
assert tender.status == 10
|
||||
assert tender.teilnahmeentscheidung == 'teilnahme'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_global_search_tasks_partners_and_issuer(admitted_client):
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
from vergabe_teilnahme.apps.partner.models import Subunternehmer
|
||||
|
||||
tender = AusschreibungFactory(titel='Tender', ausschreiber='UniqueIssuer')
|
||||
task = Aufgabe.objects.create(titel='UniqueTask', ausschreibung=tender)
|
||||
partner = Subunternehmer.objects.create(name='UniquePartner')
|
||||
cases = [
|
||||
('UniqueIssuer', reverse('ausschreibungen:detail', kwargs={'pk': tender.pk})),
|
||||
('UniqueTask', reverse('ausschreibungen:aufgaben:detail',
|
||||
kwargs={'ausschreibung_id': tender.pk, 'pk': task.pk})),
|
||||
('UniquePartner', reverse('partner:su_detail', kwargs={'pk': partner.pk})),
|
||||
]
|
||||
for query, target in cases:
|
||||
response = admitted_client.get('/suche/', {'q': query})
|
||||
assert response.status_code == 200
|
||||
assert f'href="{target}"' in response.content.decode()
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inline_status_with_rendered_csrf_header(mitarbeiter):
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.test import Client
|
||||
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
client.force_login(mitarbeiter)
|
||||
tender = AusschreibungFactory(status=1)
|
||||
response = client.get(reverse('ausschreibungen:detail', kwargs={'pk': tender.pk}))
|
||||
headers = json.loads(re.search(r"hx-headers='([^']+)'", response.content.decode())[1])
|
||||
url = reverse('ausschreibungen:status', kwargs={'pk': tender.pk})
|
||||
assert client.post(url, {'status': '4'}).status_code == 403
|
||||
response = client.post(url, {'status': '4'}, HTTP_HX_REQUEST='true',
|
||||
HTTP_X_CSRFTOKEN=headers['X-CSRFToken'])
|
||||
assert response.status_code == 200
|
||||
tender.refresh_from_db()
|
||||
assert tender.status == 4
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from datetime import date, timedelta
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
|
@ -15,6 +17,7 @@ def _is_htmx(request):
|
|||
|
||||
def dashboard(request):
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
from vergabe_teilnahme.apps.bibliothek.models import Nachweis
|
||||
|
||||
heute = date.today()
|
||||
in_14_tagen = heute + timedelta(days=14)
|
||||
|
|
@ -41,6 +44,10 @@ def dashboard(request):
|
|||
status__range=(1, 9),
|
||||
).order_by('-geaendert_am')[:10],
|
||||
|
||||
'ablaufende_nachweise': Nachweis.objects.filter(
|
||||
gueltig_bis__lte=heute + timedelta(days=60),
|
||||
).order_by('gueltig_bis', 'pk'),
|
||||
'today': heute,
|
||||
'breadcrumbs': [{'label': 'Übersicht', 'url': None}],
|
||||
}
|
||||
return render(request, 'ausschreibungen/dashboard.html', ctx)
|
||||
|
|
@ -49,6 +56,10 @@ def dashboard(request):
|
|||
def ausschreibung_liste(request):
|
||||
qs = Ausschreibung.objects.all()
|
||||
|
||||
q = request.GET.get('q', '').strip()
|
||||
if q:
|
||||
qs = qs.filter(Q(titel__icontains=q) | Q(ausschreiber__icontains=q))
|
||||
|
||||
status_filter = request.GET.get('status')
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
|
|
@ -60,7 +71,7 @@ def ausschreibung_liste(request):
|
|||
if bid_manager_filter:
|
||||
qs = qs.filter(bid_manager=bid_manager_filter)
|
||||
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count
|
||||
qs = qs.select_related('bid_manager').annotate(
|
||||
aufgaben_total=Count('aufgaben', distinct=True),
|
||||
aufgaben_erledigt=Count(
|
||||
|
|
@ -75,6 +86,7 @@ def ausschreibung_liste(request):
|
|||
'status_choices': Ausschreibung.STATUS_CHOICES,
|
||||
'mitarbeiter': Mitarbeiter.objects.all(),
|
||||
'archiviert': archiviert,
|
||||
'q': q,
|
||||
'current_status': status_filter or '',
|
||||
'current_bid_manager': bid_manager_filter or '',
|
||||
'breadcrumbs': [{'label': 'Ausschreibungen', 'url': None}],
|
||||
|
|
@ -88,14 +100,28 @@ def ausschreibung_liste(request):
|
|||
def ausschreibung_neu(request):
|
||||
from .forms import AusschreibungForm
|
||||
|
||||
historisch = request.GET.get('historisch') == '1'
|
||||
historisch = (request.GET.get('historisch') == '1'
|
||||
or request.POST.get('historisch_erfassen') == '1')
|
||||
if request.method == 'POST':
|
||||
form = AusschreibungForm(request.POST)
|
||||
form = AusschreibungForm(request.POST, historisch=historisch)
|
||||
if form.is_valid():
|
||||
a = form.save()
|
||||
with transaction.atomic():
|
||||
a = form.save()
|
||||
if historisch:
|
||||
from vergabe_teilnahme.apps.nachbetrachtung.models import Nachbetrachtung
|
||||
|
||||
ergebnis = form.cleaned_data["ergebnis"]
|
||||
Nachbetrachtung.objects.create(ausschreibung=a, ergebnis=ergebnis)
|
||||
result_status = {
|
||||
"gewonnen": 10, "verloren": 11,
|
||||
"aufgehoben": 12, "zurueckgezogen": 13,
|
||||
}
|
||||
if ergebnis in result_status:
|
||||
a.status = result_status[ergebnis]
|
||||
a.save(update_fields=["status"])
|
||||
return redirect('ausschreibungen:detail', pk=a.pk)
|
||||
else:
|
||||
form = AusschreibungForm()
|
||||
form = AusschreibungForm(historisch=historisch)
|
||||
|
||||
return render(request, 'ausschreibungen/form.html', {
|
||||
'form': form,
|
||||
|
|
|
|||
|
|
@ -200,23 +200,35 @@ def suche(request):
|
|||
q = request.GET.get('q', '').strip()
|
||||
if not q or len(q) < 2:
|
||||
return HttpResponse('')
|
||||
from django.db.models import Q
|
||||
|
||||
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.dokumente.models import Dokument
|
||||
from vergabe_teilnahme.apps.marktbegleiter.models import Marktbegleiter
|
||||
from vergabe_teilnahme.apps.partner.models import Subunternehmer
|
||||
|
||||
ausschreibungen = Ausschreibung.objects.filter(titel__icontains=q)[:5]
|
||||
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]
|
||||
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])
|
||||
has_results = any([
|
||||
ausschreibungen, aufgaben, subunternehmer, 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,
|
||||
'aufgaben': aufgaben,
|
||||
'subunternehmer': subunternehmer,
|
||||
'ausschreibungen': ausschreibungen,
|
||||
'dokumente': dokumente,
|
||||
'nachweise': nachweise,
|
||||
|
|
|
|||
|
|
@ -98,5 +98,19 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-sm font-semibold text-slate-700">Ablaufende Nachweise (60 Tage)</h2>
|
||||
<span class="text-sm">{{ ablaufende_nachweise|length }}</span>
|
||||
</div>
|
||||
{% for n in ablaufende_nachweise %}
|
||||
<div class="flex items-center justify-between py-1 text-sm">
|
||||
<a href="{% url 'bibliothek:nachweis_detail' pk=n.pk %}" class="text-brand-700 hover:underline">{{ n.titel }}</a>
|
||||
<span class="{% if n.gueltig_bis < today %}text-red-600{% else %}text-amber-600{% endif %}">{{ n.gueltig_bis|date:"d.m.Y" }}</span>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-slate-400 text-sm">Keine ablaufenden Nachweise.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@
|
|||
<label class="form-label">Teilnahmeentscheidung</label>
|
||||
{{ form.teilnahmeentscheidung }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="id_ergebnis">Ergebnis</label>
|
||||
{{ form.ergebnis }}
|
||||
{{ form.ergebnis.errors }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Begründung</label>
|
||||
{{ form.entscheidungsbegruendung }}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@
|
|||
hx-get="{% url 'ausschreibungen:liste' %}"
|
||||
hx-target="#ausschreibungen-table"
|
||||
hx-push-url="true"
|
||||
hx-trigger="change from:select, change from:input[type=checkbox]"
|
||||
hx-trigger="change from:select, change from:input[type=checkbox], input changed delay:300ms from:input[type=search]"
|
||||
class="flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<label for="tender-search" class="form-label">Suche</label>
|
||||
<input id="tender-search" type="search" name="q" value="{{ q }}" class="form-input" placeholder="Titel oder Ausschreiber">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-input">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<link rel="stylesheet" href="{% static 'dist/main.css' %}">
|
||||
<script src="{% static 'vendor/alpinejs/alpine.min.js' %}" defer></script>
|
||||
</head>
|
||||
<body class="bg-paper-2 min-h-screen text-ink">
|
||||
<body class="bg-paper-2 min-h-screen text-ink" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
{% include "partials/topbar.html" %}
|
||||
|
||||
<div class="flex h-[calc(100vh-56px)]">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if aufgaben %}
|
||||
<div class="px-3 py-1 text-xs font-medium text-slate-400 uppercase tracking-wide">Aufgaben</div>
|
||||
{% for a in aufgaben %}
|
||||
<a href="{% url 'ausschreibungen:aufgaben:detail' ausschreibung_id=a.ausschreibung_id pk=a.pk %}" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">{{ a.titel }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if subunternehmer %}
|
||||
<div class="px-3 py-1 text-xs font-medium text-slate-400 uppercase tracking-wide">Subunternehmer</div>
|
||||
{% for s in subunternehmer %}
|
||||
<a href="{% url 'partner:su_detail' pk=s.pk %}" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">{{ s.name }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if dokumente %}
|
||||
<div class="px-3 py-1 text-xs font-medium text-slate-400 uppercase tracking-wide mt-1">Dokumente</div>
|
||||
{% for d in dokumente %}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ flavor: implementation
|
|||
owner: the-custodian
|
||||
topic_slug: vergabe-teilnahme
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-11"
|
||||
updated: "2026-09-27"
|
||||
related: [HFACT-WP-0001, REUSE-WP-0022]
|
||||
state_hub_workstream_id: "27a95f7b-cec4-50ca-8383-c1c95d996217"
|
||||
---
|
||||
|
|
@ -63,7 +63,7 @@ status: wait
|
|||
priority: high
|
||||
assignee: the-custodian
|
||||
depends_on: [VERGABE-WP-0018-T02, HFACT-WP-0001-T05]
|
||||
blocking_reason: "Image main-fa9f082 is tested and published. Await HFACT-WP-0001-T05 governed worker evidence plus exact Railiance tenant, deployment, database, storage, access and recovery acceptance."
|
||||
blocking_reason: "Await HFACT-WP-0001-T05 natural governed Railiance worker trace and VERGABE-WP-0019 complete invited-user/recovery acceptance. Pilot placement and one sign-in are already evidenced; main-fa9f082 is superseded."
|
||||
state_hub_task_id: "6e428152-aad6-5184-8b1d-8860ec0b9ae6"
|
||||
```
|
||||
|
||||
|
|
@ -118,3 +118,14 @@ worker dependency. This record continues to own factory-produced delivery
|
|||
acceptance, so a manually prepared customer pilot cannot falsely close HFACT
|
||||
worker proof. The old main-fa9f082 image predates the required login gate and
|
||||
must not be used as the invited-pilot release merely because its CI passed.
|
||||
|
||||
|
||||
## Loose-end review — 2026-09-27
|
||||
|
||||
HFACT-WP-0001-T05 remains wait in prj-helixforge-factory: protected runtime and
|
||||
synthetic proof exist, but upstream admission and natural queue/model execution
|
||||
are outstanding. The September 12 pilot deployment, September 24 founder login
|
||||
and September 25 database restore supersede the old missing-placement summary.
|
||||
They do not close governed-worker proof, two-user product acceptance, or coherent
|
||||
off-host recovery. T03 and this workplan remain blocked on those existing owners;
|
||||
no duplicate tasks were opened.
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ type: workplan
|
|||
title: "Admit the first invited company pilot with protected access and recoverable data"
|
||||
domain: communication
|
||||
repo: vergabe-teilnahme
|
||||
status: active
|
||||
status: blocked
|
||||
flavor: implementation
|
||||
owner: the-custodian
|
||||
topic_slug: vergabe-teilnahme
|
||||
created: "2026-09-11"
|
||||
updated: "2026-09-25"
|
||||
updated: "2026-09-27"
|
||||
related: [VERGABE-WP-0018, RAPPS-WP-0014, HFACT-WP-0001, CUST-WP-0071]
|
||||
state_hub_workstream_id: "85b5f304-d497-5570-bebf-3a3669ef6a7d"
|
||||
---
|
||||
|
|
@ -68,7 +68,8 @@ Ruff. Source acceptance is not native customer admission.
|
|||
|
||||
```task
|
||||
id: VERGABE-WP-0019-T03
|
||||
status: progress
|
||||
status: wait
|
||||
blocking_reason: "Await RAPPS-WP-0014-T03 coherent PostgreSQL/media/issue-state recovery, populated document round-trip and off-host backup evidence; September 25 proves database restore and restart only."
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
depends_on: [VERGABE-WP-0019-T02, RAPPS-WP-0014-T02, RAPPS-WP-0014-T03]
|
||||
|
|
@ -207,7 +208,8 @@ preceding service-login failure. It does not create a Django account or session.
|
|||
|
||||
```task
|
||||
id: VERGABE-WP-0019-T06
|
||||
status: progress
|
||||
status: wait
|
||||
blocking_reason: "Source and live SSO/recovery are delivered, and one founder sign-in passed September 24. Await invited recipient setup-to-welcome-to-workflow acceptance and a second ordinary user."
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
state_hub_task_id: "455b33f9-163a-5389-86a3-04000c32d176"
|
||||
|
|
@ -307,3 +309,18 @@ checks and six fresh anonymous Chromium checks pass, including actual provider
|
|||
logout POST and return to the portal without test overrides. Real-user identity
|
||||
switching is still awaiting operator evidence; no authenticated/MFA acceptance
|
||||
is inferred. Detailed receipt: railiance-apps/docs/evidence/2026-09-12-account-recovery-live.md.
|
||||
|
||||
|
||||
## Loose-end review — 2026-09-27
|
||||
|
||||
No further product implementation is identified in T03/T04/T06. T03 awaits
|
||||
railiance-apps recovery evidence for all three stores and an off-host copy;
|
||||
the September 25 age artifact stayed on the workstation and the two file
|
||||
claims were empty. A database restore does not close that contract. T04 needs
|
||||
Bernd Worsch and the company contact to demonstrate two ordinary users,
|
||||
document/task collaboration, revocation, restored workflow and support handoff.
|
||||
T06's September 24 founder login resolves the old issuer-login blocker, but
|
||||
cannot establish a fresh invited recipient's complete setup-to-company journey.
|
||||
Both previously progressing tasks now wait; the workplan is blocked. No new
|
||||
workplan or task was opened, and no live deployment or user impersonation was
|
||||
performed by this review.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ title: Dashboard und Ausschreibungen-CRUD
|
|||
status: finished
|
||||
phase: 4-of-12
|
||||
created: "2026-05-08"
|
||||
updated: "2026-09-27"
|
||||
depends_on: WP-0003
|
||||
domain: communication
|
||||
repo: vergabe-teilnahme
|
||||
|
|
@ -21,10 +22,13 @@ Entscheidungsregel-Auswertung, Archivierung und historische Erfassung.
|
|||
|
||||
---
|
||||
|
||||
## Dashboard-View mit Kacheln und Fristenliste
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T01
|
||||
title: Dashboard-View mit Kacheln und Fristenliste
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`ausschreibungen/views.py` — Dashboard-View:
|
||||
```python
|
||||
|
|
@ -67,12 +71,14 @@ 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`.
|
||||
```
|
||||
|
||||
## Ausschreibungsliste mit Filter und HTMX-Suche
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T02
|
||||
title: Ausschreibungsliste mit Filter und HTMX-Suche
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`ausschreibungen/views.py` — ListView:
|
||||
```python
|
||||
|
|
@ -107,12 +113,14 @@ Alle Filter-Änderungen: `hx-get="/ausschreibungen/" hx-target="#ausschreibungen
|
|||
|
||||
Tabelle: Titel, Ausschreiber, Status (status_badge), Abgabefrist (farbig wenn < 14 Tage),
|
||||
Verantwortlicher, Link zum Detail.
|
||||
```
|
||||
|
||||
## Ausschreibung anlegen — Form und View (UC-AS-01)
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T03
|
||||
title: Ausschreibung anlegen — Form und View (UC-AS-01)
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`ausschreibungen/forms.py`:
|
||||
```python
|
||||
|
|
@ -158,12 +166,14 @@ def ausschreibung_neu(request):
|
|||
`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).
|
||||
```
|
||||
|
||||
## Ausschreibung-Detailseite (Phase 1 — Stammdaten)
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T04
|
||||
title: Ausschreibung-Detailseite (Phase 1 — Stammdaten)
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`ausschreibungen/views.py` — Detailview:
|
||||
```python
|
||||
|
|
@ -192,12 +202,14 @@ def ausschreibung_detail(request, pk):
|
|||
- 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)
|
||||
```
|
||||
|
||||
## Ausschreibung bearbeiten (Edit-View) und Status inline wechseln
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T05
|
||||
title: Ausschreibung bearbeiten (Edit-View) und Status inline wechseln
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`ausschreibungen/views.py`:
|
||||
|
||||
|
|
@ -236,12 +248,14 @@ def ausschreibung_status(request, pk):
|
|||
</select>
|
||||
</div>
|
||||
```
|
||||
```
|
||||
|
||||
## Teilnahmeentscheidung-Seite (Phase 2, UC-AS-04)
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T06
|
||||
title: Teilnahmeentscheidung-Seite (Phase 2, UC-AS-04)
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`ausschreibungen/views.py` — Teilnahmeentscheidungs-View:
|
||||
```python
|
||||
|
|
@ -272,12 +286,14 @@ def ausschreibung_entscheidung(request, pk):
|
|||
- 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)
|
||||
```
|
||||
|
||||
## Entscheidungsregel-Auswertungs-Service
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T07
|
||||
title: Entscheidungsregel-Auswertungs-Service
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`vergabe_teilnahme/apps/ausschreibungen/services.py`:
|
||||
|
||||
|
|
@ -321,12 +337,14 @@ def _wende_regel_an(regel, ausschreibung):
|
|||
'begruendung': f'Restlaufzeit {delta} Tage unter Schwellenwert.'}
|
||||
return {'empfehlung': 'pruefen', 'begruendung': regel.begruendung or '—'}
|
||||
```
|
||||
```
|
||||
|
||||
## Ausschreibung archivieren und historisch erfassen (UC-AS-06, UC-AS-07)
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T08
|
||||
title: Ausschreibung archivieren und historisch erfassen (UC-AS-06, UC-AS-07)
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
**Archivieren:**
|
||||
```python
|
||||
|
|
@ -351,12 +369,14 @@ 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.
|
||||
```
|
||||
|
||||
## Globale Suchleiste — HTMX-Endpunkt und Ergebnis-Template
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T09
|
||||
title: Globale Suchleiste — HTMX-Endpunkt und Ergebnis-Template
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`core/views.py`:
|
||||
```python
|
||||
|
|
@ -397,12 +417,14 @@ def global_search(request):
|
|||
|
||||
URL: `path('suche/', core_views.global_search, name='global_search')`
|
||||
Topbar-Formular (aus WP-0003-T02) zeigt Ergebnisse in `#search-results`.
|
||||
```
|
||||
|
||||
## Ausschreibungen-URL-Verkabelung und App-Namespace
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T10
|
||||
title: Ausschreibungen-URL-Verkabelung und App-Namespace
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`vergabe_teilnahme/apps/ausschreibungen/urls.py`:
|
||||
```python
|
||||
|
|
@ -436,12 +458,14 @@ Jede referenzierte App-URL-Datei wird hier als leere Stub-Datei angelegt
|
|||
|
||||
Prüfe: `uv run manage.py check --deploy` → keine URL-Fehler.
|
||||
Smoke-Test: alle Hauptseiten (/ausschreibungen/, /ausschreibungen/neu/) laden ohne 500.
|
||||
```
|
||||
|
||||
## Ausschreibungs-Tests (Models und Views)
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T11
|
||||
title: Ausschreibungs-Tests (Models und Views)
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Erstelle `vergabe_teilnahme/apps/ausschreibungen/tests/`:
|
||||
|
||||
|
|
@ -468,12 +492,14 @@ class AusschreibungFactory(factory.django.DjangoModelFactory):
|
|||
ausschreiber = "Testausschreiber GmbH"
|
||||
status = 1
|
||||
```
|
||||
```
|
||||
|
||||
## Seed-Daten prüfen und Dashboard-Kacheln verifizieren
|
||||
|
||||
```task
|
||||
id: VT-WP-0004-T12
|
||||
title: Seed-Daten prüfen und Dashboard-Kacheln verifizieren
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Führe die gesamte Integrations-Smoke-Test-Sequenz durch:
|
||||
|
||||
|
|
@ -493,4 +519,37 @@ Führe die gesamte Integrations-Smoke-Test-Sequenz durch:
|
|||
10. `uv run pytest vergabe_teilnahme/apps/ausschreibungen/` → alle Tests grün
|
||||
|
||||
Erst wenn alle 10 Punkte erfüllt sind: Task als done markieren.
|
||||
```
|
||||
|
||||
## Completion review — 2026-09-27
|
||||
|
||||
The finished plan retained twelve stale todo blocks with nested Markdown fences.
|
||||
Reconciled the existing task IDs into one valid task block per section; no new
|
||||
tasks were created. T03–T07 and T10–T11 already had application implementations
|
||||
and model/view acceptance. T01 now includes the missing 60-day evidence-expiry
|
||||
card (including expired evidence). T02 now searches title and issuing authority
|
||||
alongside status/manager/archive filters with HTMX. T08 now validates and saves
|
||||
a historical result atomically in the existing Nachbetrachtung model and sets
|
||||
the corresponding terminal tender status. T09 now includes issuing authority,
|
||||
tasks and partners alongside the already implemented search categories.
|
||||
|
||||
The old illustrative fields are superseded by the current model: bid_manager
|
||||
replaces hauptverantwortung, and ergebnis belongs to Nachbetrachtung. Historical
|
||||
entry uses those models without a schema change. Existing approval, decision,
|
||||
archive, detail and URL implementations remain in place.
|
||||
|
||||
Validation: all 131 application tests pass on disposable SQLite; all 20 tender
|
||||
model/view tests also pass on disposable PostgreSQL 16. Vite build, Django system
|
||||
check and migration-drift check pass. Changed Python passes Ruff excluding the
|
||||
pre-existing E501 long lines. Local tests retain the pre-existing naive-datetime
|
||||
fixture and uncollected-static-directory warnings. T12 uses freshly migrated/seeded disposable PostgreSQL and local
|
||||
Chromium for dashboard/navigation, tender list/detail, HTMX search/status and
|
||||
historical creation. This is local source acceptance, not pilot-user evidence.
|
||||
|
||||
|
||||
The final T12 server-log review exposed a missing CSRF header on inline status
|
||||
POSTs. T05 now inherits the rendered Django CSRF token through base.html's
|
||||
HTMX headers. A CSRF-enforcing regression proves missing-token rejection and
|
||||
valid-token mutation. The Chromium check now changes to a different status,
|
||||
requires HTTP 200 for the POST, and checks persistence after reload; an unchanged
|
||||
seeded value is not accepted as proof. This supersedes the initial browser
|
||||
script's false-positive status assertion.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue