2025-08-19 07:45:14 +00:00
from django . conf import settings
2025-08-31 21:38:22 +00:00
from django . forms import ModelForm , ChoiceField , RadioSelect , BooleanField , CharField , EmailField
2020-09-29 10:16:10 +00:00
from django . contrib . admin . widgets import AdminDateWidget
2025-08-20 12:28:46 +00:00
from django . forms . renderers import DjangoTemplates
2020-10-20 12:12:12 +00:00
from django . utils . html import format_html
2025-08-19 14:08:48 +00:00
from django . utils . safestring import mark_safe
2025-08-31 21:38:22 +00:00
from django import forms
2025-09-28 22:22:47 +00:00
from . models import ProjectRequest , PROJECT_CATEGORIES , WIKIMEDIA_CHOICES
2020-09-29 10:16:10 +00:00
2025-08-20 10:06:43 +00:00
from . models import (
TYPE_CHOICES ,
Project ,
ConcreteVolunteer ,
ConcreteExtern ,
IFG ,
Library ,
ELiterature ,
Software ,
HonoraryCertificate ,
Travel ,
Email ,
Literature ,
List ,
BusinessCard ,
)
2020-09-29 07:54:31 +00:00
2020-11-17 15:11:10 +00:00
2025-08-20 12:28:46 +00:00
class TableFormRenderer ( DjangoTemplates ) :
form_template_name = ' django/forms/table.html '
2020-11-17 15:11:10 +00:00
class FdbForm ( ModelForm ) :
''' this base class provides the required css class for all forms '''
required_css_class = ' required '
class ProjectForm ( FdbForm ) :
2020-10-21 11:53:39 +00:00
# start = DateField(widget=AdminDateWidget())
2020-09-29 10:16:10 +00:00
2020-09-29 07:54:31 +00:00
class Meta :
model = Project
2025-08-31 21:38:22 +00:00
exclude = ( ' pid ' , ' project_of_year ' , ' finance_id ' , ' granted ' , ' granted_date ' , ' realname ' , ' email ' , \
2023-02-27 17:09:29 +00:00
' end_mail_send ' , ' status ' , ' persons ' , ' survey_mail_date ' , ' mail_state ' )
2020-10-21 11:53:39 +00:00
widgets = { ' start ' : AdminDateWidget ( ) ,
2025-08-31 21:38:22 +00:00
' end ' : AdminDateWidget ( ) , }
2020-10-01 08:51:19 +00:00
2023-12-30 17:46:15 +00:00
class Media :
js = ( ' dropdown/js/otrs_link.js ' , )
2023-02-27 17:09:29 +00:00
2025-08-31 21:38:22 +00:00
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 )
2020-11-17 15:11:10 +00:00
class ExternForm ( FdbForm ) :
2020-10-01 08:51:19 +00:00
2020-10-21 11:27:05 +00:00
choice = ChoiceField ( choices = TYPE_CHOICES . items ( ) , widget = RadioSelect ,
2020-10-19 12:46:58 +00:00
label = ' Was möchtest Du beantragen? ' )
2020-10-01 08:51:19 +00:00
2020-10-20 12:12:12 +00:00
check = BooleanField ( required = True ,
2025-06-16 09:43:08 +00:00
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 " ,
2025-08-19 07:45:14 +00:00
settings . DATAPROTECTION , settings . FOERDERRICHTLINIEN ) )
2020-10-20 11:16:03 +00:00
2020-10-01 08:51:19 +00:00
class Meta :
2021-07-06 11:00:34 +00:00
model = ConcreteExtern
2023-10-25 15:50:40 +00:00
exclude = ( ' username ' , ' granted ' , ' granted_date ' , ' survey_mail_send ' , ' service_id ' , ' survey_mail_date ' , ' mail_state ' )
2020-10-01 08:51:19 +00:00
2023-02-27 17:09:29 +00:00
2020-10-28 14:04:53 +00:00
INTERN_CHOICES = { ' PRO ' : ' Projektsteckbrief ' ,
' HON ' : ' Ehrenamtsbescheinigung, Akkreditierung oder Redaktionsbestätigung ' ,
' TRAV ' : ' Reisekostenerstattung ' }
2020-10-21 07:54:12 +00:00
2020-11-17 15:11:10 +00:00
class InternForm ( FdbForm ) :
2020-10-28 14:04:53 +00:00
choice = ChoiceField ( choices = INTERN_CHOICES . items ( ) , widget = RadioSelect ,
2020-10-21 07:54:12 +00:00
label = ' Was möchtest Du eingeben? ' )
class Meta :
2021-07-07 07:42:51 +00:00
model = ConcreteVolunteer
2023-02-27 17:09:29 +00:00
exclude = ( ' granted ' , ' granted_date ' , ' survey_mail_send ' , ' survey_mail_date ' , ' mail_state ' )
2020-10-21 07:54:12 +00:00
2023-02-27 17:09:29 +00:00
2025-08-19 14:08:48 +00:00
HOTEL_CHOICES = { ' TRUE ' : mark_safe ( ' Hotelzimmer benötigt ' ) ,
2025-08-31 21:38:22 +00:00
' 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 (
2025-09-01 11:24:59 +00:00
label = ' Realname ' ,
2025-08-31 21:38:22 +00:00
required = True ,
2025-09-01 11:24:59 +00:00
help_text = ' Bitte gib deinen Vor- und Nachnamen ein. '
2025-08-31 21:38:22 +00:00
)
email = EmailField (
2025-09-01 11:24:59 +00:00
label = ' E-Mail-Adresse ' ,
2025-08-31 21:38:22 +00:00
required = True ,
2025-09-01 11:24:59 +00:00
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. '
2025-08-31 21:38:22 +00:00
)
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 ) )
2023-02-27 17:09:29 +00:00
2025-08-31 21:38:22 +00:00
class TravelForm ( BaseApplicationForm , CommonOrderMixin ) :
2020-11-03 11:56:40 +00:00
# TODO: add some javascript to show/hide other-field
2025-08-19 12:10:15 +00:00
2023-02-27 17:09:29 +00:00
# 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
2023-02-27 17:09:29 +00:00
self . fields [ ' hotel ' ] . required = True
2023-02-27 17:09:29 +00:00
2020-10-26 10:38:56 +00:00
class Meta :
model = Travel
2023-02-27 17:09:29 +00:00
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 ' )
2020-11-02 13:04:01 +00:00
widgets = { ' checkin ' : AdminDateWidget ( ) ,
' checkout ' : AdminDateWidget ( ) , }
2023-02-27 17:09:29 +00:00
fields = [ ' project_name ' , ' transport ' , ' travelcost ' , ' checkin ' , ' checkout ' , ' hotel ' , ' notes ' ]
2023-02-27 17:09:29 +00:00
hotel = ChoiceField ( label = ' Hotelzimmer benötigt: ' , choices = HOTEL_CHOICES . items ( ) , widget = RadioSelect ( ) )
2020-10-21 07:54:12 +00:00
2023-02-27 17:09:29 +00:00
class Media :
2025-02-26 12:13:46 +00:00
js = ( ' dropdown/js/otrs_link.js ' , )
2023-02-27 17:09:29 +00:00
css = {
2023-02-27 17:09:29 +00:00
' all ' : ( ' css/dateFieldNoNowShortcutInTravels.css ' , )
2023-02-27 17:09:29 +00:00
}
2025-08-20 10:06:43 +00:00
2025-08-31 21:38:22 +00:00
class LibraryForm ( BaseApplicationForm , CommonOrderMixin ) :
2020-10-19 11:29:36 +00:00
2020-10-01 08:51:19 +00:00
class Meta :
model = Library
2023-02-27 17:09:29 +00:00
fields = [ ' cost ' , ' library ' , ' duration ' , ' notes ' , ' survey_mail_send ' ]
2023-02-27 17:09:29 +00:00
exclude = [ ' intern_notes ' , ' survey_mail_send ' , ' mail_state ' ]
2020-10-01 08:51:19 +00:00
2025-08-20 10:06:43 +00:00
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
2020-11-18 17:02:23 +00:00
class HonoraryCertificateForm ( FdbForm ) :
2025-08-19 12:10:15 +00:00
2020-11-18 17:02:23 +00:00
class Meta :
model = HonoraryCertificate
fields = [ ' request_url ' , ' project ' ]
2023-02-27 17:09:29 +00:00
exclude = [ ' intern_notes ' ]
2025-02-27 09:15:25 +00:00
class Media :
js = ( ' dropdown/js/otrs_link.js ' , )
2025-08-19 12:10:15 +00:00
2020-11-18 17:02:23 +00:00
2025-08-31 21:38:22 +00:00
class IFGForm ( BaseApplicationForm , CommonOrderMixin ) :
2020-10-01 08:51:19 +00:00
class Meta :
2020-10-01 10:08:02 +00:00
model = IFG
2020-11-02 14:41:08 +00:00
fields = [ ' cost ' , ' url ' , ' notes ' ]
2023-02-27 17:09:29 +00:00
exclude = [ ' intern_notes ' , ' survey_mail_send ' , ' mail_state ' ]
2020-10-21 07:54:12 +00:00
2020-10-27 10:00:58 +00:00
2020-11-18 17:06:00 +00:00
class CheckForm ( FdbForm ) :
2025-08-19 07:45:14 +00:00
termstoaccept = settings . NUTZUNGSBEDINGUNGEN
2025-06-16 09:43:08 +00:00
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
self . fields [ ' check ' ] = BooleanField (
required = True ,
label = format_html (
" Ich stimme den <a href= ' {} ' >Nutzungsbedingungen</a> zu " ,
self . termstoaccept
)
)
2020-11-18 17:06:00 +00:00
""" Baseclass for all classes which need a check for Nutzungsbedingungen """
2025-06-16 09:43:08 +00:00
# def __init__(self, *args, **kwargs):
# check = BooleanField(required=True,
# label=format_html("Ich stimme den <a href='{}'>Nutzungsbedingungen</a> zu",
# termstoaccept))
# NUTZUNGSBEDINGUNGEN))
2020-11-18 17:02:23 +00:00
2023-02-27 17:09:29 +00:00
2025-08-31 21:38:22 +00:00
class LiteratureForm ( BaseApplicationForm , CommonOrderMixin ) :
2025-06-16 09:43:08 +00:00
termstoaccept = settings . NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
2023-02-27 17:09:28 +00:00
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
2023-02-27 17:09:29 +00:00
self . fields [ ' selfbuy_give_data ' ] . required = True
2020-10-27 12:47:46 +00:00
class Meta :
model = Literature
2023-02-27 17:09:29 +00:00
fields = [ ' cost ' , ' info ' , ' source ' , ' notes ' , ' selfbuy ' , ' selfbuy_data ' , ' selfbuy_give_data ' ]
2023-02-27 17:09:29 +00:00
exclude = [ ' intern_notes ' , ' survey_mail_send ' , ' mail_state ' ]
2023-02-27 17:09:28 +00:00
class Media :
2023-02-27 17:09:29 +00:00
js = ( ' dropdown/js/literature.js ' , )
2020-10-27 12:47:46 +00:00
2025-08-19 14:08:48 +00:00
ADULT_CHOICES = { ' TRUE ' : mark_safe ( ' Ich bin volljährig. ' ) ,
' FALSE ' : mark_safe ( ' Ich bin noch nicht volljährig. ' )
2023-02-27 17:09:29 +00:00
}
2025-08-31 21:38:22 +00:00
class EmailForm ( BaseApplicationForm , CommonOrderMixin ) :
2025-06-16 09:43:08 +00:00
termstoaccept = settings . NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
2025-08-19 12:10:15 +00:00
2023-02-27 17:09:29 +00:00
# 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
2023-02-27 17:09:29 +00:00
self . fields [ ' other ' ] . required = True
2023-02-27 17:09:29 +00:00
2023-02-27 17:09:29 +00:00
adult = ChoiceField ( label = ' Volljährigkeit ' , choices = ADULT_CHOICES . items ( ) , widget = RadioSelect ( ) )
2020-10-27 10:00:58 +00:00
# TODO: add some javascript to show/hide other-field
class Meta :
model = Email
2023-02-27 17:09:29 +00:00
fields = [ ' domain ' , ' address ' , ' other ' , ' adult ' ]
2023-02-27 17:09:29 +00:00
exclude = [ ' intern_notes ' , ' survey_mail_send ' , ' mail_state ' ]
2023-02-27 17:09:28 +00:00
class Media :
2023-02-27 17:09:29 +00:00
js = ( ' dropdown/js/mail.js ' , )
2023-02-27 17:09:28 +00:00
2020-10-27 12:47:46 +00:00
2025-08-31 21:38:22 +00:00
class BusinessCardForm ( BaseApplicationForm , CommonOrderMixin ) :
2025-06-16 09:43:08 +00:00
termstoaccept = settings . NUTZUNGSBEDINGUNGEN_VISITENKARTEN
# this is the code, to change required to false if needed
def __init__ ( self , * args , * * kwargs ) :
2023-02-27 17:09:28 +00:00
super ( ) . __init__ ( * args , * * kwargs )
self . fields [ ' url_of_pic ' ] . required = True
2023-02-27 17:09:29 +00:00
self . fields [ ' send_data_to_print ' ] . required = True
2023-02-27 17:09:28 +00:00
2025-06-16 09:43:08 +00:00
class Meta :
2020-10-27 12:47:46 +00:00
model = BusinessCard
2025-08-19 12:10:15 +00:00
exclude = [ ' intern_notes ' , ' survey_mail_send ' , ' mail_state ' ]
2023-02-27 17:09:29 +00:00
fields = [ ' project ' , ' data ' , ' variant ' , ' url_of_pic ' , ' send_data_to_print ' , ' sent_to ' ]
2025-06-16 09:43:08 +00:00
class Media :
2023-02-27 17:09:29 +00:00
js = ( ' dropdown/js/businessCard.js ' , )
2020-10-27 12:47:46 +00:00
2023-02-27 17:09:29 +00:00
2025-08-31 21:38:22 +00:00
class ListForm ( BaseApplicationForm , CommonOrderMixin ) :
2025-06-16 09:43:08 +00:00
termstoaccept = settings . NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
2020-10-27 12:47:46 +00:00
class Meta :
model = List
fields = [ ' domain ' , ' address ' ]
2023-02-27 17:09:29 +00:00
exclude = [ ' intern_notes ' , ' survey_mail_send ' , ' mail_state ' ]
2025-09-28 22:22:47 +00:00
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 <a href= " https://de.wikipedia.org/wiki/Wikipedia:F % C3 % B6rderung/Versicherung " > Unfall- und Haftpflichtversicherung</a> 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 <a href= " mailto:community@wikimedia.de " >community@wikimedia.de</a>. ' ) ,
}
# 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 ' , [ ] ) )