forked from beba/foerderbarometer
424 lines
15 KiB
Python
Executable File
424 lines
15 KiB
Python
Executable File
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 <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):
|
|
"""
|
|
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 <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))
|
|
|
|
|
|
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 <a href='{}'>Nutzungsbedingungen</a> 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 <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>.'),
|
|
}
|
|
|
|
|
|
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', []))
|