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, Project, HonoraryCertificate, Library, ELiterature, Software, IFG, Travel, Email, BusinessCard, List, Literature, ) def export_as_csv(self, request, queryset): meta = self.model._meta field_names = [field.name for field in meta.fields] response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) writer = csv.writer(response) writer.writerow(field_names) for obj in queryset: row = writer.writerow([getattr(obj, field) for field in field_names]) return response export_as_csv.short_description = "Ausgewähltes zu CSV exportieren" admin.site.add_action(export_as_csv) @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): save_as = True search_fields = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal') list_display = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal') fields = ('realname', 'email', 'granted', 'granted_date', 'mail_state', 'end_mail_send', 'survey_mail_send', 'survey_mail_date', 'name', 'description', 'pid', 'finance_id', 'start', 'end', 'otrs', 'plan', 'page', 'urls', 'group', 'location', 'participants_estimated', 'participants_real', 'insurance', 'insurance_technic', 'support', 'cost', 'account', 'granted_from', 'notes', 'intern_notes', 'status', 'project_of_year', 'end_quartal') # action = ['export_as_csv'] date_hierarchy = 'end' readonly_fields = ('end_quartal', 'project_of_year', 'pid', 'finance_id') class Media: js = ('dropdown/js/otrs_link.js',) @admin.register(BusinessCard) class BusinessCardAdmin(admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted') list_display_links = ('realname', 'service_id') # action = ['export_as_csv'] date_hierarchy = 'granted_date' readonly_fields = ['service_id'] class Media: js = ('dropdown/js/base.js',) @admin.register(Literature) class LiteratureAdmin(admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display_links = ('realname', 'service_id') date_hierarchy = 'granted_date' readonly_fields = ['service_id'] @admin.register(Account) class AccountAdmin(admin.ModelAdmin): save_as = True @admin.register(HonoraryCertificate) class HonoraryCertificateAdmin(admin.ModelAdmin): save_as = True search_fields = ['realname', 'granted', 'project__name', 'project__pid'] list_display = ('realname', 'granted','project') date_hierarchy = 'granted_date' autocomplete_fields = ['project'] class Media: js = ('dropdown/js/otrs_link.js',) @admin.register(Library, ELiterature, Software) class LibraryAdmin(admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display_links = ('realname', 'service_id') date_hierarchy = 'granted_date' readonly_fields = ['service_id'] exclude = ['type'] def get_queryset(self, request): return super().get_queryset(request).filter(type=self.model.TYPE) def formfield_for_dbfield(self, db_field, request, **kwargs): if db_field.name == 'library': kwargs['label'] = self.model.LIBRARY_LABEL kwargs['help_text'] = self.model.LIBRARY_HELP_TEXT elif db_field.name == 'duration': kwargs['help_text'] = self.model.DURATION_HELP_TEXT return super().formfield_for_dbfield(db_field, request, **kwargs) @admin.register(IFG) class IFGAdmin(admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display_links = ('realname', 'service_id') date_hierarchy = 'granted_date' readonly_fields = ['service_id'] @admin.register(Travel) class TravelAdmin(admin.ModelAdmin): save_as = True search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid'] list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project', 'project_end_quartal') list_display_links = ('realname', 'project') date_hierarchy = 'project_end' autocomplete_fields = ['project'] readonly_fields = ['service_id', 'project_end', 'project_end_quartal'] class Media: js = ('dropdown/js/otrs_link.js',) @admin.register(Email) class EmailAdmin(admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display_links = ('realname', 'service_id') date_hierarchy = 'granted_date' radio_fields = {'adult': admin.VERTICAL} readonly_fields = ['service_id'] class Media: js = ('dropdown/js/base.js',) @admin.register(List) class ListAdmin(admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display_links = ('realname', 'service_id') 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([ # Account, # HonoraryCertificate, # Library, # IFG, # Travel, # Email, # List, # ])