From 864df9613ad826db2690f1fba0915d1126dfa44d Mon Sep 17 00:00:00 2001 From: Oliver Zander Date: Mon, 20 Oct 2025 15:36:21 +0200 Subject: [PATCH] cleaned up forms & fixed missing terms field --- input/forms.py | 251 ++++++++++++++++++------------------------- input/models.py | 2 +- input/tests/views.py | 1 + 3 files changed, 106 insertions(+), 148 deletions(-) diff --git a/input/forms.py b/input/forms.py index 3688344..62b0b8b 100755 --- a/input/forms.py +++ b/input/forms.py @@ -1,23 +1,19 @@ from django import forms from django.conf import settings from django.contrib.admin.widgets import AdminDateWidget -from django.forms import ModelForm, ChoiceField, RadioSelect, BooleanField, CharField, EmailField +from django.forms import ModelForm from django.forms.renderers import DjangoTemplates from django.utils.html import format_html from django.utils.safestring import mark_safe from .models import ( - TYPE_CHOICES, Project, ProjectCategory, WikimediaProject, - ConcreteVolunteer, - ConcreteExtern, IFG, Library, ELiterature, Software, - HonoraryCertificate, Travel, Email, Literature, @@ -27,108 +23,33 @@ from .models import ( class TableFormRenderer(DjangoTemplates): + """ + Set in settings as the default form renderer. + """ + form_template_name = 'django/forms/table.html' -class FdbForm(ModelForm): - '''this base class provides the required css class for all forms''' - required_css_class = 'required' +class RadioField(forms.ChoiceField): + widget = forms.RadioSelect -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. Put the 'head' fields first. - 2. Put all other fields in the middle. - 3. Put the 'tail' fields last. - - Non-existing fields are ignored by `order_fields`. - """ - - super().__init__(*args, **kwargs) - - ordered = {*self.field_order_head, *self.field_order_tail} - - self.order_fields([ - *self.field_order_head, - *[field for field in self.fields if field not in ordered], - *self.field_order_tail, - ]) - - -class ExternForm(FdbForm): - choice = ChoiceField(choices=TYPE_CHOICES.items(), widget=RadioSelect, - label='Was möchtest Du beantragen?') - - check = BooleanField(required=True, - label=format_html("Ich stimme den Datenschutzbestimmungen und der
Richtlinie zur Förderung der Communitys zu", - settings.DATAPROTECTION, settings.FOERDERRICHTLINIEN)) - - class Meta: - model = ConcreteExtern - exclude = ('username', 'granted', 'granted_date', 'survey_mail_send', 'service_id', 'survey_mail_date', 'mail_state') - - -INTERN_CHOICES = { - 'PRO': 'Projektsteckbrief', - 'HON': 'Ehrenamtsbescheinigung, Akkreditierung oder Redaktionsbestätigung', - 'TRAV': 'Reisekostenerstattung', -} - - -class InternForm(FdbForm): - choice = ChoiceField(choices=INTERN_CHOICES.items(), widget=RadioSelect, - label='Was möchtest Du eingeben?') - - class Meta: - model = ConcreteVolunteer - exclude = ('granted', 'granted_date', 'survey_mail_send', 'survey_mail_date', 'mail_state') - - -class BaseApplicationForm(FdbForm): +class BaseApplicationForm(ModelForm): """ 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_css_class = 'required' + + check = forms.BooleanField( required=True, - help_text='Bitte gib deinen Vor- und Nachnamen ein.' + label=format_html( + """Ich stimme den Datenschutzbestimmungen und der
+ Richtlinie zur Förderung der Communitys zu.""", + settings.DATAPROTECTION, + settings.FOERDERRICHTLINIEN + ), ) - email = EmailField( - label='E-Mail-Adresse', - required=True, - help_text='Bitte gib deine E-Mail-Adresse ein, damit dich
Wikimedia Deutschland bei Rückfragen oder für
die Zusage kontaktieren kann.' - ) - check = BooleanField(required=True, - label=format_html( - "Ich stimme den Datenschutzbestimmungen und der
Richtlinie zur Förderung der Communitys zu", - settings.DATAPROTECTION, settings.FOERDERRICHTLINIEN)) PROJECT_COST_GT_1000_MESSAGE = format_html( @@ -166,7 +87,7 @@ class BaseProjectForm(ModelForm): return cleaned_data -class ProjectForm(CommonOrderMixin, BaseProjectForm, BaseApplicationForm): +class ProjectForm(BaseProjectForm, BaseApplicationForm): OPTIONAL_FIELDS = { 'categories_other', 'wikimedia_projects_other', @@ -186,6 +107,8 @@ class ProjectForm(CommonOrderMixin, BaseProjectForm, BaseApplicationForm): class Meta: model = Project fields = [ + 'realname', + 'email', 'name', 'description', 'categories', @@ -227,10 +150,10 @@ HOTEL_CHOICES = { } -class TravelForm(BaseApplicationForm, CommonOrderMixin): +class TravelForm(BaseApplicationForm): # TODO: add some javascript to show/hide other-field - hotel = ChoiceField(label='Hotelzimmer benötigt:', choices=HOTEL_CHOICES.items(), widget=RadioSelect()) + hotel = RadioField(label='Hotelzimmer benötigt', choices=HOTEL_CHOICES) # this is the code, to change required to false if needed def __init__(self, *args, **kwargs): @@ -244,9 +167,17 @@ class TravelForm(BaseApplicationForm, CommonOrderMixin): class Meta: model = Travel - fields = ['project_name', 'transport', 'travelcost', 'checkin', 'checkout', 'hotel', 'notes'] - exclude = ('granted', 'granted_date', 'survey_mail_send', 'realname', 'email', 'survey_mail_date', 'project', - 'request_url', 'payed_for_hotel_by', 'payed_for_travel_by', 'intern_notes', 'mail_state') + fields = [ + 'realname', + 'email', + 'project_name', + 'transport', + 'travelcost', + 'checkin', + 'checkout', + 'hotel', + 'notes', + ] widgets = { 'checkin': AdminDateWidget, 'checkout': AdminDateWidget, @@ -259,12 +190,18 @@ class TravelForm(BaseApplicationForm, CommonOrderMixin): } -class LibraryForm(BaseApplicationForm, CommonOrderMixin): +class LibraryForm(BaseApplicationForm): class Meta: model = Library - fields = ['cost', 'library', 'duration', 'notes', 'survey_mail_send'] - exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] + fields = [ + 'realname', + 'email', + 'cost', + 'library', + 'duration', + 'notes', + ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -286,43 +223,32 @@ class SoftwareForm(LibraryForm): model = Software -class HonoraryCertificateForm(FdbForm): +class IFGForm(BaseApplicationForm): - class Meta: - model = HonoraryCertificate - fields = ['request_url', 'project'] - exclude = ['intern_notes'] - - class Media: - js = ('dropdown/js/otrs_link.js',) - - -class IFGForm(BaseApplicationForm, CommonOrderMixin): class Meta: model = IFG - fields = ['cost', 'url', 'notes'] - exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] + fields = [ + 'realname', + 'email', + 'cost', + 'url', + 'notes', + ] -class CheckForm(FdbForm): - termstoaccept = settings.NUTZUNGSBEDINGUNGEN +class TermsForm(BaseApplicationForm): + terms_accepted_label = 'Ich stimme den Nutzungsbedingungen zu.' + terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Check if the model field 'terms_accepted' is present - if 'terms_accepted' in self.fields: - # Make the field required (HTML5 validation) - self.fields['terms_accepted'].required = True - # Set custom label with link to terms - self.fields['terms_accepted'].label = format_html( - "Ich stimme den Nutzungsbedingungen zu", - self.termstoaccept - ) + self.fields['terms_accepted'].required = True + self.fields['terms_accepted'].label = format_html(self.terms_accepted_label, self.terms_accepted_url) -class LiteratureForm(BaseApplicationForm, CommonOrderMixin): - termstoaccept = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM +class LiteratureForm(TermsForm): + terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -330,8 +256,18 @@ class LiteratureForm(BaseApplicationForm, CommonOrderMixin): class Meta: model = Literature - fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data', 'terms_accepted'] - exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] + fields = [ + 'realname', + 'email', + 'cost', + 'info', + 'source', + 'notes', + 'selfbuy', + 'selfbuy_data', + 'selfbuy_give_data', + 'terms_accepted', + ] class Media: js = ('dropdown/js/literature.js',) @@ -343,8 +279,8 @@ ADULT_CHOICES = { } -class EmailForm(BaseApplicationForm, CommonOrderMixin): - termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE +class EmailForm(TermsForm): + terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE # this is the code, to change required to false if needed def __init__(self, *args, **kwargs): @@ -352,20 +288,27 @@ class EmailForm(BaseApplicationForm, CommonOrderMixin): self.fields['adult'].required = True self.fields['other'].required = True - adult = ChoiceField(label='Volljährigkeit', choices=ADULT_CHOICES.items(), widget=RadioSelect()) + adult = RadioField(label='Volljährigkeit', choices=ADULT_CHOICES) # TODO: add some javascript to show/hide other-field class Meta: model = Email - fields = ['domain', 'address', 'other', 'adult', 'terms_accepted'] - exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] + fields = [ + 'realname', + 'email', + 'domain', + 'address', + 'other', + 'adult', + 'terms_accepted', + ] class Media: js = ('dropdown/js/mail.js',) -class BusinessCardForm(BaseApplicationForm, CommonOrderMixin): - termstoaccept = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN +class BusinessCardForm(TermsForm): + terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN # this is the code, to change required to false if needed def __init__(self, *args, **kwargs): @@ -375,20 +318,34 @@ class BusinessCardForm(BaseApplicationForm, CommonOrderMixin): class Meta: model = BusinessCard - exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] - fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to', 'terms_accepted'] + fields = [ + 'realname', + 'email', + 'project', + 'data', + 'variant', + 'url_of_pic', + 'send_data_to_print', + 'sent_to', + 'terms_accepted', + ] class Media: js = ('dropdown/js/businessCard.js',) -class ListForm(BaseApplicationForm, CommonOrderMixin): - termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN +class ListForm(TermsForm): + terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN class Meta: model = List - fields = ['domain', 'address', 'terms_accepted'] - exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] + fields = [ + 'realname', + 'email', + 'domain', + 'address', + 'terms_accepted', + ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/input/models.py b/input/models.py index 5193d7f..8a92f3c 100755 --- a/input/models.py +++ b/input/models.py @@ -25,7 +25,7 @@ EMAIL_STATES = { class TermsConsentMixin(models.Model): """Abstract mixin to add a terms_accepted field for documenting user consent.""" - terms_accepted = models.BooleanField(default=False, verbose_name="Nutzungsbedingungen zugestimmt") + terms_accepted = models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt') class Meta: abstract = True diff --git a/input/tests/views.py b/input/tests/views.py index c7fab8f..4cf1bab 100644 --- a/input/tests/views.py +++ b/input/tests/views.py @@ -90,6 +90,7 @@ class AnonymousViewTestCase(TestCase): 'selfbuy_data': 'NONE', 'selfbuy_give_data': True, 'check': True, + 'terms_accepted': True, }) def test_extern_lit_without_consent_fails(self):