feat(aufgaben): Verknüpfungen, implizite Fälligkeit, Issue-Facade (WP-0015)

- Aufgabe.erstellt_am für implizite 7-Tage-Fälligkeit
- frist_effektiv property; ist_ueberfaellig nutzt sie
- AufgabenVerknuepfung (GenericForeignKey) mit HTMX-Panel
- ExternalIssue (OneToOne) mit IssueAdapter-ABC und HTMX-Panel
- link_registry.py und issue_facade.py als zentrale Registries
- 8 neue Tests, 76 gesamt grün

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-05-14 04:18:28 +02:00
parent a0c0ddf2eb
commit 816c281f6a
19 changed files with 713 additions and 19 deletions

View file

@ -5,7 +5,7 @@ from django.urls import reverse
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
from vergabe_teilnahme.apps.lose.tests import AnforderungFactory
from .models import Aufgabe, Bieterfrage
from .models import Aufgabe, AufgabenVerknuepfung, Bieterfrage, ExternalIssue
class AufgabeFactory(factory.django.DjangoModelFactory):
@ -94,3 +94,110 @@ def test_bieterfrage_antwort_speichern(client):
bf.refresh_from_db()
assert bf.antwort == 'Die Antwort lautet 42.'
assert bf.status == 'beantwortet'
# ─── Implizite Fälligkeit ─────────────────────────────────────────────────────
@pytest.mark.django_db
def test_frist_effektiv_mit_frist():
from datetime import date
aufgabe = AufgabeFactory(frist=date(2026, 6, 1))
assert aufgabe.frist_effektiv == date(2026, 6, 1)
@pytest.mark.django_db
def test_frist_effektiv_ohne_frist():
from datetime import timedelta
from django.utils import timezone
aufgabe = AufgabeFactory(frist=None)
expected = (aufgabe.erstellt_am + timedelta(days=7)).date()
assert aufgabe.frist_effektiv == expected
@pytest.mark.django_db
def test_ueberfaellig_ohne_frist_nach_7_tagen(client):
from datetime import timedelta
from django.utils import timezone
a = AusschreibungFactory()
alte_erstellung = timezone.now() - timedelta(days=8)
aufgabe = AufgabeFactory(ausschreibung=a, frist=None, status='offen')
Aufgabe.objects.filter(pk=aufgabe.pk).update(erstellt_am=alte_erstellung)
url = reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': a.pk})
client.get(url)
aufgabe.refresh_from_db()
assert aufgabe.status == 'ueberfaellig'
# ─── Aufgaben-Verknüpfungen ───────────────────────────────────────────────────
@pytest.mark.django_db
def test_aufgaben_verknuepfung_erstellen(client):
from django.contrib.contenttypes.models import ContentType
aufgabe = AufgabeFactory()
anf = AnforderungFactory(ausschreibung=aufgabe.ausschreibung)
ct = ContentType.objects.get_for_model(anf)
url = reverse('ausschreibungen:aufgaben:verknuepfung_neu',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {
'ziel_typ': ct.pk,
'ziel_id': anf.pk,
'kommentar': 'Testverknüpfung',
}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert AufgabenVerknuepfung.objects.filter(aufgabe=aufgabe, object_id=anf.pk).exists()
@pytest.mark.django_db
def test_aufgaben_verknuepfung_loeschen(client):
from django.contrib.contenttypes.models import ContentType
aufgabe = AufgabeFactory()
anf = AnforderungFactory(ausschreibung=aufgabe.ausschreibung)
ct = ContentType.objects.get_for_model(anf)
vk = AufgabenVerknuepfung.objects.create(
aufgabe=aufgabe, content_type=ct, object_id=anf.pk
)
url = reverse('ausschreibungen:aufgaben:verknuepfung_loeschen',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk, 'vk_pk': vk.pk})
response = client.post(url, {}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert not AufgabenVerknuepfung.objects.filter(pk=vk.pk).exists()
# ─── ExternalIssue ────────────────────────────────────────────────────────────
@pytest.mark.django_db
def test_external_issue_erstellen(client):
aufgabe = AufgabeFactory()
url = reverse('ausschreibungen:aufgaben:external_issue',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {
'system': 'github',
'issue_url': 'https://github.com/org/repo/issues/42',
'issue_key': 'GH-42',
'notizen': '',
}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert ExternalIssue.objects.filter(aufgabe=aufgabe, system='github').exists()
@pytest.mark.django_db
def test_external_issue_loeschen(client):
aufgabe = AufgabeFactory()
ei = ExternalIssue.objects.create(
aufgabe=aufgabe, system='jira', issue_key='PROJ-1'
)
url = reverse('ausschreibungen:aufgaben:external_issue_loeschen',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert not ExternalIssue.objects.filter(pk=ei.pk).exists()
@pytest.mark.django_db
def test_issue_adapter_interface():
from .issue_facade import get_adapter
assert get_adapter('github') is None
assert get_adapter('nichtexistent') is None