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.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,
List,
BusinessCard,
)
class TableFormRenderer(DjangoTemplates):
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 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):
"""
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
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(
"""Bitte beachte, dass für Projektkosten über 1.000 € ein öffentlicher Projektplan erforderlich
ist (siehe Wikipedia:Förderung/Projektplanung).""",
'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/Projektplanung'
)
class BaseProjectForm(ModelForm):
categories = {
'categories': ProjectCategory,
'wikimedia_projects': WikimediaProject,
}
class Media:
js = ('dropdown/js/otrs_link.js', 'js/project-categories.js')
def clean(self):
cleaned_data = ModelForm.clean(self)
if self.errors:
return cleaned_data
for field, model in self.categories.items():
field_other = f'{field}_other'
values = cleaned_data[field]
if model.other in values:
if not cleaned_data[field_other]:
self.add_error(field_other, f'Bitte gib einen Wert an oder deselektiere "{model.OTHER}".')
else:
cleaned_data[field_other] = ''
return cleaned_data
class ProjectForm(CommonOrderMixin, BaseProjectForm, BaseApplicationForm):
OPTIONAL_FIELDS = {
'categories_other',
'wikimedia_projects_other',
'page',
'group',
'location',
'insurance',
'notes',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in set(self.fields) - self.OPTIONAL_FIELDS:
self.fields[field].required = True
class Meta:
model = Project
fields = [
'name',
'description',
'categories',
'categories_other',
'wikimedia_projects',
'wikimedia_projects_other',
'start',
'end',
'participants_estimated',
'page',
'group',
'location',
'cost',
'insurance',
'notes',
]
widgets = {
'start': AdminDateWidget,
'end': AdminDateWidget,
}
class Media:
css = {
'all': ('css/dateFieldNoNowShortcutInTravels.css',)
}
def clean_cost(self):
cost = self.cleaned_data['cost']
if cost > 1000:
raise forms.ValidationError(PROJECT_COST_GT_1000_MESSAGE, code='cost-gt-1000')
return cost
HOTEL_CHOICES = {
'TRUE': mark_safe('Hotelzimmer benötigt'),
'FALSE': mark_safe('Kein Hotelzimmer benötigt'),
}
class TravelForm(BaseApplicationForm, CommonOrderMixin):
# TODO: add some javascript to show/hide other-field
hotel = ChoiceField(label='Hotelzimmer benötigt:', choices=HOTEL_CHOICES.items(), widget=RadioSelect())
# this is the code, to change required to false if needed
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['project_name'].required = True
self.fields['transport'].required = True
self.fields['travelcost'].required = True
self.fields['checkin'].required = True
self.fields['checkout'].required = True
self.fields['hotel'].required = True
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')
widgets = {
'checkin': AdminDateWidget,
'checkout': AdminDateWidget,
}
class Media:
js = ('dropdown/js/otrs_link.js',)
css = {
'all': ('css/dateFieldNoNowShortcutInTravels.css',)
}
class LibraryForm(BaseApplicationForm, CommonOrderMixin):
class Meta:
model = Library
fields = ['cost', 'library', 'duration', 'notes', 'survey_mail_send']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['library'].label = self._meta.model.LIBRARY_LABEL
self.fields['library'].help_text = self._meta.model.LIBRARY_HELP_TEXT
self.fields['duration'].help_text = self._meta.model.DURATION_HELP_TEXT
class ELiteratureForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = ELiterature
class SoftwareForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = Software
class HonoraryCertificateForm(FdbForm):
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']
class CheckForm(FdbForm):
termstoaccept = 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
)
class LiteratureForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['selfbuy_give_data'].required = True
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']
class Media:
js = ('dropdown/js/literature.js',)
ADULT_CHOICES = {
'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': mark_safe('Ich bin noch nicht volljährig.'),
}
class EmailForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
# this is the code, to change required to false if needed
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['adult'].required = True
self.fields['other'].required = True
adult = ChoiceField(label='Volljährigkeit', choices=ADULT_CHOICES.items(), widget=RadioSelect())
# 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']
class Media:
js = ('dropdown/js/mail.js',)
class BusinessCardForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN
# this is the code, to change required to false if needed
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['url_of_pic'].required = True
self.fields['send_data_to_print'].required = True
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']
class Media:
js = ('dropdown/js/businessCard.js',)
class ListForm(BaseApplicationForm, CommonOrderMixin):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta:
model = List
fields = ['domain', 'address', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['address'].initial = ''