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 ProjectRequest, PROJECT_CATEGORIES, WIKIMEDIA_CHOICES
from .models import (
TYPE_CHOICES,
Project,
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 ProjectForm(FdbForm):
# start = DateField(widget=AdminDateWidget())
class Meta:
model = Project
exclude = ('pid', 'project_of_year', 'finance_id', 'granted', 'granted_date', 'realname', 'email', \
'end_mail_send', 'status', 'persons', 'survey_mail_date', 'mail_state')
widgets = {'start': AdminDateWidget(),
'end': AdminDateWidget(), }
class Media:
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. 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))
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 = ''
class ProjectRequestForm(BaseApplicationForm, CommonOrderMixin):
"""
Public-facing form for < 1000 EUR project requests.
Key points:
- JSONField-backed multi-selects are exposed as MultipleChoiceField with checkbox widgets.
- Extra UX tweaks: textareas for long text, number inputs with min/max/step, help_texts with links via format_html.
"""
# Expose JSON-backed categories as a checkbox multi-select
categories = forms.MultipleChoiceField(
choices=[(c, c) for c in PROJECT_CATEGORIES],
widget=forms.CheckboxSelectMultiple,
label='Projektkategorie',
help_text='In welche dieser Kategorien lässt sich dein Projekt einordnen?'
)
# Expose JSON-backed wikimedia_projects as a checkbox multi-select
wikimedia_projects = forms.MultipleChoiceField(
choices=[(w, w) for w in WIKIMEDIA_CHOICES],
widget=forms.CheckboxSelectMultiple,
label='Wikimedia Projekt(e)',
help_text='Auf welches Wikimedia-Projekt bezieht sich dein Vorhaben?',
)
class Meta:
model = ProjectRequest
fields = [
'realname', 'email',
'name', 'description',
'categories', 'categories_other',
'wikimedia_projects', 'wikimedia_other',
'start', 'end', 'participants_estimated',
'page', 'group', 'location',
'cost', 'insurance', 'notes',
]
# Widgets are chosen for better UX and to gently guide valid inputs in the browser
widgets = {
'start': AdminDateWidget(),
'end': AdminDateWidget(),
# Long-text fields as textareas with sensible row counts
'description': forms.Textarea(attrs={'rows': 5}),
'notes': forms.Textarea(attrs={'rows': 6}),
# Integer-like fields: browser-side constraints (server still validates in the model)
'participants_estimated': forms.NumberInput(attrs={'min': 0, 'step': 1}),
'cost': forms.NumberInput(attrs={'min': 0, 'max': 1000, 'step': 1}),
}
# Human-readable help_texts; use format_html for safe HTML (links)
help_texts = {
'name': 'Bitte gib einen Namen für das Projekt an.',
'description': 'Bitte beschreibe kurz, was die Ziele deines Projekts sind.',
'participants_estimated': 'Wie viele Personen werden ungefähr an diesem Projekt teilnehmen?',
'page': 'Bitte gib einen Link zur Projektseite in den Wikimedia-Projekten an, wenn vorhanden.',
'group': 'Sofern zutreffend: Bitte gib an, welche Personen das Projekt gemeinsam mit dir organisieren.',
'location': 'Sofern zutreffend: Bitte gib hier den Ort an, an welchem das Projekt stattfinden wird.',
'cost': 'Wie hoch werden die Projektkosten voraussichtlich sein? Bitte gib diese auf volle Euro gerundet an.',
'insurance': format_html(
'Möchtest du die Unfall- und Haftpflichtversicherung von Wikimedia Deutschland in Anspruch nehmen?'),
'notes': format_html(
'Falls du noch weitere Informationen hast, teile sie gern an dieser Stelle mit uns. Für umfangreichere Informationen, schreibe uns eine E-Mail an community@wikimedia.de.'),
}
class ProjectRequestAdminForm(forms.ModelForm):
"""
Admin form for ProjectRequest.
Key points:
- Same checkbox multi-selects for JSON-backed fields to improve admin UX.
- Keep fields="__all__" so admin users can inspect/set workflow fields if needed.
- Do NOT add extra business logic here; validation lives in the model's clean().
"""
categories = forms.MultipleChoiceField(
choices=[(c, c) for c in PROJECT_CATEGORIES],
widget=forms.CheckboxSelectMultiple,
label='Projektkategorie(n)'
)
wikimedia_projects = forms.MultipleChoiceField(
choices=[(w, w) for w in WIKIMEDIA_CHOICES],
widget=forms.CheckboxSelectMultiple,
label='Wikimedia Projekt(e)'
)
class Meta:
# Make longer texts easier to edit in the admin UI
widgets = {
'description': forms.Textarea(attrs={'rows': 5}),
'notes': forms.Textarea(attrs={'rows': 6}),
}
# Ensure JSONField receives a list
def clean_categories(self):
return list(self.cleaned_data.get('categories', []))
def clean_wikimedia_projects(self):
return list(self.cleaned_data.get('wikimedia_projects', []))