cleaned up forms & fixed missing terms field

This commit is contained in:
Oliver Zander 2025-10-20 15:36:21 +02:00
parent 1dbd38dc4a
commit 864df9613a
3 changed files with 106 additions and 148 deletions

View File

@ -1,23 +1,19 @@
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.admin.widgets import AdminDateWidget 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.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 .models import ( from .models import (
TYPE_CHOICES,
Project, Project,
ProjectCategory, ProjectCategory,
WikimediaProject, WikimediaProject,
ConcreteVolunteer,
ConcreteExtern,
IFG, IFG,
Library, Library,
ELiterature, ELiterature,
Software, Software,
HonoraryCertificate,
Travel, Travel,
Email, Email,
Literature, Literature,
@ -27,108 +23,33 @@ from .models import (
class TableFormRenderer(DjangoTemplates): class TableFormRenderer(DjangoTemplates):
"""
Set in settings as the default form renderer.
"""
form_template_name = 'django/forms/table.html' form_template_name = 'django/forms/table.html'
class FdbForm(ModelForm): class RadioField(forms.ChoiceField):
'''this base class provides the required css class for all forms''' widget = forms.RadioSelect
required_css_class = 'required'
class CommonOrderMixin(forms.Form): class BaseApplicationForm(ModelForm):
"""
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 <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 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):
""" """
Base form for all external applications. 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, 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( 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", """Ich stimme den <a href="{}" target="_blank" rel="noopener">Datenschutzbestimmungen</a> und der<br>
settings.DATAPROTECTION, settings.FOERDERRICHTLINIEN)) <a href="{}" target="_blank" rel="noopener">Richtlinie zur Förderung der Communitys</a> zu.""",
settings.DATAPROTECTION,
settings.FOERDERRICHTLINIEN
),
)
PROJECT_COST_GT_1000_MESSAGE = format_html( PROJECT_COST_GT_1000_MESSAGE = format_html(
@ -166,7 +87,7 @@ class BaseProjectForm(ModelForm):
return cleaned_data return cleaned_data
class ProjectForm(CommonOrderMixin, BaseProjectForm, BaseApplicationForm): class ProjectForm(BaseProjectForm, BaseApplicationForm):
OPTIONAL_FIELDS = { OPTIONAL_FIELDS = {
'categories_other', 'categories_other',
'wikimedia_projects_other', 'wikimedia_projects_other',
@ -186,6 +107,8 @@ class ProjectForm(CommonOrderMixin, BaseProjectForm, BaseApplicationForm):
class Meta: class Meta:
model = Project model = Project
fields = [ fields = [
'realname',
'email',
'name', 'name',
'description', 'description',
'categories', 'categories',
@ -227,10 +150,10 @@ HOTEL_CHOICES = {
} }
class TravelForm(BaseApplicationForm, CommonOrderMixin): class TravelForm(BaseApplicationForm):
# TODO: add some javascript to show/hide other-field # 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 # this is the code, to change required to false if needed
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -244,9 +167,17 @@ class TravelForm(BaseApplicationForm, CommonOrderMixin):
class Meta: class Meta:
model = Travel model = Travel
fields = ['project_name', 'transport', 'travelcost', 'checkin', 'checkout', 'hotel', 'notes'] fields = [
exclude = ('granted', 'granted_date', 'survey_mail_send', 'realname', 'email', 'survey_mail_date', 'project', 'realname',
'request_url', 'payed_for_hotel_by', 'payed_for_travel_by', 'intern_notes', 'mail_state') 'email',
'project_name',
'transport',
'travelcost',
'checkin',
'checkout',
'hotel',
'notes',
]
widgets = { widgets = {
'checkin': AdminDateWidget, 'checkin': AdminDateWidget,
'checkout': AdminDateWidget, 'checkout': AdminDateWidget,
@ -259,12 +190,18 @@ class TravelForm(BaseApplicationForm, CommonOrderMixin):
} }
class LibraryForm(BaseApplicationForm, CommonOrderMixin): class LibraryForm(BaseApplicationForm):
class Meta: class Meta:
model = Library model = Library
fields = ['cost', 'library', 'duration', 'notes', 'survey_mail_send'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'cost',
'library',
'duration',
'notes',
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -286,43 +223,32 @@ class SoftwareForm(LibraryForm):
model = Software 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: class Meta:
model = IFG model = IFG
fields = ['cost', 'url', 'notes'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'cost',
'url',
'notes',
]
class CheckForm(FdbForm): class TermsForm(BaseApplicationForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN terms_accepted_label = 'Ich stimme den <a href="{}">Nutzungsbedingungen</a> zu.'
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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 self.fields['terms_accepted'].required = True
# Set custom label with link to terms self.fields['terms_accepted'].label = format_html(self.terms_accepted_label, self.terms_accepted_url)
self.fields['terms_accepted'].label = format_html(
"Ich stimme den <a href='{}'>Nutzungsbedingungen</a> zu",
self.termstoaccept
)
class LiteratureForm(BaseApplicationForm, CommonOrderMixin): class LiteratureForm(TermsForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -330,8 +256,18 @@ class LiteratureForm(BaseApplicationForm, CommonOrderMixin):
class Meta: class Meta:
model = Literature model = Literature
fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data', 'terms_accepted'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'cost',
'info',
'source',
'notes',
'selfbuy',
'selfbuy_data',
'selfbuy_give_data',
'terms_accepted',
]
class Media: class Media:
js = ('dropdown/js/literature.js',) js = ('dropdown/js/literature.js',)
@ -343,8 +279,8 @@ ADULT_CHOICES = {
} }
class EmailForm(BaseApplicationForm, CommonOrderMixin): class EmailForm(TermsForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
# 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):
@ -352,20 +288,27 @@ class EmailForm(BaseApplicationForm, CommonOrderMixin):
self.fields['adult'].required = True self.fields['adult'].required = True
self.fields['other'].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 # TODO: add some javascript to show/hide other-field
class Meta: class Meta:
model = Email model = Email
fields = ['domain', 'address', 'other', 'adult', 'terms_accepted'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'domain',
'address',
'other',
'adult',
'terms_accepted',
]
class Media: class Media:
js = ('dropdown/js/mail.js',) js = ('dropdown/js/mail.js',)
class BusinessCardForm(BaseApplicationForm, CommonOrderMixin): class BusinessCardForm(TermsForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN terms_accepted_url = 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):
@ -375,20 +318,34 @@ class BusinessCardForm(BaseApplicationForm, CommonOrderMixin):
class Meta: class Meta:
model = BusinessCard model = BusinessCard
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] fields = [
fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to', 'terms_accepted'] 'realname',
'email',
'project',
'data',
'variant',
'url_of_pic',
'send_data_to_print',
'sent_to',
'terms_accepted',
]
class Media: class Media:
js = ('dropdown/js/businessCard.js',) js = ('dropdown/js/businessCard.js',)
class ListForm(BaseApplicationForm, CommonOrderMixin): class ListForm(TermsForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta: class Meta:
model = List model = List
fields = ['domain', 'address', 'terms_accepted'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'domain',
'address',
'terms_accepted',
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -25,7 +25,7 @@ EMAIL_STATES = {
class TermsConsentMixin(models.Model): class TermsConsentMixin(models.Model):
"""Abstract mixin to add a terms_accepted field for documenting user consent.""" """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: class Meta:
abstract = True abstract = True

View File

@ -90,6 +90,7 @@ class AnonymousViewTestCase(TestCase):
'selfbuy_data': 'NONE', 'selfbuy_data': 'NONE',
'selfbuy_give_data': True, 'selfbuy_give_data': True,
'check': True, 'check': True,
'terms_accepted': True,
}) })
def test_extern_lit_without_consent_fails(self): def test_extern_lit_without_consent_fails(self):