diff --git a/input/admin.py b/input/admin.py index 181a994..b545c0b 100755 --- a/input/admin.py +++ b/input/admin.py @@ -1,10 +1,8 @@ import csv from django.contrib import admin -from django.http import HttpResponse from django.db import models -from django import forms -from django.contrib.admin.helpers import ActionForm +from django.http import HttpResponse from .forms import BaseProjectForm from .models import ( @@ -27,8 +25,25 @@ from .models import ( ) -def export_as_csv(self, request, queryset): +class RequestURLBeforeInternNotesMixin: + """ + Ensures that 'request_url' appears directly before 'intern_notes'. + Works whether 'fields' is explicitly defined or derived from the Model/Form. + """ + def get_fields(self, request, obj=None): + fields = [*super().get_fields(request, obj)] + + fields.remove('request_url') + + index = fields.index('intern_notes') + + fields.insert(index, 'request_url') + + return fields + + +def export_as_csv(self, request, queryset): meta = self.model._meta field_names = [field.name for field in meta.fields] @@ -42,6 +57,7 @@ def export_as_csv(self, request, queryset): return response + export_as_csv.short_description = "Ausgewähltes zu CSV exportieren" admin.site.add_action(export_as_csv) @@ -73,6 +89,7 @@ class ProjectAdminForm(BaseProjectForm): @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): + save_as = True form = ProjectAdminForm 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') @@ -145,7 +162,7 @@ class ProjectDeclinedAdmin(ProjectAdmin): @admin.register(BusinessCard) -class BusinessCardAdmin(admin.ModelAdmin): +class BusinessCardAdmin(RequestURLBeforeInternNotesMixin, 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') @@ -153,11 +170,13 @@ class BusinessCardAdmin(admin.ModelAdmin): # action = ['export_as_csv'] date_hierarchy = 'granted_date' readonly_fields = ['service_id'] + class Media: - js = ('dropdown/js/base.js',) + js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js') + @admin.register(Literature) -class LiteratureAdmin(admin.ModelAdmin): +class LiteratureAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') @@ -165,6 +184,9 @@ class LiteratureAdmin(admin.ModelAdmin): date_hierarchy = 'granted_date' readonly_fields = ['service_id'] + class Media: + js = ('dropdown/js/otrs_link.js',) + @admin.register(Account) class AccountAdmin(admin.ModelAdmin): @@ -175,15 +197,16 @@ class AccountAdmin(admin.ModelAdmin): class HonoraryCertificateAdmin(admin.ModelAdmin): save_as = True search_fields = ['realname', 'granted', 'project__name', 'project__pid'] - list_display = ('realname', 'granted','project') + 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): +class LibraryAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date') @@ -192,6 +215,9 @@ class LibraryAdmin(admin.ModelAdmin): readonly_fields = ['service_id'] exclude = ['type'] + class Media: + js = ('dropdown/js/otrs_link.js',) + def get_queryset(self, request): return super().get_queryset(request).filter(type=self.model.TYPE) @@ -207,7 +233,7 @@ class LibraryAdmin(admin.ModelAdmin): @admin.register(IFG) -class IFGAdmin(admin.ModelAdmin): +class IFGAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date') @@ -215,11 +241,16 @@ class IFGAdmin(admin.ModelAdmin): date_hierarchy = 'granted_date' readonly_fields = ['service_id'] + class Media: + js = ('dropdown/js/otrs_link.js',) + + @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 = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project', + 'project_end_quartal') list_display_links = ('realname', 'project') date_hierarchy = 'project_end' autocomplete_fields = ['project'] @@ -230,7 +261,7 @@ class TravelAdmin(admin.ModelAdmin): @admin.register(Email) -class EmailAdmin(admin.ModelAdmin): +class EmailAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): save_as = True search_fields = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') @@ -238,24 +269,16 @@ class EmailAdmin(admin.ModelAdmin): date_hierarchy = 'granted_date' radio_fields = {'adult': admin.VERTICAL} readonly_fields = ['service_id'] + class Media: - js = ('dropdown/js/base.js',) + js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js') @admin.register(List) -class ListAdmin(admin.ModelAdmin): +class ListAdmin(RequestURLBeforeInternNotesMixin, 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)') diff --git a/input/migrations/0103_add_request_url.py b/input/migrations/0103_add_request_url.py new file mode 100644 index 0000000..d9ba4d6 --- /dev/null +++ b/input/migrations/0103_add_request_url.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.5 on 2025-10-08 10:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('input', '0102_project_request_declined'), + ] + + operations = [ + migrations.AddField( + model_name='businesscard', + name='request_url', + field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'), + ), + migrations.AddField( + model_name='email', + name='request_url', + field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'), + ), + migrations.AddField( + model_name='ifg', + name='request_url', + field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'), + ), + migrations.AddField( + model_name='library', + name='request_url', + field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'), + ), + migrations.AddField( + model_name='list', + name='request_url', + field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'), + ), + migrations.AddField( + model_name='literature', + name='request_url', + field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'), + ), + ] diff --git a/input/models.py b/input/models.py index fc9f566..3622a04 100755 --- a/input/models.py +++ b/input/models.py @@ -28,6 +28,20 @@ class TermsConsentMixin(models.Model): abstract = True +class RequestUrlMixin(models.Model): + """ + Abstract mixin for adding an OTRS request URL field to admin models. + + This field stores a direct link to the related OTRS ticket. + Note: OTRS links may contain semicolons, which must not be URL-encoded. + """ + + request_url = models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)') + + class Meta: + abstract = True + + class Volunteer(models.Model): realname = models.CharField(max_length=200, null=True, verbose_name='Realname', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', default='') @@ -370,7 +384,7 @@ def get_project_end(sender, instance, **kwargs): # abstract base class for Library and IFG -class Grant(Extern): +class Grant(RequestUrlMixin, Extern): cost = models.CharField(max_length=10, verbose_name='Kosten', help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.') notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen', @@ -496,7 +510,7 @@ DOMAIN_CHOICES = { } -class Domain(Extern): +class Domain(RequestUrlMixin, Extern): domain = models.CharField(max_length=10, choices=DOMAIN_CHOICES.items(), default='PEDIA') @@ -553,7 +567,7 @@ BC_VARIANT = { } -class BusinessCard(TermsConsentMixin, Extern): +class BusinessCard(RequestUrlMixin, TermsConsentMixin, Extern): project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(), default='PEDIA', verbose_name='Wikimedia-Projekt', help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?')