forked from beba/foerderbarometer
377 lines
12 KiB
Python
Executable File
377 lines
12 KiB
Python
Executable File
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
|
||
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.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,
|
||
)
|
||
|
||
HELP_TEXTS = {
|
||
TYPE_IFG: {
|
||
'notes': (
|
||
'Bitte gib an, wie die gewonnenen Informationen den<br>'
|
||
'Wikimedia-Projekten zugute kommen sollen.'
|
||
)
|
||
},
|
||
TYPE_MAIL: {
|
||
'domain': (
|
||
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
|
||
'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,<br>'
|
||
'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[str] = None
|
||
|
||
def __post_init__(self):
|
||
if self.label is None:
|
||
self.label = TYPE_CHOICES[self.code]
|
||
|
||
if self.help_texts is None:
|
||
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
|
||
|
||
data = {**form.cleaned_data, 'choice': self.type_code}
|
||
|
||
# Special rule for literature applications
|
||
if self.type_code == TYPE_LIT and data.get('selfbuy') == 'TRUE':
|
||
data['selfbuy_give_data'] = 'False'
|
||
|
||
return data
|
||
|
||
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, 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'
|