from django.conf import settings from django.forms import ModelForm, ChoiceField, RadioSelect, BooleanField, CharField, EmailField from django.contrib.admin.widgets import AdminDateWidget from django.forms.renderers import DjangoTemplates from django.utils.html import format_html from django.utils.safestring import mark_safe from django import forms 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. Collects all existing field names. 2. Puts the 'head' fields first (if they exist in this form). 3. Keeps all other fields in the middle. 4. Puts the 'tail' fields last (if they exist in this form). """ super().__init__(*args, **kwargs) existing = list(self.fields.keys()) # Select only fields that actually exist in this form head = [f for f in self.field_order_head if f in self.fields] tail = [f for f in self.field_order_tail if f in self.fields] # All other fields that are not explicitly in head or tail middle = [f for f in existing if f not in (*head, *tail)] # Apply the new order to the form fields self.order_fields(head + middle + 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') HOTEL_CHOICES = {'TRUE': mark_safe('Hotelzimmer benötigt'), 'FALSE': mark_safe('Kein Hotelzimmer benötigt') } 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)) class TravelForm(BaseApplicationForm, CommonOrderMixin): # TODO: add some javascript to show/hide other-field # 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 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(),} fields = ['project_name', 'transport', 'travelcost', 'checkin', 'checkout', 'hotel', 'notes'] hotel = ChoiceField(label='Hotelzimmer benötigt:', choices=HOTEL_CHOICES.items(), widget=RadioSelect()) 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) self.fields['check'] = BooleanField( required=True, label=format_html( "Ich stimme den Nutzungsbedingungen zu", self.termstoaccept ) ) """Baseclass for all classes which need a check for Nutzungsbedingungen""" # def __init__(self, *args, **kwargs): # check = BooleanField(required=True, # label=format_html("Ich stimme den Nutzungsbedingungen zu", # termstoaccept)) # NUTZUNGSBEDINGUNGEN)) 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'] 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'] 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'] class Media: js = ('dropdown/js/businessCard.js',) class ListForm(BaseApplicationForm, CommonOrderMixin): termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN class Meta: model = List fields = ['domain', 'address'] exclude = ['intern_notes', 'survey_mail_send','mail_state'] class ProjectRequestForm(CommonOrderMixin, forms.ModelForm): """ Public-facing form for < 1000 EUR project requests. Key points: - JSONField-backed multi-selects are exposed as MultipleChoiceField with checkbox widgets. - We return `list(...)` in clean_* so the JSONField gets a native list. - 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.'), } # Persist multi-selects as Python lists so JSONField stores a JSON array def clean_categories(self): return list(self.cleaned_data.get('categories', [])) def clean_wikimedia_projects(self): return list(self.cleaned_data.get('wikimedia_projects', [])) 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' ) wikimedia_projects = forms.MultipleChoiceField( choices=[(w, w) for w in WIKIMEDIA_CHOICES], widget=forms.CheckboxSelectMultiple, label='Wikimedia Projekt(e)' ) class Meta: model = ProjectRequest fields = "__all__" # 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', []))