Add admin integration for ProjectRequest with approve/decline actions and custom ApproveActionForm

This commit is contained in:
Roman 2025-09-29 00:25:16 +02:00
parent 4e6906e318
commit 14717c8318
1 changed files with 147 additions and 0 deletions

View File

@ -2,6 +2,14 @@ import csv
from django.contrib import admin from django.contrib import admin
from django.http import HttpResponse 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 ( from .models import (
Account, Account,
@ -160,6 +168,145 @@ class ListAdmin(admin.ModelAdmin):
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] 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 # commented out because of the individual registering to control displays in admin panel
#admin.site.register([ #admin.site.register([