refactor(forms): unify forms with BaseApplicationForm and CommonOrderMixin for consistent fields and order

This commit is contained in:
Roman 2025-08-31 23:38:22 +02:00
parent eea08e6075
commit 98d1ae9284
1 changed files with 81 additions and 13 deletions

View File

@ -1,9 +1,10 @@
from django.conf import settings from django.conf import settings
from django.forms import ModelForm, ChoiceField, RadioSelect, BooleanField from django.forms import ModelForm, ChoiceField, RadioSelect, BooleanField, CharField, EmailField
from django.contrib.admin.widgets import AdminDateWidget from django.contrib.admin.widgets import AdminDateWidget
from django.forms.renderers import DjangoTemplates from django.forms.renderers import DjangoTemplates
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django import forms
from .models import ( from .models import (
TYPE_CHOICES, TYPE_CHOICES,
@ -33,20 +34,61 @@ class FdbForm(ModelForm):
class ProjectForm(FdbForm): class ProjectForm(FdbForm):
# start = DateField(widget=AdminDateWidget()) # start = DateField(widget=AdminDateWidget())
class Meta: class Meta:
model = Project model = Project
exclude = ('pid', 'project_of_year', 'finance_id','granted', 'granted_date', 'realname', 'email',\ exclude = ('pid', 'project_of_year', 'finance_id', 'granted', 'granted_date', 'realname', 'email', \
'end_mail_send', 'status', 'persons', 'survey_mail_date', 'mail_state') 'end_mail_send', 'status', 'persons', 'survey_mail_date', 'mail_state')
widgets = {'start': AdminDateWidget(), widgets = {'start': AdminDateWidget(),
'end': AdminDateWidget(),} 'end': AdminDateWidget(), }
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
class CommonOrderMixin(forms.Form):
"""
Ensures a consistent field order for all forms that inherit from this mixin.
The goal is to always render:
- "realname" and "email" fields at the top,
- all other fields in the middle (in the order Django provides),
- "check" (terms/privacy checkbox) at the bottom.
This keeps the UX consistent across all forms without repeating field_order logic.
"""
# Fields that should always appear first in the form
field_order_head = ('realname', 'email')
# Fields that should always appear last in the form
field_order_tail = ('check',) # rename if your checkbox field is called differently
def __init__(self, *args, **kwargs):
"""
Override the default form initialization to reorder fields.
The method:
1. Collects all existing field names.
2. Puts the 'head' fields first (if they exist in this form).
3. Keeps all other fields in the middle.
4. Puts the 'tail' fields last (if they exist in this form).
"""
super().__init__(*args, **kwargs)
existing = list(self.fields.keys())
# Select only fields that actually exist in this form
head = [f for f in self.field_order_head if f in self.fields]
tail = [f for f in self.field_order_tail if f in self.fields]
# All other fields that are not explicitly in head or tail
middle = [f for f in existing if f not in (*head, *tail)]
# Apply the new order to the form fields
self.order_fields(head + middle + tail)
class ExternForm(FdbForm): class ExternForm(FdbForm):
choice = ChoiceField(choices=TYPE_CHOICES.items(), widget=RadioSelect, choice = ChoiceField(choices=TYPE_CHOICES.items(), widget=RadioSelect,
@ -79,7 +121,33 @@ HOTEL_CHOICES = {'TRUE': mark_safe('Hotelzimmer benötigt'),
} }
class TravelForm(FdbForm): class BaseApplicationForm(FdbForm):
"""
Base form for all external applications.
- Adds standard fields that must appear in every form:
* realname (applicant's full name)
* email (contact address for notifications)
* check (confirmation of data protection and funding policy)
- Ensures consistency across all application types.
"""
realname = CharField(
label="Realname",
required=True,
help_text="Bitte gib deinen Vor- und Nachnamen ein."
)
email = EmailField(
label="E-Mail-Adresse",
required=True,
help_text="Bitte gib deine E-Mail-Adresse ein, damit dich <br> Wikimedia Deutschland bei Rückfragen oder für <br> die Zusage kontaktieren kann."
)
check = BooleanField(required=True,
label=format_html(
"Ich stimme den <a href='{}' target='_blank' rel='noopener'>Datenschutzbestimmungen</a> und der<br> <a href='{}' target='_blank' rel='noopener'>Richtlinie zur Förderung der Communitys</a> zu",
settings.DATAPROTECTION, settings.FOERDERRICHTLINIEN))
class TravelForm(BaseApplicationForm, CommonOrderMixin):
# TODO: add some javascript to show/hide other-field # TODO: add some javascript to show/hide other-field
# this is the code, to change required to false if needed # this is the code, to change required to false if needed
@ -107,7 +175,7 @@ class TravelForm(FdbForm):
} }
class LibraryForm(FdbForm): class LibraryForm(BaseApplicationForm, CommonOrderMixin):
class Meta: class Meta:
model = Library model = Library
@ -144,7 +212,7 @@ class HonoraryCertificateForm(FdbForm):
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
class IFGForm(FdbForm): class IFGForm(BaseApplicationForm, CommonOrderMixin):
class Meta: class Meta:
model = IFG model = IFG
fields = ['cost', 'url', 'notes'] fields = ['cost', 'url', 'notes']
@ -174,7 +242,7 @@ class CheckForm(FdbForm):
# NUTZUNGSBEDINGUNGEN)) # NUTZUNGSBEDINGUNGEN))
class LiteratureForm(CheckForm): class LiteratureForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM termstoaccept = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -192,7 +260,7 @@ ADULT_CHOICES = {'TRUE': mark_safe('Ich bin volljährig.'),
} }
class EmailForm(CheckForm): class EmailForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
@ -216,7 +284,7 @@ class EmailForm(CheckForm):
class BusinessCardForm(CheckForm): class BusinessCardForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN termstoaccept = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN
# this is the code, to change required to false if needed # this is the code, to change required to false if needed
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -232,7 +300,7 @@ class BusinessCardForm(CheckForm):
js = ('dropdown/js/businessCard.js',) js = ('dropdown/js/businessCard.js',)
class ListForm(CheckForm): class ListForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta: class Meta:
model = List model = List