import datetime from dataclasses import dataclass from smtplib import SMTPException from typing import Optional from urllib.parse import urljoin from django.forms import ChoiceField, Field from django.shortcuts import render, redirect from django.http import HttpResponse, Http404 from django.urls import reverse from django.utils.choices import flatten_choices from django.utils.formats import date_format from django.utils.functional import cached_property from django.utils.http import url_has_allowed_host_and_scheme from django.utils.safestring import mark_safe from django.utils.text import get_text_list from django.core.mail import BadHeaderError from django.conf import settings from django.contrib.auth.decorators import login_required from django.views.generic import TemplateView from django.views.generic.edit import FormView from django.utils.html import strip_tags from input.utils.admin import admin_url_name from input.utils.mail import build_email, collect_and_attach from .forms import ( BaseApplicationForm, ProjectForm, LibraryForm, ELiteratureForm, SoftwareForm, IFGForm, LiteratureForm, TravelForm, EmailForm, ListForm, BusinessCardForm, ) from .models import ( MODELS, LIBRARY_TYPES, TYPE_CHOICES, TYPE_BIB, TYPE_ELIT, TYPE_IFG, TYPE_LIT, TYPE_LIST, TYPE_MAIL, TYPE_PROJ, TYPE_SOFT, TYPE_TRAV, TYPE_VIS, Project, ProductCategoryFormField, ) HELP_TEXTS = { TYPE_IFG: { 'notes': ( 'Bitte gib an, wie die gewonnenen Informationen den
' 'Wikimedia-Projekten zugute kommen sollen.' ) }, TYPE_MAIL: { 'domain': ( 'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
' 'möchtest du eine Mailadresse beantragen?' ) }, TYPE_LIT: { 'notes': 'Bitte gib an, wofür du die Literatur verwenden möchtest.' }, TYPE_LIST: { 'domain': ( 'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,
' 'möchtest du eine Mailingliste beantragen?' ) }, } @dataclass class ApplicationType: code: str path: str form_class: type[BaseApplicationForm] link: str label: Optional[str] = None help_texts: Optional[dict] = None def __post_init__(self): if self.label is None: self.label = TYPE_CHOICES[self.code] if self.help_texts is None: # pragma: no branch self.help_texts = HELP_TEXTS.get(self.code) @property def url(self): return f'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/{self.link}' PROJECT_FUNDING = [ ApplicationType(TYPE_PROJ, 'projektfoerderung', ProjectForm, 'Projektplanung', 'Projektförderung mit einer Gesamtsumme unter 1.000,— EUR'), ApplicationType(TYPE_PROJ, 'projektfoerderung-ab-1000', ProjectForm, 'Projektplanung', 'Projektförderung mit einer Gesamtsumme ab 1.000,— EUR'), ] SERVICES = [ ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm, 'Zugang_zu_Fachliteratur#Bibliotheksstipendium'), ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm, 'Zugang_zu_Fachliteratur#eLiteraturstipendium'), ApplicationType(TYPE_MAIL, 'email', EmailForm, 'E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen'), ApplicationType(TYPE_IFG, 'ifg', IFGForm, 'Geb%C3%BChrenerstattungen_f%C3%BCr_Beh%C3%B6rdenanfragen'), ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm, 'Zugang_zu_Fachliteratur#Literaturstipendium'), ApplicationType(TYPE_LIST, 'mailingliste', ListForm, 'E-Mail-Adressen_und_Visitenkarten#Mailinglisten'), ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm, 'Reisekostenerstattungen'), ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm, 'Software-Stipendien'), ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm, 'E-Mail-Adressen_und_Visitenkarten#Visitenkarten'), ] APPLICATIONS = [ ('Projektförderung', PROJECT_FUNDING), ('Serviceleistungen', SERVICES), ] TYPES = {info.path: info for info in PROJECT_FUNDING + SERVICES} def auth_deny(choice, pk, auth): if choice not in MODELS: return HttpResponse(f'ERROR! UNKNOWN CHOICE TYPE! {choice}') MODELS[choice].set_granted(pk, auth) @login_required def export(request): '''export the project database to a csv''' return HttpResponse('WE WANT CSV!') @login_required def authorize(request, choice, pk): '''If IF grant a support they click a link in a mail which leads here. We write the granted field in the database here and set a timestamp.''' if ret := auth_deny(choice, pk, True): return ret else: return HttpResponse(f'AUTHORIZED! choice: {choice}, pk: {pk}') @login_required def deny(request, choice, pk): '''If IF denies a support they click a link in a mail which leads here We write the granted field in the database here.''' if ret := auth_deny(choice, pk, False): return ret else: 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.') def index(request): return render(request, 'input/index.html') class ApplicationStartView(TemplateView): template_name = 'input/forms/extern.html' extra_context = {'applications': APPLICATIONS} def post(self, request, *args, **kwargs): if url := request.POST.get('url'): if url_has_allowed_host_and_scheme(url, None): return redirect(url) return self.get(request, *args, **kwargs) class ProjectFundingInfoView(TemplateView): template_name = 'input/info_project_funding_gt_1000.html' class ApplicationView(FormView): """ View for all application types. - 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' @cached_property def type_info(self) -> ApplicationType: type_path = self.kwargs['type'] if type_info := TYPES.get(type_path): return type_info raise Http404(f'"{type_path}" existiert nicht.') @property def type_code(self): return self.type_info.code @property def form_class(self): return self.type_info.form_class def get_context_data(self, **kwargs): return super().get_context_data(**kwargs, type_label=self.type_info.label) 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) # Apply help_text overrides if defined for this type_code if help_texts := self.type_info.help_texts: for field, text in help_texts.items(): if field in form.fields: form.fields[field].help_text = mark_safe(text) 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. """ data = self.prepare_data(form) obj = self.save_obj(form, data) if response := self.send_mail(obj, data): return response return done(self.request) def prepare_data(self, form): # Collect cleaned data and mark the current type return {**form.cleaned_data, 'choice': self.type_code} def save_obj(self, form, data): # Save model instance modell = form.save(commit=False) # Username from session if present if user := self.request.session.get('user'): 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'] # Set model.type for specific request types if self.type_code in LIBRARY_TYPES: modell.type = self.type_code # Literature-specific extra field if self.type_code == TYPE_LIT and 'selfbuy_give_data' in data: modell.selfbuy_give_data = data['selfbuy_give_data'] modell.save() if hasattr(form, 'save_m2m'): form.save_m2m() return modell def send_mail(self, obj, data): # Prepare minimal mail context and send mails context = self.get_email_context(obj, data) applicant_subject = 'Deine Förderanfrage bei Wikimedia Deutschland' staff_subject = 'Anfrage {type_label} von {applicant_name}'.format(**context) try: self.send_email('applicant', 'ifg_volunteer_mail', applicant_subject, data['email'], context) self.send_email('staff', 'if_mail', staff_subject, settings.IF_EMAIL, context) except BadHeaderError: obj.delete() return HttpResponse('Invalid header found. Data not saved!') except SMTPException: obj.delete() return HttpResponse('Error in sending mails (probably wrong address?). Data not saved!') def get_email_context(self, obj, data): return { 'data': data, 'urls': self.get_urls(obj), 'form_data': self.get_form_data(obj, data), 'applicant_name': self.get_recipient_name(obj, data), 'type_label': self.sanitize_label(self.type_info.label), } def get_urls(self, obj, **urls): urls['admin'] = self.get_absolute_url(admin_url_name(obj, 'change'), obj.id) if isinstance(obj, Project): urls['authorize'] = urls['deny'] = None else: urls['authorize'] = self.get_absolute_url('authorize', self.type_info.code, obj.id) urls['deny'] = self.get_absolute_url('deny', self.type_info.code, obj.id) return urls @staticmethod def get_absolute_url(view, *args): return urljoin(settings.EMAIL_URL_PREFIX, reverse(view, args=args)) def get_form_data(self, obj, data): return { self.sanitize_label(field.label): self.format_value(field.field, field.initial) for field in self.type_info.form_class(initial=data) } @staticmethod def sanitize_label(label: str): label = strip_tags(label) words = str.split(label) return ' '.join(words) @staticmethod def format_value(field: Field, value): if isinstance(field, ProductCategoryFormField): value = get_text_list(value, 'und') elif isinstance(field, ChoiceField): choices = flatten_choices(field.choices) value = dict(choices).get(value, value) elif isinstance(value, bool): value = '✓' if value else '✗' elif isinstance(value, datetime.date): value = date_format(value) elif value in field.empty_values: value = '–' return value def send_email(self, kind, template_name, subject, recipient, context, *, fail_silently=False): email = build_email(template_name, context, subject, recipient) collect_and_attach(email, kind, self.type_code) return email.send(fail_silently) @staticmethod def get_recipient_name(obj, data): for field in 'username', 'realname', 'email': if name := getattr(obj, field, None) or data.get(field): return name return 'Unbekannt'