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.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 <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):
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 <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
),
)
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))
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 <a href="{}">Nutzungsbedingungen</a> 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 <a href='{}'>Nutzungsbedingungen</a> 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)

View File

@ -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

View File

@ -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):