foerderbarometer/input/views.py

370 lines
12 KiB
Python
Raw Normal View History

import datetime
from dataclasses import dataclass
from smtplib import SMTPException
from typing import Optional
from urllib.parse import urljoin
from django.forms import ChoiceField, BoundField, Field
from django.shortcuts import render, redirect
2025-10-14 09:39:58 +00:00
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
2025-10-14 09:39:58 +00:00
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
2025-10-17 12:51:18 +00:00
from django.core.mail import BadHeaderError
from django.conf import settings
from django.contrib.auth.decorators import login_required
2025-10-14 09:39:58 +00:00
from django.views.generic import TemplateView
from django.views.generic.edit import FormView
from django.utils.html import strip_tags
2025-10-17 10:06:23 +00:00
from input.utils.admin import admin_url_name
2025-10-17 15:31:51 +00:00
from input.utils.mail import build_email, collect_and_attach
2020-09-22 10:21:05 +00:00
from .forms import (
2025-10-14 09:39:58 +00:00
BaseApplicationForm,
ProjectForm,
LibraryForm,
ELiteratureForm,
SoftwareForm,
IFGForm,
LiteratureForm,
TravelForm,
EmailForm,
ListForm,
BusinessCardForm,
)
2025-10-14 09:39:58 +00:00
from .models import (
MODELS,
LIBRARY_TYPES,
TYPE_CHOICES,
TYPE_BIB,
TYPE_ELIT,
TYPE_IFG,
TYPE_LIT,
TYPE_LIST,
TYPE_MAIL,
TYPE_PROJ,
2025-10-14 09:39:58 +00:00
TYPE_SOFT,
TYPE_TRAV,
TYPE_VIS,
Project,
2025-10-14 09:39:58 +00:00
)
HELP_TEXTS = {
2025-10-14 09:39:58 +00:00
TYPE_IFG: {
'notes': (
'Bitte gib an, wie die gewonnenen Informationen den<br>'
'Wikimedia-Projekten zugute kommen sollen.'
)
},
2025-10-14 09:39:58 +00:00
TYPE_MAIL: {
'domain': (
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
'möchtest du eine Mailadresse beantragen?'
)
},
2025-10-14 09:39:58 +00:00
TYPE_LIT: {
'notes': 'Bitte gib an, wofür du die Literatur verwenden möchtest.'
},
2025-10-14 09:39:58 +00:00
TYPE_LIST: {
'domain': (
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
'möchtest du eine Mailingliste beantragen?'
)
},
}
@dataclass
class ApplicationType:
2025-10-14 09:39:58 +00:00
code: str
path: str
form_class: type[BaseApplicationForm]
link: str
label: Optional[str] = None
help_texts: Optional[dict] = None
2025-10-14 09:39:58 +00:00
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)
2025-10-14 09:39:58 +00:00
@property
def url(self):
return f'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/{self.link}'
2025-10-14 09:39:58 +00:00
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'),
]
2025-11-10 10:37:00 +00:00
APPLICATIONS = [
('Projektförderung', PROJECT_FUNDING),
('Serviceleistungen', SERVICES),
2025-10-14 09:39:58 +00:00
]
TYPES = {info.path: info for info in PROJECT_FUNDING + SERVICES}
2025-10-14 09:39:58 +00:00
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)
2025-08-21 08:08:38 +00:00
2020-11-19 14:55:10 +00:00
@login_required
def export(request):
'''export the project database to a csv'''
return HttpResponse('WE WANT CSV!')
2021-01-04 09:44:03 +00:00
2025-08-21 08:08:38 +00:00
@login_required
def authorize(request, choice, pk):
2020-10-21 07:54:12 +00:00
'''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.'''
2025-08-21 08:42:55 +00:00
if ret := auth_deny(choice, pk, True):
return ret
else:
return HttpResponse(f'AUTHORIZED! choice: {choice}, pk: {pk}')
2025-08-21 08:08:38 +00:00
@login_required
def deny(request, choice, pk):
2020-10-21 07:54:12 +00:00
'''If IF denies a support they click a link in a mail which leads here
We write the granted field in the database here.'''
2025-08-21 08:42:55 +00:00
if ret := auth_deny(choice, pk, False):
return ret
else:
return HttpResponse(f'DENIED! choice: {choice}, pk: {pk}')
2020-09-29 09:08:16 +00:00
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.')
2020-09-30 12:26:08 +00:00
2025-08-21 08:02:19 +00:00
def index(request):
return render(request, 'input/index.html')
2020-11-18 15:03:27 +00:00
2025-10-14 09:39:58 +00:00
class ApplicationStartView(TemplateView):
template_name = 'input/forms/extern.html'
2025-11-10 10:37:00 +00:00
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)
2025-10-14 09:39:58 +00:00
class ProjectFundingInfoView(TemplateView):
2025-10-14 09:39:58 +00:00
template_name = 'input/info_project_funding_gt_1000.html'
class ApplicationView(FormView):
"""
2025-10-14 09:39:58 +00:00
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.
"""
2025-10-14 09:39:58 +00:00
template_name = 'input/forms/form_generic.html'
2025-10-14 09:39:58 +00:00
@cached_property
def type_info(self) -> ApplicationType:
type_path = self.kwargs['type']
if type_info := TYPES.get(type_path):
return type_info
2025-10-14 09:39:58 +00:00
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
2020-11-18 11:05:18 +00:00
def get_context_data(self, **kwargs):
2025-10-14 09:39:58 +00:00
return super().get_context_data(**kwargs, type_label=self.type_info.label)
2025-08-18 14:32:31 +00:00
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
2025-10-14 09:39:58 +00:00
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
2025-11-10 16:54:03 +00:00
return {**form.cleaned_data, 'choice': self.type_code}
def save_obj(self, form, data):
# Save model instance
modell = form.save(commit=False)
2025-08-18 14:32:31 +00:00
# Username from session if present
if user := self.request.session.get('user'):
modell.username = user.get('username')
2025-08-18 14:32:31 +00:00
# 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
2025-10-14 09:39:58 +00:00
if self.type_code in LIBRARY_TYPES:
modell.type = self.type_code
# Literature-specific extra field
2025-10-14 09:39:58 +00:00
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)
2025-10-17 10:06:23 +00:00
applicant_subject = 'Deine Förderanfrage bei Wikimedia Deutschland'
staff_subject = 'Anfrage {type_label} von {applicant_name}'.format(**context)
2025-08-18 14:32:31 +00:00
2025-10-17 10:06:23 +00:00
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)
2020-10-08 10:38:49 +00:00
except BadHeaderError:
obj.delete()
return HttpResponse('Invalid header found. Data not saved!')
except SMTPException:
obj.delete()
2025-10-17 10:06:23 +00:00
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, 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
2025-10-17 12:51:18 +00:00
def send_email(self, kind, template_name, subject, recipient, context, *, fail_silently=False):
email = build_email(template_name, context, subject, recipient)
2025-10-17 10:06:23 +00:00
2025-10-17 15:31:51 +00:00
collect_and_attach(email, kind, self.type_code)
2025-10-17 10:06:23 +00:00
2025-10-17 12:51:18 +00:00
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'