diff --git a/input/admin.py b/input/admin.py index 7da1e6f..555f6a0 100755 --- a/input/admin.py +++ b/input/admin.py @@ -2,6 +2,14 @@ import csv from django.contrib import admin from django.http import HttpResponse +from .models import ProjectRequest, ProjectsDeclined +from .forms import ProjectRequestAdminForm +from django.db import models +from django import forms +from django.contrib import messages +from .services import approve_project_request, decline_project_request +from django.contrib.admin.helpers import ActionForm + from .models import ( Account, @@ -160,6 +168,145 @@ class ListAdmin(admin.ModelAdmin): date_hierarchy = 'granted_date' readonly_fields = ['service_id'] + +class ApproveActionForm(ActionForm): + """ + Extra control rendered next to the bulk actions dropdown. + Admin must choose an Account (Kostenstelle) when approving requests. + """ + account_code = forms.ModelChoiceField(queryset=Account.objects.all(), required=True, + label='Kostenstelle (Account)') + + +@admin.register(ProjectRequest) +class ProjectRequestAdmin(admin.ModelAdmin): + """ + Admin for incoming project requests (< 1000 EUR). + + - Uses a custom ModelForm to render JSON-backed fields (checkbox multiselects). + - Provides two bulk actions: approve (moves to Projects) and decline (moves to Projects_declined). + - Enforces that an Account must be selected for approval. + """ + form = ProjectRequestAdminForm + save_as = True + list_display = ('name', 'realname', 'email', 'start', 'end', 'cost', 'decision') + list_filter = ('decision', 'insurance',) + search_fields = ('name', 'realname', 'email') + readonly_fields = ('decision', 'decision_date', 'decided_by') + + # Make text areas more comfortable to edit in admin + formfield_overrides = { + # Increase rows for TextField edit widgets + models.TextField: {'widget': forms.Textarea(attrs={'rows': 5, 'cols': 80})}, + } + + # Show actions at the top (common UX in Django admin) + actions = ['approve_selected', 'decline_selected'] + actions_on_top = True + actions_on_bottom = False + action_form = ApproveActionForm + + @admin.action(description='Bewilligen → nach „Projects“') + def approve_selected(self, request, queryset): + """ + Bulk-approve selected requests: + - Requires an Account (Kostenstelle) chosen via ApproveActionForm. + - Delegates the creation/move logic to the service layer. + - Counts successes and reports via Django messages. + """ + account_pk = request.POST.get('account_code') + if not account_pk: + self.message_user( + request, + 'Bitte eine Kostenstelle auswählen.', + level=messages.ERROR + ) + return + + try: + account = Account.objects.get(pk=account_pk) + except Account.DoesNotExist: + self.message_user( + request, + f'Unbekannte Kostenstelle (Account pk={account_pk}).', + level=messages.ERROR + ) + return + + decided_by = request.user.get_username() + ok, failed = 0, 0 + + for req in queryset: + try: + # Service call is atomic and locks the row (select_for_update) + approve_project_request(req.id, decided_by, account.code) + ok += 1 + except Exception as exc: + failed += 1 + # Show a concise per-object error; keep details in server logs if needed + self.message_user( + request, + f'Fehler beim Bewilligen von „{req}“: {exc}', + level=messages.ERROR + ) + + if ok: + self.message_user( + request, + f'{ok} Antrag/Anträge bewilligt und als Project angelegt.', + level=messages.SUCCESS + ) + if failed: + self.message_user( + request, + f'{failed} Antrag/Anträge konnten nicht bewilligt werden.', + level=messages.WARNING + ) + + @admin.action(description='Ablehnen → nach „Projects_declined“') + def decline_selected(self, request, queryset): + """ + Bulk-decline selected requests: + - Archives a minimal snapshot to Projects_declined (per ticket). + - Delegates the move logic to the service layer. + """ + ok, failed = 0, 0 + + for req in queryset: + try: + decline_project_request(req.id, reason='') + ok += 1 + except Exception as exc: + failed += 1 + self.message_user( + request, + f'Fehler beim Ablehnen von „{req}“: {exc}', + level=messages.ERROR + ) + + if ok: + self.message_user( + request, + f'{ok} Antrag/Anträge abgelehnt → „Projects_declined“', + level=messages.WARNING + ) + if failed: + self.message_user( + request, + f'{failed} Antrag/Anträge konnten nicht abgelehnt werden.', + level=messages.ERROR + ) + + +@admin.register(ProjectsDeclined) +class ProjectsDeclinedAdmin(admin.ModelAdmin): + """ + Read-only-ish list of declined requests for auditing. + """ + list_display = ('name', 'realname', 'email', 'decision_date') + search_fields = ('name', 'realname', 'email') + date_hierarchy = 'decision_date' + # commented out because of the individual registering to control displays in admin panel #admin.site.register([