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:
parent
a0c0ddf2eb
commit
816c281f6a
19 changed files with 713 additions and 19 deletions
|
|
@ -1,10 +1,13 @@
|
|||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
|
||||
from .models import Aufgabe, Bieterfrage
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .models import Aufgabe, AufgabenVerknuepfung, Bieterfrage, ExternalIssue
|
||||
|
||||
AKTIVE_STATUS = [
|
||||
'offen', 'in_bearbeitung', 'wartend_intern', 'wartend_sub', 'wartend_ausschreiber',
|
||||
|
|
@ -31,6 +34,12 @@ def aufgaben_liste(request, ausschreibung_id=None):
|
|||
qs = Aufgabe.objects.select_related('ausschreibung').all()
|
||||
|
||||
qs.filter(frist__lt=heute, status__in=AKTIVE_STATUS).update(status='ueberfaellig')
|
||||
implizite_grenze = timezone.now() - timedelta(days=7)
|
||||
qs.filter(
|
||||
frist__isnull=True,
|
||||
erstellt_am__lt=implizite_grenze,
|
||||
status__in=AKTIVE_STATUS,
|
||||
).update(status='ueberfaellig')
|
||||
|
||||
status_filter = request.GET.get('status')
|
||||
if status_filter:
|
||||
|
|
@ -99,6 +108,11 @@ def aufgabe_neu(request, ausschreibung_id):
|
|||
else:
|
||||
form = AufgabeForm(ausschreibung=ausschreibung)
|
||||
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/aufgabe_form_inline.html', {
|
||||
'form': form,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
return render(request, 'aufgaben/form.html', {
|
||||
'form': form,
|
||||
'ausschreibung': ausschreibung,
|
||||
|
|
@ -316,3 +330,138 @@ def bieterfrage_antwort(request, ausschreibung_id, pk):
|
|||
'show_antwort_form': True,
|
||||
'breadcrumbs': [],
|
||||
})
|
||||
|
||||
|
||||
# ─── Aufgaben-Verknüpfungen ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def verknuepfung_neu(request, ausschreibung_id, pk):
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from .forms import AufgabenVerknuepfungForm
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AufgabenVerknuepfungForm(request.POST)
|
||||
if form.is_valid():
|
||||
ct_id = form.cleaned_data['ziel_typ']
|
||||
obj_id = form.cleaned_data['ziel_id']
|
||||
ct = get_object_or_404(ContentType, pk=ct_id)
|
||||
vk = AufgabenVerknuepfung.objects.create(
|
||||
aufgabe=aufgabe,
|
||||
content_type=ct,
|
||||
object_id=obj_id,
|
||||
kommentar=form.cleaned_data['kommentar'],
|
||||
)
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/verknuepfung_row.html',
|
||||
{'v': vk, 'aufgabe': aufgabe, 'ausschreibung': ausschreibung})
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
else:
|
||||
form = AufgabenVerknuepfungForm()
|
||||
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/verknuepfung_form_inline.html', {
|
||||
'form': form,
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
|
||||
|
||||
def verknuepfung_objekte(request, ausschreibung_id, pk):
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ct_id = request.GET.get('ziel_typ') or request.GET.get('ct_id')
|
||||
objekte = []
|
||||
if ct_id:
|
||||
try:
|
||||
ct = ContentType.objects.get(pk=ct_id)
|
||||
model_class = ct.model_class()
|
||||
if model_class is not None:
|
||||
objekte = list(model_class.objects.all()[:200])
|
||||
except ContentType.DoesNotExist:
|
||||
pass
|
||||
return render(request, 'aufgaben/partials/verknuepfung_objekte_select.html', {
|
||||
'objekte': objekte,
|
||||
'ct_id': ct_id,
|
||||
})
|
||||
|
||||
|
||||
def verknuepfung_loeschen(request, ausschreibung_id, pk, vk_pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
vk = get_object_or_404(AufgabenVerknuepfung, pk=vk_pk, aufgabe=aufgabe)
|
||||
if request.method == 'POST':
|
||||
vk.delete()
|
||||
if _is_htmx(request):
|
||||
return HttpResponse('')
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
return render(request, 'aufgaben/partials/verknuepfung_loeschen_confirm.html', {
|
||||
'v': vk,
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
|
||||
|
||||
# ─── External Issue ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def external_issue_bearbeiten(request, ausschreibung_id, pk):
|
||||
from .forms import ExternalIssueForm
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
issue = getattr(aufgabe, 'external_issue', None)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ExternalIssueForm(request.POST, instance=issue)
|
||||
if form.is_valid():
|
||||
ei = form.save(commit=False)
|
||||
ei.aufgabe = aufgabe
|
||||
ei.save()
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/external_issue_panel.html', {
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
else:
|
||||
form = ExternalIssueForm(instance=issue)
|
||||
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/external_issue_form.html', {
|
||||
'form': form,
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
|
||||
|
||||
def external_issue_panel(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
return render(request, 'aufgaben/partials/external_issue_panel.html', {
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
|
||||
|
||||
def external_issue_loeschen(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST':
|
||||
ExternalIssue.objects.filter(aufgabe=aufgabe).delete()
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/external_issue_panel.html', {
|
||||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue