Merge branch 'feature/add-request-url-to-services' of gitlab.cosmocode.de:wikimedia/foerderbarometer into cosmocode

This commit is contained in:
Oliver Zander 2025-10-16 16:31:35 +02:00
commit 6a36b293f4
3 changed files with 106 additions and 26 deletions

View File

@ -1,10 +1,8 @@
import csv import csv
from django.contrib import admin from django.contrib import admin
from django.http import HttpResponse
from django.db import models from django.db import models
from django import forms from django.http import HttpResponse
from django.contrib.admin.helpers import ActionForm
from .forms import BaseProjectForm from .forms import BaseProjectForm
from .models import ( 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 meta = self.model._meta
field_names = [field.name for field in meta.fields] field_names = [field.name for field in meta.fields]
@ -42,6 +57,7 @@ def export_as_csv(self, request, queryset):
return response return response
export_as_csv.short_description = "Ausgewähltes zu CSV exportieren" export_as_csv.short_description = "Ausgewähltes zu CSV exportieren"
admin.site.add_action(export_as_csv) admin.site.add_action(export_as_csv)
@ -73,6 +89,7 @@ class ProjectAdminForm(BaseProjectForm):
@admin.register(Project) @admin.register(Project)
class ProjectAdmin(admin.ModelAdmin): class ProjectAdmin(admin.ModelAdmin):
save_as = True
form = ProjectAdminForm form = ProjectAdminForm
search_fields = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal') 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') 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) @admin.register(BusinessCard)
class BusinessCardAdmin(admin.ModelAdmin): class BusinessCardAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project') search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
@ -153,11 +170,13 @@ class BusinessCardAdmin(admin.ModelAdmin):
# action = ['export_as_csv'] # action = ['export_as_csv']
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media: class Media:
js = ('dropdown/js/base.js',) js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js')
@admin.register(Literature) @admin.register(Literature)
class LiteratureAdmin(admin.ModelAdmin): class LiteratureAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
@ -165,6 +184,9 @@ class LiteratureAdmin(admin.ModelAdmin):
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Account) @admin.register(Account)
class AccountAdmin(admin.ModelAdmin): class AccountAdmin(admin.ModelAdmin):
@ -175,15 +197,16 @@ class AccountAdmin(admin.ModelAdmin):
class HonoraryCertificateAdmin(admin.ModelAdmin): class HonoraryCertificateAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ['realname', 'granted', 'project__name', 'project__pid'] search_fields = ['realname', 'granted', 'project__name', 'project__pid']
list_display = ('realname', 'granted','project') list_display = ('realname', 'granted', 'project')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
autocomplete_fields = ['project'] autocomplete_fields = ['project']
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
@admin.register(Library, ELiterature, Software) @admin.register(Library, ELiterature, Software)
class LibraryAdmin(admin.ModelAdmin): class LibraryAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('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'] readonly_fields = ['service_id']
exclude = ['type'] exclude = ['type']
class Media:
js = ('dropdown/js/otrs_link.js',)
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).filter(type=self.model.TYPE) return super().get_queryset(request).filter(type=self.model.TYPE)
@ -207,7 +233,7 @@ class LibraryAdmin(admin.ModelAdmin):
@admin.register(IFG) @admin.register(IFG)
class IFGAdmin(admin.ModelAdmin): class IFGAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('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' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Travel) @admin.register(Travel)
class TravelAdmin(admin.ModelAdmin): class TravelAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid'] 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') list_display_links = ('realname', 'project')
date_hierarchy = 'project_end' date_hierarchy = 'project_end'
autocomplete_fields = ['project'] autocomplete_fields = ['project']
@ -230,7 +261,7 @@ class TravelAdmin(admin.ModelAdmin):
@admin.register(Email) @admin.register(Email)
class EmailAdmin(admin.ModelAdmin): class EmailAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
@ -238,24 +269,16 @@ class EmailAdmin(admin.ModelAdmin):
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
radio_fields = {'adult': admin.VERTICAL} radio_fields = {'adult': admin.VERTICAL}
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media: class Media:
js = ('dropdown/js/base.js',) js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js')
@admin.register(List) @admin.register(List)
class ListAdmin(admin.ModelAdmin): class ListAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
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)')

View File

@ -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)'),
),
]

View File

@ -28,6 +28,20 @@ class TermsConsentMixin(models.Model):
abstract = True 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): class Volunteer(models.Model):
realname = models.CharField(max_length=200, null=True, verbose_name='Realname', realname = models.CharField(max_length=200, null=True, verbose_name='Realname',
help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', default='') 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 # abstract base class for Library and IFG
class Grant(Extern): class Grant(RequestUrlMixin, Extern):
cost = models.CharField(max_length=10, verbose_name='Kosten', cost = models.CharField(max_length=10, verbose_name='Kosten',
help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.') help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen', 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, domain = models.CharField(max_length=10,
choices=DOMAIN_CHOICES.items(), choices=DOMAIN_CHOICES.items(),
default='PEDIA') 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(), project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(),
default='PEDIA', verbose_name='Wikimedia-Projekt', default='PEDIA', verbose_name='Wikimedia-Projekt',
help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?') help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?')