From 8d57909a170e0b8df053d773ebbe991cb69353a4 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 27 Aug 2025 11:58:40 +0200 Subject: [PATCH 1/8] =?UTF-8?q?Add=20static=20info=20page=20for=20project?= =?UTF-8?q?=20funding=20=E2=89=A5=201,000=20=E2=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input/info_project_funding_gt_1000.html | 12 ++++++++++++ input/urls.py | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 input/templates/input/info_project_funding_gt_1000.html diff --git a/input/templates/input/info_project_funding_gt_1000.html b/input/templates/input/info_project_funding_gt_1000.html new file mode 100644 index 0000000..2109e51 --- /dev/null +++ b/input/templates/input/info_project_funding_gt_1000.html @@ -0,0 +1,12 @@ + + + + + Projektförderung ab 1.000,— EUR + + +

Projektförderung mit einer Gesamtsumme ab 1.000,— EUR

+

Für Projektförderungen ab 1.000,— EUR ist ein öffentlicher Projektplan erforderlich.

+

Dummy-Content – wird bei Freigabe ersetzt.

+ + diff --git a/input/urls.py b/input/urls.py index 807e9bf..682dfb1 100755 --- a/input/urls.py +++ b/input/urls.py @@ -1,5 +1,5 @@ from django.urls import path - +from django.views.generic import TemplateView from .views import ExternView, index, done, authorize, deny, export urlpatterns = [ @@ -9,4 +9,7 @@ urlpatterns = [ path('export', export, name='export'), path('authorize//', authorize, name='authorize'), path('deny//', deny, name='deny'), + path('extern/info/projektfoerderung-ab-1000/', + TemplateView.as_view(template_name='input/info_project_funding_gt_1000.html'), + name="info-foerderprojekt-ab-1000"), ] From 98d1ae9284b38f0e6ce9c494f2beb3ac3e001f3f Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 31 Aug 2025 23:38:22 +0200 Subject: [PATCH 2/8] refactor(forms): unify forms with BaseApplicationForm and CommonOrderMixin for consistent fields and order --- input/forms.py | 94 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/input/forms.py b/input/forms.py index 32d40d6..cdfa6ba 100755 --- a/input/forms.py +++ b/input/forms.py @@ -1,9 +1,10 @@ from django.conf import settings -from django.forms import ModelForm, ChoiceField, RadioSelect, BooleanField +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 ( TYPE_CHOICES, @@ -33,20 +34,61 @@ class FdbForm(ModelForm): class ProjectForm(FdbForm): - # start = DateField(widget=AdminDateWidget()) class Meta: model = Project - exclude = ('pid', 'project_of_year', 'finance_id','granted', 'granted_date', 'realname', 'email',\ + 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(),} + '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, @@ -75,11 +117,37 @@ class InternForm(FdbForm): HOTEL_CHOICES = {'TRUE': mark_safe('Hotelzimmer benötigt'), - 'FALSE': mark_safe('Kein Hotelzimmer benötigt') - } + 'FALSE': mark_safe('Kein Hotelzimmer benötigt') + } -class TravelForm(FdbForm): +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 @@ -107,7 +175,7 @@ class TravelForm(FdbForm): } -class LibraryForm(FdbForm): +class LibraryForm(BaseApplicationForm, CommonOrderMixin): class Meta: model = Library @@ -144,7 +212,7 @@ class HonoraryCertificateForm(FdbForm): js = ('dropdown/js/otrs_link.js',) -class IFGForm(FdbForm): +class IFGForm(BaseApplicationForm, CommonOrderMixin): class Meta: model = IFG fields = ['cost', 'url', 'notes'] @@ -174,7 +242,7 @@ class CheckForm(FdbForm): # NUTZUNGSBEDINGUNGEN)) -class LiteratureForm(CheckForm): +class LiteratureForm(BaseApplicationForm, CommonOrderMixin): termstoaccept = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM def __init__(self, *args, **kwargs): @@ -192,7 +260,7 @@ ADULT_CHOICES = {'TRUE': mark_safe('Ich bin volljährig.'), } -class EmailForm(CheckForm): +class EmailForm(BaseApplicationForm, CommonOrderMixin): termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE @@ -216,7 +284,7 @@ class EmailForm(CheckForm): -class BusinessCardForm(CheckForm): +class BusinessCardForm(BaseApplicationForm, CommonOrderMixin): termstoaccept = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN # this is the code, to change required to false if needed def __init__(self, *args, **kwargs): @@ -232,7 +300,7 @@ class BusinessCardForm(CheckForm): js = ('dropdown/js/businessCard.js',) -class ListForm(CheckForm): +class ListForm(BaseApplicationForm, CommonOrderMixin): termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN class Meta: model = List From 7cfd477d4070ba403b407f4822924fceaa3f64ee Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 31 Aug 2025 23:41:11 +0200 Subject: [PATCH 3/8] refactor ExternView into separate class-based views with BaseApplicationView --- input/views.py | 271 ++++++++++++++++++++++++++++--------------------- 1 file changed, 158 insertions(+), 113 deletions(-) diff --git a/input/views.py b/input/views.py index 46c92c7..6c92635 100755 --- a/input/views.py +++ b/input/views.py @@ -8,6 +8,7 @@ from django.core.mail import BadHeaderError, EmailMultiAlternatives from django.template.loader import get_template from django.conf import settings from django.contrib.auth.decorators import login_required +from django.views.generic.edit import FormView from .forms import ( ExternForm, @@ -29,6 +30,30 @@ LIBRARY_FORMS = { TYPE_SOFT: SoftwareForm, } +HELP_TEXTS = { + "IFG": { + "notes": ( + "Bitte gib an, wie die gewonnenen Informationen den
" + "Wikimedia-Projekten zugute kommen sollen." + ) + }, + "MAIL": { + "domain": ( + "Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
" + "möchtest du eine Mailadresse beantragen?" + ) + }, + "LIT": { + "notes": "Bitte gib an, wofür du die Literatur verwenden möchtest." + }, + "LIST": { + "domain": ( + "Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
" + "möchtest du eine Mailingliste beantragen?" + ) + }, +} + def auth_deny(choice, pk, auth): if choice not in MODELS: @@ -66,146 +91,166 @@ def deny(request, choice, pk): def done(request): - return HttpResponse("Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.") + return HttpResponse( + "Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.") def index(request): return render(request, 'input/index.html') -class ExternView(CookieWizardView): - '''This View is for Volunteers''' +class BaseApplicationView(FormView): + """ + Base view for all application types. - template_name = "input/extern.html" - form_list = [ExternForm, LibraryForm] - - def get_form(self, step=None, data=None, files=None): - '''this function determines which part of the multipart form is - displayed next''' - - if step is None: - step = self.steps.current - print ("get_form() step " + step) - - if step == '1': - prev_data = self.get_cleaned_data_for_step('0') - choice = prev_data.get('choice') - print(f'choice detection in ExternView: {TYPE_CHOICES[choice]}') - if choice == 'IFG': - form = IFGForm(data) - form.fields['notes'].help_text = mark_safe("Bitte gib an, wie die gewonnenen Informationen den
Wikimedia-Projekten zugute kommen sollen.") - elif choice in LIBRARY_FORMS: - form = LIBRARY_FORMS[choice](data) - elif choice == 'MAIL': - form = EmailForm(data) - form.fields['domain'].help_text = mark_safe("Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
möchtest du eine Mailadresse beantragen?") - elif choice == 'LIT': - form = LiteratureForm(data) - form.fields['notes'].help_text = "Bitte gib an, wofür du die Literatur verwenden möchtest." - elif choice == 'VIS': - form = BusinessCardForm(data) - elif choice == 'LIST': - form = ListForm(data) - form.fields['domain'].help_text = mark_safe("Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
möchtest du eine Mailingliste beantragen?") - elif choice == 'TRAV': - form = TravelForm(data) - else: # pragma: no cover - raise RuntimeError(f'ERROR! UNKNOWN FORMTYPE {choice} in ExternView') - self.choice = choice - else: - form = super().get_form(step, data, files) - return form + - Each application type (travel, literature, email, etc.) gets its own subclass. + - Renders the generic form template. + - Handles saving the submitted form to the database. + - Adds extra fields from the session or request type if needed. + - Applies optional help_text overrides for certain fields. + - Sends confirmation mail to the applicant. + - Sends notification mail to the internal IF address. + - Returns the "done" response after successful processing. + """ + template_name = "input/forms/form_generic.html" + type_code: str = "" def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if hasattr(self, 'choice'): - context["choice"] = TYPE_CHOICES[self.choice] - return context + """Add the human-readable type string (from TYPE_CHOICES) to the template context.""" + ctx = super().get_context_data(**kwargs) + ctx["typestring"] = TYPE_CHOICES.get(self.type_code, self.type_code) + return ctx - def done(self, form_list, **kwargs): - print('ExternView.done() reached') - # gather data from all forms - data = {} - for form in form_list: - data = {**data, **form.cleaned_data} + def get_form(self, form_class=None): + """Return the form instance and inject custom help_texts if defined for this type.""" + form = super().get_form(form_class) - if data['choice'] == 'LIT': - if data['selfbuy'] == 'TRUE': - data['selfbuy_give_data'] = 'False' + # Apply help_text overrides if defined for this type_code + if self.type_code in HELP_TEXTS: + for field, text in HELP_TEXTS[self.type_code].items(): + if field in form.fields: + form.fields[field].help_text = mark_safe(text) - # write data to database + return form + + def form_valid(self, form): + """ + Process a valid form submission: + - Enrich form data (e.g., set type_code, handle special rules). + - Save the model instance and related data. + - Send confirmation and notification mails. + - Return the "done" response. + """ + + # Collect cleaned data and mark the current type + data = form.cleaned_data.copy() + data["choice"] = self.type_code + + # Special rule for literature applications + if self.type_code == "LIT" and data.get("selfbuy") == "TRUE": + data["selfbuy_give_data"] = "False" + + # Save model instance modell = form.save(commit=False) - # we have to copy the data from the first form here - # this is a bit ugly code. can we copy this without explicit writing? - if data['choice'] == 'LIT': - modell.selfbuy_give_data = data['selfbuy_give_data'] + # Username from session if present + user = self.request.session.get("user") + if user: + modell.username = user.get("username") - if user := self.request.session.get('user'): - modell.username = user['username'] + # Copy common fields if provided by the form + if "realname" in data: + modell.realname = data["realname"] + if "email" in data: + modell.email = data["email"] - modell.realname = data['realname'] - modell.email = data['email'] - # write type of form in some cases - if data['choice'] in ('BIB', 'ELIT', 'SOFT'): - modell.type = data['choice'] + # Set model.type for specific request types + if self.type_code in ("BIB", "ELIT", "SOFT"): + modell.type = self.type_code - form.save() + # Literature-specific extra field + if self.type_code == "LIT" and "selfbuy_give_data" in data: + modell.selfbuy_give_data = data["selfbuy_give_data"] - # add some data to context for mail templates - data['pk'] = modell.pk - data['url_prefix'] = settings.EMAIL_URL_PREFIX - data['grant'] = ('LIT', 'SOFT', 'ELIT', 'BIB', 'IFG') - data['DOMAIN'] = ('MAIL', 'LIST') - data['typestring'] = TYPE_CHOICES[data['choice']] + modell.save() + if hasattr(form, "save_m2m"): + form.save_m2m() + + # Prepare minimal mail context and send mails + data["pk"] = modell.pk + data["url_prefix"] = settings.EMAIL_URL_PREFIX + data["typestring"] = TYPE_CHOICES.get(self.type_code, self.type_code) + context = {"data": data} - # we need to send the following mails here: - context = { 'data': data } try: - # - mail with entered data to the Volunteer - - txt_mail_template1 = get_template('input/ifg_volunteer_mail.txt') - html_mail_template1 = get_template('input/ifg_volunteer_mail.html') - - subject1, from_email1, to1 = 'Formular ausgefüllt', settings.IF_EMAIL, data['email'] - text_content1 = txt_mail_template1.render(context) - html_content1 = html_mail_template1.render(context) - msg1 = EmailMultiAlternatives(subject1, text_content1, from_email1, [to1]) - msg1.attach_alternative(html_content1, "text/html") + # Mail to applicant + txt1 = get_template("input/ifg_volunteer_mail.txt").render(context) + html1 = get_template("input/ifg_volunteer_mail.html").render(context) + msg1 = EmailMultiAlternatives( + "Formular ausgefüllt", txt1, settings.IF_EMAIL, [data["email"]] + ) + msg1.attach_alternative(html1, "text/html") msg1.send() - #print('ifg volunteer mail would have been sent') - #send_mail( - # 'Formular ausgefüllt', - # txt_mail_template1.render(context), - # IF_EMAIL, - # [data['email']], - # fail_silently=False) - ## - mail to IF with link to accept/decline - txt_mail_template = get_template('input/if_mail.txt') - html_mail_template = get_template('input/if_mail.html') - - subject, from_email, to = 'Formular ausgefüllt', settings.IF_EMAIL, settings.IF_EMAIL - text_content = txt_mail_template.render(context) - html_content = html_mail_template.render(context) - msg2 = EmailMultiAlternatives(subject, text_content, from_email, [to]) - msg2.attach_alternative(html_content, "text/html") + # Mail to IF + txt2 = get_template("input/if_mail.txt").render(context) + html2 = get_template("input/if_mail.html").render(context) + msg2 = EmailMultiAlternatives( + "Formular ausgefüllt", txt2, settings.IF_EMAIL, [settings.IF_EMAIL] + ) + msg2.attach_alternative(html2, "text/html") msg2.send() - #print('if mail would have been sent') - #send_mail( - # 'Formular ausgefüllt', - # txt_mail_template.render(context), - # IF_EMAIL, - # [IF_EMAIL], - # fail_silently=False) - ## raise SMTPException("testing pupose only") except BadHeaderError: modell.delete() - return HttpResponse('Invalid header found. Data not saved!') + return HttpResponse("Invalid header found. Data not saved!") except SMTPException: modell.delete() - return HttpResponse('Error in sending mails (probably wrong adress?). Data not saved!') - + return HttpResponse("Error in sending mails (probably wrong adress?). Data not saved!") return done(self.request) + + +class TravelApplicationView(BaseApplicationView): + form_class = TravelForm + type_code = "TRAV" + + +class LibraryApplicationView(BaseApplicationView): + form_class = LibraryForm + type_code = "BIB" + + +class ELiteratureApplicationView(BaseApplicationView): + form_class = ELiteratureForm + type_code = "ELIT" + + +class SoftwareApplicationView(BaseApplicationView): + form_class = SoftwareForm + type_code = "SOFT" + + +class IFGApplicationView(BaseApplicationView): + form_class = IFGForm + type_code = "IFG" + + +class EmailApplicationView(BaseApplicationView): + form_class = EmailForm + type_code = "MAIL" + + +class LiteratureApplicationView(BaseApplicationView): + form_class = LiteratureForm + type_code = "LIT" + + +class ListApplicationView(BaseApplicationView): + form_class = ListForm + type_code = "LIST" + + +class BusinessCardApplicationView(BaseApplicationView): + form_class = BusinessCardForm + type_code = "VIS" From 35ae9ab0e5bafb2931e85ccd8cd1b3f63ecf6cde Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 31 Aug 2025 23:43:29 +0200 Subject: [PATCH 4/8] update URL routing to use new class-based application views and static extern page --- input/urls.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/input/urls.py b/input/urls.py index 682dfb1..d23ebdf 100755 --- a/input/urls.py +++ b/input/urls.py @@ -1,15 +1,41 @@ from django.urls import path from django.views.generic import TemplateView -from .views import ExternView, index, done, authorize, deny, export +from django.views.i18n import JavaScriptCatalog +from .views import ( + index, done, export, authorize, deny, + TravelApplicationView, IFGApplicationView, EmailApplicationView, + LiteratureApplicationView, ListApplicationView, BusinessCardApplicationView, + LibraryApplicationView, ELiteratureApplicationView, SoftwareApplicationView, +) urlpatterns = [ path('', index, name='index'), - path('extern', ExternView.as_view(), name='extern'), + path( + "extern/", + TemplateView.as_view(template_name="input/forms/extern.html"), + name="extern", + ), path('saved', done, name='done'), path('export', export, name='export'), path('authorize//', authorize, name='authorize'), path('deny//', deny, name='deny'), + + # Static info page for project funding above 1000 EUR path('extern/info/projektfoerderung-ab-1000/', TemplateView.as_view(template_name='input/info_project_funding_gt_1000.html'), name="info-foerderprojekt-ab-1000"), + + # New single-page application views + path("extern/reisekosten/", TravelApplicationView.as_view(), name="reisekosten"), + path("extern/ifg/", IFGApplicationView.as_view(), name="ifg"), + path("extern/email/", EmailApplicationView.as_view(), name="email"), + path("extern/literaturstipendium/", LiteratureApplicationView.as_view(), name="literatur"), + path("extern/mailingliste/", ListApplicationView.as_view(), name="mailingliste"), + path("extern/visitenkarten/", BusinessCardApplicationView.as_view(), name="visitenkarten"), + path("extern/bibliotheksstipendium/", LibraryApplicationView.as_view(), name="bibliotheksstipendium"), + path("extern/eliteraturstipendium/", ELiteratureApplicationView.as_view(), name="eliteraturstipendium"), + path("extern/softwarestipendium/", SoftwareApplicationView.as_view(), name="softwarestipendium"), + + # JavaScript translations for date widgets, etc. + path('jsi18n/', JavaScriptCatalog.as_view(), name='jsi18n'), ] From a2ec4071b48472c16e8d909a5d87aa27d6564162 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 31 Aug 2025 23:47:27 +0200 Subject: [PATCH 5/8] restructure templates: introduce base with header/footer, replace old extern template, add generic form layout, and update info page --- input/templates/input/base.html | 37 ++++++++++ input/templates/input/extern.html | 71 ------------------- input/templates/input/forms/extern.html | 38 ++++++++++ input/templates/input/forms/form_generic.html | 30 ++++++++ .../input/info_project_funding_gt_1000.html | 10 ++- input/templates/input/partials/_footer.html | 21 ++++++ input/templates/input/partials/_header.html | 4 ++ 7 files changed, 138 insertions(+), 73 deletions(-) create mode 100644 input/templates/input/base.html delete mode 100755 input/templates/input/extern.html create mode 100755 input/templates/input/forms/extern.html create mode 100644 input/templates/input/forms/form_generic.html create mode 100644 input/templates/input/partials/_footer.html create mode 100644 input/templates/input/partials/_header.html diff --git a/input/templates/input/base.html b/input/templates/input/base.html new file mode 100644 index 0000000..9f75cb2 --- /dev/null +++ b/input/templates/input/base.html @@ -0,0 +1,37 @@ +{% load static %} + + + + + + + + + + + + + + + + + + + + + + + {% block head_extra %}{% endblock %} + + +{% include "input/partials/_header.html" %} + +
+ {% block pre_content %}{% endblock %} + {% block content %}{% endblock %} + {% block post_content %}{% endblock %} +
+ +{% include "input/partials/_footer.html" %} + + diff --git a/input/templates/input/extern.html b/input/templates/input/extern.html deleted file mode 100755 index d0721bf..0000000 --- a/input/templates/input/extern.html +++ /dev/null @@ -1,71 +0,0 @@ -{% load static %} - - - - - - -{{ form.media }} - - - - - -{% load i18n %} -{% block content %} - -
- - - - -

Schritt {{ wizard.steps.step1 }} von {{ wizard.steps.count }}

-
- {% csrf_token %} - -{% if choice %} -Du hast {{choice}} ausgewählt. -{% endif %} - {{ wizard.management_form }} - {% if wizard.form.forms %} - {{ wizard.form.management_form }} - {% for form in wizard.form.forms %} - {{ form }} - {% endfor %} - {% else %} - {{ wizard.form }} - {% endif %} -
-

- * Pflichtfeld -

- {% if wizard.steps.prev %} - - {% endif %} - {% if wizard.steps.current == wizard.steps.last %} - - {% else %} - - {% endif %} -

-

-

-Eine Übersicht aller Förderangebote von Wikimedia Deutschland findest du im - Förderportal in der deutschsprachigen Wikipedia. -
Für alle Fragen wende dich gern an das Team Communitys und Engagement. -

-Für interessierte Hacker gibts auch den Sourcecode zum Formular und was damit passiert. -

- Impressum -

{% endblock %} diff --git a/input/templates/input/forms/extern.html b/input/templates/input/forms/extern.html new file mode 100755 index 0000000..0566f2b --- /dev/null +++ b/input/templates/input/forms/extern.html @@ -0,0 +1,38 @@ +{% extends "input/base.html" %} + +{% load i18n %} +{% block content %} +
+ + + + + +
Was möchtest du beantragen? + Projektförderung + + + Serviceleistungen + +
+
+{% endblock %} diff --git a/input/templates/input/forms/form_generic.html b/input/templates/input/forms/form_generic.html new file mode 100644 index 0000000..b7b1f92 --- /dev/null +++ b/input/templates/input/forms/form_generic.html @@ -0,0 +1,30 @@ +{% extends "input/base.html" %} +{% load static %} + +{% block content %} + {{ form.media }} + +
+

Du hast {{ typestring }} ausgewählt.

+
+ +
+ {% csrf_token %} + + {% block pre_table %}{% endblock %} + + + {{ form.non_field_errors }} + {{ form.as_table }} +
+ + {% block post_table %}{% endblock %} + +

* Pflichtfeld

+ +
+ + +
+
+{% endblock %} diff --git a/input/templates/input/info_project_funding_gt_1000.html b/input/templates/input/info_project_funding_gt_1000.html index 2109e51..d3e9856 100644 --- a/input/templates/input/info_project_funding_gt_1000.html +++ b/input/templates/input/info_project_funding_gt_1000.html @@ -6,7 +6,13 @@

Projektförderung mit einer Gesamtsumme ab 1.000,— EUR

-

Für Projektförderungen ab 1.000,— EUR ist ein öffentlicher Projektplan erforderlich.

-

Dummy-Content – wird bei Freigabe ersetzt.

+

Vielen Dank für dein Interesse an einer Projektförderung!
+ Für Projektförderungen mit einer Gesamtsumme ab 1.000,— EUR ist ein öffentlicher Projektplan + erforderlich. Weitere Informationen zu diesem Prozess findest du unter + + Wikipedia:Förderung/Projektplanung.
+ Für Fragen steht dir das Team Community-Konferenzen & Förderung gern unter + community@wikimedia.de zur Verfügung. +

diff --git a/input/templates/input/partials/_footer.html b/input/templates/input/partials/_footer.html new file mode 100644 index 0000000..af0f7e0 --- /dev/null +++ b/input/templates/input/partials/_footer.html @@ -0,0 +1,21 @@ + diff --git a/input/templates/input/partials/_header.html b/input/templates/input/partials/_header.html new file mode 100644 index 0000000..d2a45f4 --- /dev/null +++ b/input/templates/input/partials/_header.html @@ -0,0 +1,4 @@ +{% load static %} +
+ Wikimedia Deutschland +
From 8ba54bdca90c5f9c94ebac8e7d09357c725064af Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 31 Aug 2025 23:55:50 +0200 Subject: [PATCH 6/8] add CSS styles for form layout --- input/static/css/forms.css | 11 +++++++++++ input/templates/input/base.html | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 input/static/css/forms.css diff --git a/input/static/css/forms.css b/input/static/css/forms.css new file mode 100644 index 0000000..548ddc7 --- /dev/null +++ b/input/static/css/forms.css @@ -0,0 +1,11 @@ +.star { + color: red; +} + +.wm-table { + margin: 0 auto; +} + +.col-request { + width: 40%; +} \ No newline at end of file diff --git a/input/templates/input/base.html b/input/templates/input/base.html index 9f75cb2..7a08389 100644 --- a/input/templates/input/base.html +++ b/input/templates/input/base.html @@ -17,7 +17,7 @@ - + From 238c6745172be2d094eb459e9246311cc5684df4 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 1 Sep 2025 13:24:59 +0200 Subject: [PATCH 7/8] change all string literals to single quotes --- input/forms.py | 8 ++-- input/urls.py | 26 ++++++------ input/views.py | 112 ++++++++++++++++++++++++------------------------- 3 files changed, 73 insertions(+), 73 deletions(-) diff --git a/input/forms.py b/input/forms.py index cdfa6ba..cd9e281 100755 --- a/input/forms.py +++ b/input/forms.py @@ -132,14 +132,14 @@ class BaseApplicationForm(FdbForm): - Ensures consistency across all application types. """ realname = CharField( - label="Realname", + label='Realname', required=True, - help_text="Bitte gib deinen Vor- und Nachnamen ein." + help_text='Bitte gib deinen Vor- und Nachnamen ein.' ) email = EmailField( - label="E-Mail-Adresse", + 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." + 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( diff --git a/input/urls.py b/input/urls.py index d23ebdf..3d89297 100755 --- a/input/urls.py +++ b/input/urls.py @@ -11,9 +11,9 @@ from .views import ( urlpatterns = [ path('', index, name='index'), path( - "extern/", - TemplateView.as_view(template_name="input/forms/extern.html"), - name="extern", + 'extern/', + TemplateView.as_view(template_name='input/forms/extern.html'), + name='extern', ), path('saved', done, name='done'), path('export', export, name='export'), @@ -23,18 +23,18 @@ urlpatterns = [ # Static info page for project funding above 1000 EUR path('extern/info/projektfoerderung-ab-1000/', TemplateView.as_view(template_name='input/info_project_funding_gt_1000.html'), - name="info-foerderprojekt-ab-1000"), + name='info-foerderprojekt-ab-1000'), # New single-page application views - path("extern/reisekosten/", TravelApplicationView.as_view(), name="reisekosten"), - path("extern/ifg/", IFGApplicationView.as_view(), name="ifg"), - path("extern/email/", EmailApplicationView.as_view(), name="email"), - path("extern/literaturstipendium/", LiteratureApplicationView.as_view(), name="literatur"), - path("extern/mailingliste/", ListApplicationView.as_view(), name="mailingliste"), - path("extern/visitenkarten/", BusinessCardApplicationView.as_view(), name="visitenkarten"), - path("extern/bibliotheksstipendium/", LibraryApplicationView.as_view(), name="bibliotheksstipendium"), - path("extern/eliteraturstipendium/", ELiteratureApplicationView.as_view(), name="eliteraturstipendium"), - path("extern/softwarestipendium/", SoftwareApplicationView.as_view(), name="softwarestipendium"), + path('extern/reisekosten/', TravelApplicationView.as_view(), name='reisekosten'), + path('extern/ifg/', IFGApplicationView.as_view(), name='ifg'), + path('extern/email/', EmailApplicationView.as_view(), name='email'), + path('extern/literaturstipendium/', LiteratureApplicationView.as_view(), name='literatur'), + path('extern/mailingliste/', ListApplicationView.as_view(), name='mailingliste'), + path('extern/visitenkarten/', BusinessCardApplicationView.as_view(), name='visitenkarten'), + path('extern/bibliotheksstipendium/', LibraryApplicationView.as_view(), name='bibliotheksstipendium'), + path('extern/eliteraturstipendium/', ELiteratureApplicationView.as_view(), name='eliteraturstipendium'), + path('extern/softwarestipendium/', SoftwareApplicationView.as_view(), name='softwarestipendium'), # JavaScript translations for date widgets, etc. path('jsi18n/', JavaScriptCatalog.as_view(), name='jsi18n'), diff --git a/input/views.py b/input/views.py index 6c92635..b71e301 100755 --- a/input/views.py +++ b/input/views.py @@ -31,25 +31,25 @@ LIBRARY_FORMS = { } HELP_TEXTS = { - "IFG": { - "notes": ( - "Bitte gib an, wie die gewonnenen Informationen den
" - "Wikimedia-Projekten zugute kommen sollen." + 'IFG': { + 'notes': ( + 'Bitte gib an, wie die gewonnenen Informationen den
' + 'Wikimedia-Projekten zugute kommen sollen.' ) }, - "MAIL": { - "domain": ( - "Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
" - "möchtest du eine Mailadresse beantragen?" + 'MAIL': { + 'domain': ( + 'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
' + 'möchtest du eine Mailadresse beantragen?' ) }, - "LIT": { - "notes": "Bitte gib an, wofür du die Literatur verwenden möchtest." + 'LIT': { + 'notes': 'Bitte gib an, wofür du die Literatur verwenden möchtest.' }, - "LIST": { - "domain": ( - "Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
" - "möchtest du eine Mailingliste beantragen?" + 'LIST': { + 'domain': ( + 'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
' + 'möchtest du eine Mailingliste beantragen?' ) }, } @@ -76,7 +76,7 @@ def authorize(request, choice, pk): if ret := auth_deny(choice, pk, True): return ret else: - return HttpResponse(f"AUTHORIZED! choice: {choice}, pk: {pk}") + return HttpResponse(f'AUTHORIZED! choice: {choice}, pk: {pk}') @login_required @@ -87,12 +87,12 @@ def deny(request, choice, pk): if ret := auth_deny(choice, pk, False): return ret else: - return HttpResponse(f"DENIED! choice: {choice}, pk: {pk}") + return HttpResponse(f'DENIED! choice: {choice}, pk: {pk}') def done(request): return HttpResponse( - "Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.") + 'Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.') def index(request): @@ -112,13 +112,13 @@ class BaseApplicationView(FormView): - Sends notification mail to the internal IF address. - Returns the "done" response after successful processing. """ - template_name = "input/forms/form_generic.html" - type_code: str = "" + template_name = 'input/forms/form_generic.html' + type_code: str = '' def get_context_data(self, **kwargs): """Add the human-readable type string (from TYPE_CHOICES) to the template context.""" ctx = super().get_context_data(**kwargs) - ctx["typestring"] = TYPE_CHOICES.get(self.type_code, self.type_code) + ctx['typestring'] = TYPE_CHOICES.get(self.type_code, self.type_code) return ctx def get_form(self, form_class=None): @@ -144,113 +144,113 @@ class BaseApplicationView(FormView): # Collect cleaned data and mark the current type data = form.cleaned_data.copy() - data["choice"] = self.type_code + data['choice'] = self.type_code # Special rule for literature applications - if self.type_code == "LIT" and data.get("selfbuy") == "TRUE": - data["selfbuy_give_data"] = "False" + if self.type_code == 'LIT' and data.get('selfbuy') == 'TRUE': + data['selfbuy_give_data'] = 'False' # Save model instance modell = form.save(commit=False) # Username from session if present - user = self.request.session.get("user") + user = self.request.session.get('user') if user: - modell.username = user.get("username") + modell.username = user.get('username') # Copy common fields if provided by the form - if "realname" in data: - modell.realname = data["realname"] - if "email" in data: - modell.email = data["email"] + if 'realname' in data: + modell.realname = data['realname'] + if 'email' in data: + modell.email = data['email'] # Set model.type for specific request types - if self.type_code in ("BIB", "ELIT", "SOFT"): + if self.type_code in ('BIB', 'ELIT', 'SOFT'): modell.type = self.type_code # Literature-specific extra field - if self.type_code == "LIT" and "selfbuy_give_data" in data: - modell.selfbuy_give_data = data["selfbuy_give_data"] + if self.type_code == 'LIT' and 'selfbuy_give_data' in data: + modell.selfbuy_give_data = data['selfbuy_give_data'] modell.save() - if hasattr(form, "save_m2m"): + if hasattr(form, 'save_m2m'): form.save_m2m() # Prepare minimal mail context and send mails - data["pk"] = modell.pk - data["url_prefix"] = settings.EMAIL_URL_PREFIX - data["typestring"] = TYPE_CHOICES.get(self.type_code, self.type_code) - context = {"data": data} + data['pk'] = modell.pk + data['url_prefix'] = settings.EMAIL_URL_PREFIX + data['typestring'] = TYPE_CHOICES.get(self.type_code, self.type_code) + context = {'data': data} try: # Mail to applicant - txt1 = get_template("input/ifg_volunteer_mail.txt").render(context) - html1 = get_template("input/ifg_volunteer_mail.html").render(context) + txt1 = get_template('input/ifg_volunteer_mail.txt').render(context) + html1 = get_template('input/ifg_volunteer_mail.html').render(context) msg1 = EmailMultiAlternatives( - "Formular ausgefüllt", txt1, settings.IF_EMAIL, [data["email"]] + 'Formular ausgefüllt', txt1, settings.IF_EMAIL, [data['email']] ) - msg1.attach_alternative(html1, "text/html") + msg1.attach_alternative(html1, 'text/html') msg1.send() # Mail to IF - txt2 = get_template("input/if_mail.txt").render(context) - html2 = get_template("input/if_mail.html").render(context) + txt2 = get_template('input/if_mail.txt').render(context) + html2 = get_template('input/if_mail.html').render(context) msg2 = EmailMultiAlternatives( - "Formular ausgefüllt", txt2, settings.IF_EMAIL, [settings.IF_EMAIL] + 'Formular ausgefüllt', txt2, settings.IF_EMAIL, [settings.IF_EMAIL] ) - msg2.attach_alternative(html2, "text/html") + msg2.attach_alternative(html2, 'text/html') msg2.send() except BadHeaderError: modell.delete() - return HttpResponse("Invalid header found. Data not saved!") + return HttpResponse('Invalid header found. Data not saved!') except SMTPException: modell.delete() - return HttpResponse("Error in sending mails (probably wrong adress?). Data not saved!") + return HttpResponse('Error in sending mails (probably wrong adress?). Data not saved!') return done(self.request) class TravelApplicationView(BaseApplicationView): form_class = TravelForm - type_code = "TRAV" + type_code = 'TRAV' class LibraryApplicationView(BaseApplicationView): form_class = LibraryForm - type_code = "BIB" + type_code = 'BIB' class ELiteratureApplicationView(BaseApplicationView): form_class = ELiteratureForm - type_code = "ELIT" + type_code = 'ELIT' class SoftwareApplicationView(BaseApplicationView): form_class = SoftwareForm - type_code = "SOFT" + type_code = 'SOFT' class IFGApplicationView(BaseApplicationView): form_class = IFGForm - type_code = "IFG" + type_code = 'IFG' class EmailApplicationView(BaseApplicationView): form_class = EmailForm - type_code = "MAIL" + type_code = 'MAIL' class LiteratureApplicationView(BaseApplicationView): form_class = LiteratureForm - type_code = "LIT" + type_code = 'LIT' class ListApplicationView(BaseApplicationView): form_class = ListForm - type_code = "LIST" + type_code = 'LIST' class BusinessCardApplicationView(BaseApplicationView): form_class = BusinessCardForm - type_code = "VIS" + type_code = 'VIS' From a239922e14cdfdd0845781c2ce78455adc5bc026 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 1 Sep 2025 13:33:17 +0200 Subject: [PATCH 8/8] use walrus operator for user session check --- input/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/input/views.py b/input/views.py index b71e301..04b5dc8 100755 --- a/input/views.py +++ b/input/views.py @@ -154,8 +154,7 @@ class BaseApplicationView(FormView): modell = form.save(commit=False) # Username from session if present - user = self.request.session.get('user') - if user: + if user := self.request.session.get('user'): modell.username = user.get('username') # Copy common fields if provided by the form