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 django.http import HttpResponse from .issue_backends import gitea_configured as _gitea_configured from .models import Aufgabe, AufgabenVerknuepfung, Bieterfrage, ExternalIssue AKTIVE_STATUS = [ 'offen', 'in_bearbeitung', 'wartend_intern', 'wartend_sub', 'wartend_ausschreiber', ] def _is_htmx(request): return request.headers.get('HX-Request') == 'true' # ─── Aufgaben ───────────────────────────────────────────────────────────────── def aufgaben_liste(request, ausschreibung_id=None): from vergabe_teilnahme.apps.accounts.models import Mitarbeiter heute = date.today() if ausschreibung_id: ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) qs = Aufgabe.objects.filter(ausschreibung=ausschreibung) else: ausschreibung = 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: qs = qs.filter(status=status_filter) typ_filter = request.GET.get('typ') if typ_filter: qs = qs.filter(typ=typ_filter) phase_filter = request.GET.get('phase') if phase_filter: qs = qs.filter(phase=phase_filter) verantwortlicher_filter = request.GET.get('verantwortlicher') if verantwortlicher_filter: qs = qs.filter(verantwortlicher=verantwortlicher_filter) if not ausschreibung_id and request.GET.get('nur_meine') == '1' and request.user.is_authenticated: qs = qs.filter(verantwortlicher=request.user) qs = qs.select_related('ausschreibung', 'verantwortlicher', 'los').order_by('prioritaet', 'frist') if ausschreibung: breadcrumbs = [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Aufgaben', 'url': None}, ] else: breadcrumbs = [{'label': 'Aufgaben', 'url': None}] ctx = { 'aufgaben': qs, 'ausschreibung': ausschreibung, 'ausschreibung_id': ausschreibung_id, 'status_choices': Aufgabe.STATUS_CHOICES, 'typ_choices': Aufgabe.TYP_CHOICES, 'mitarbeiter': Mitarbeiter.objects.all(), 'current_status': status_filter or '', 'current_typ': typ_filter or '', 'phase_choices': Aufgabe.PHASE_CHOICES, 'current_phase': phase_filter or '', 'current_verantwortlicher': verantwortlicher_filter or '', 'breadcrumbs': breadcrumbs, } if _is_htmx(request): return render(request, 'aufgaben/liste_partial.html', ctx) return render(request, 'aufgaben/liste.html', ctx) def aufgabe_neu(request, ausschreibung_id): from .forms import AufgabeForm ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) if request.method == 'POST': form = AufgabeForm(request.POST, ausschreibung=ausschreibung) if form.is_valid(): aufgabe = form.save(commit=False) aufgabe.ausschreibung = ausschreibung aufgabe.save() if _is_htmx(request): return render(request, 'aufgaben/partials/aufgabe_row.html', {'aufgabe': aufgabe, 'ausschreibung': ausschreibung}) return redirect('ausschreibungen:aufgaben:liste', ausschreibung_id=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, 'titel': 'Aufgabe anlegen', 'breadcrumbs': [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'}, {'label': 'Neu', 'url': None}, ], }) def aufgabe_bearbeiten(request, ausschreibung_id, pk): from .forms import AufgabeForm ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung) form = AufgabeForm(request.POST or None, instance=aufgabe, ausschreibung=ausschreibung) if request.method == 'POST' and form.is_valid(): form.save() return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk) return render(request, 'aufgaben/form.html', { 'form': form, 'ausschreibung': ausschreibung, 'aufgabe': aufgabe, 'titel': 'Aufgabe bearbeiten', 'breadcrumbs': [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'}, {'label': aufgabe.titel[:50], 'url': None}, ], }) def aufgabe_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': aufgabe.status = 'verworfen' aufgabe.save(update_fields=['status']) return redirect('ausschreibungen:aufgaben:liste', ausschreibung_id=ausschreibung_id) return render(request, 'aufgaben/loeschen_confirm.html', { 'aufgabe': aufgabe, 'ausschreibung': ausschreibung, 'breadcrumbs': [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'}, {'label': 'Löschen', 'url': None}, ], }) def aufgabe_detail(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/detail.html', { 'aufgabe': aufgabe, 'ausschreibung': ausschreibung, 'breadcrumbs': [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'}, {'label': aufgabe.titel[:50], 'url': None}, ], }) def aufgabe_status(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': neuer_status = request.POST.get('status') if neuer_status: aufgabe.status = neuer_status aufgabe.save(update_fields=['status']) return render(request, 'aufgaben/partials/aufgabe_row.html', {'aufgabe': aufgabe, 'ausschreibung': ausschreibung}) def aufgabe_ergebnis(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': aufgabe.ergebnis = request.POST.get('ergebnis', aufgabe.ergebnis) aufgabe.save(update_fields=['ergebnis']) return render(request, 'aufgaben/partials/aufgabe_row.html', {'aufgabe': aufgabe, 'ausschreibung': ausschreibung}) # ─── Bieterfragen ───────────────────────────────────────────────────────────── def bieterfragen_liste(request, ausschreibung_id): from vergabe_teilnahme.apps.accounts.models import Mitarbeiter ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) qs = Bieterfrage.objects.filter(ausschreibung=ausschreibung) status_filter = request.GET.get('status') if status_filter: qs = qs.filter(status=status_filter) prioritaet_filter = request.GET.get('prioritaet') if prioritaet_filter: qs = qs.filter(prioritaet=prioritaet_filter) verfasser_filter = request.GET.get('verfasser') if verfasser_filter: qs = qs.filter(verfasser=verfasser_filter) qs = qs.select_related('verfasser', 'anforderung').order_by('prioritaet', 'status') return render(request, 'aufgaben/bieterfragen_liste.html', { 'bieterfragen': qs, 'ausschreibung': ausschreibung, 'status_choices': Bieterfrage.STATUS_CHOICES, 'prioritaet_choices': Bieterfrage.PRIORITAET_CHOICES, 'mitarbeiter': Mitarbeiter.objects.all(), 'current_status': status_filter or '', 'breadcrumbs': [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Bieterfragen', 'url': None}, ], }) def bieterfrage_neu(request, ausschreibung_id): from .forms import BieterfragenForm from vergabe_teilnahme.apps.lose.models import Anforderung ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) initial = {} anforderung_id = request.GET.get('anforderung_id') if anforderung_id: try: anf = Anforderung.objects.get(pk=anforderung_id, ausschreibung=ausschreibung) initial['anforderung'] = anf except Anforderung.DoesNotExist: pass if request.method == 'POST': form = BieterfragenForm(request.POST, ausschreibung=ausschreibung) if form.is_valid(): bf = form.save(commit=False) bf.ausschreibung = ausschreibung bf.save() return redirect('ausschreibungen:bieterfragen:detail', ausschreibung_id=ausschreibung_id, pk=bf.pk) else: form = BieterfragenForm(initial=initial, ausschreibung=ausschreibung) return render(request, 'aufgaben/bieterfrage_form.html', { 'form': form, 'ausschreibung': ausschreibung, 'titel': 'Bieterfrage anlegen', 'breadcrumbs': [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Bieterfragen', 'url': f'/ausschreibungen/{ausschreibung_id}/bieterfragen/'}, {'label': 'Neu', 'url': None}, ], }) def bieterfrage_detail(request, ausschreibung_id, pk): ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) bieterfrage = get_object_or_404(Bieterfrage, pk=pk, ausschreibung=ausschreibung) return render(request, 'aufgaben/bieterfrage_detail.html', { 'bieterfrage': bieterfrage, 'ausschreibung': ausschreibung, 'breadcrumbs': [ {'label': 'Ausschreibungen', 'url': '/ausschreibungen/'}, {'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'}, {'label': 'Bieterfragen', 'url': f'/ausschreibungen/{ausschreibung_id}/bieterfragen/'}, {'label': str(bieterfrage)[:50], 'url': None}, ], }) def bieterfrage_status(request, ausschreibung_id, pk): ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) bieterfrage = get_object_or_404(Bieterfrage, pk=pk, ausschreibung=ausschreibung) if request.method == 'POST': neuer_status = request.POST.get('status') if neuer_status: bieterfrage.status = neuer_status if neuer_status == 'eingereicht' and not bieterfrage.einreichungsdatum: bieterfrage.einreichungsdatum = date.today() if neuer_status == 'eingearbeitet': bieterfrage.eingearbeitet = True bieterfrage.save() return redirect('ausschreibungen:bieterfragen:detail', ausschreibung_id=ausschreibung_id, pk=pk) def bieterfrage_antwort(request, ausschreibung_id, pk): ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) bieterfrage = get_object_or_404(Bieterfrage, pk=pk, ausschreibung=ausschreibung) if request.method == 'POST': bieterfrage.antwort = request.POST.get('antwort', bieterfrage.antwort) bieterfrage.auswirkung_angebot = request.POST.get('auswirkung_angebot', bieterfrage.auswirkung_angebot) if bieterfrage.status == 'eingereicht': bieterfrage.status = 'beantwortet' bieterfrage.save() return redirect('ausschreibungen:bieterfragen:detail', ausschreibung_id=ausschreibung_id, pk=pk) return render(request, 'aufgaben/bieterfrage_detail.html', { 'bieterfrage': bieterfrage, 'ausschreibung': ausschreibung, '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 from .issue_facade import lokales_issue_erstellen 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 if not issue: daten = lokales_issue_erstellen(aufgabe) ei.issue_facade_backend = 'local' ei.issue_facade_id = daten['issue_facade_id'] ei.issue_key = daten['issue_key'] ei.sync_status = daten['sync_status'] ei.save() if _is_htmx(request): return render(request, 'aufgaben/partials/external_issue_panel.html', { 'aufgabe': aufgabe, 'ausschreibung': ausschreibung, 'gitea_configured': _gitea_configured(), }) 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_push_remote(request, ausschreibung_id, pk): """Schiebt ein lokales Issue nach Gitea.""" from .issue_facade import an_gitea_delegieren ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung) ei = get_object_or_404(ExternalIssue, aufgabe=aufgabe) push_error = None if request.method == 'POST': try: daten = an_gitea_delegieren(aufgabe, ei) for k, v in daten.items(): setattr(ei, k, v) ei.save() except ValueError as exc: push_error = str(exc) if _is_htmx(request): return render(request, 'aufgaben/partials/external_issue_panel.html', { 'aufgabe': aufgabe, 'ausschreibung': ausschreibung, 'gitea_configured': _gitea_configured(), 'push_error': push_error, }) return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk) def external_issue_sync(request, ausschreibung_id, pk): """Aktualisiert sync_status aus dem Backend.""" from .issue_facade import status_synchronisieren ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung) ei = get_object_or_404(ExternalIssue, aufgabe=aufgabe) if request.method == 'POST': try: status_synchronisieren(ei) except Exception: ei.sync_status = 'error' ei.save(update_fields=['sync_status']) if _is_htmx(request): return render(request, 'aufgaben/partials/external_issue_panel.html', { 'aufgabe': aufgabe, 'ausschreibung': ausschreibung, 'gitea_configured': _gitea_configured(), }) 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, 'gitea_configured': _gitea_configured(), }) 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, 'gitea_configured': _gitea_configured(), }) return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk) return redirect('ausschreibungen:aufgaben:detail', ausschreibung_id=ausschreibung_id, pk=pk)