- Dokument-Vorlagen-Editor: create/edit/reset document templates (admin) - Upload-Portal: public portal for Nachweis uploads via token - Onboarding: invite Destinatäre via email with multi-step wizard - Bestätigungsschreiben: preview and send confirmation letters - Email settings: SMTP configuration UI - Management command: import_veranstaltung_teilnehmer for bulk participant import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
9.8 KiB
Python
255 lines
9.8 KiB
Python
# views/veranstaltung.py
|
||
# Phase 0: Vision 2026 – Code-Refactoring
|
||
|
||
import csv
|
||
import io
|
||
import json
|
||
import os
|
||
import time
|
||
from datetime import datetime, timedelta, date
|
||
from decimal import Decimal
|
||
|
||
import qrcode
|
||
import qrcode.image.svg
|
||
import requests
|
||
from django.conf import settings
|
||
from django.contrib import messages
|
||
from django.contrib.auth.decorators import login_required
|
||
from django.core.paginator import Paginator
|
||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||
Sum, Value)
|
||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||
from django.http import HttpResponse, JsonResponse
|
||
from django.shortcuts import get_object_or_404, redirect, render
|
||
from django.urls import reverse
|
||
from django.utils import timezone
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django_otp.decorators import otp_required
|
||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||
from django_otp.util import random_hex
|
||
from rest_framework.decorators import api_view
|
||
from rest_framework.response import Response
|
||
|
||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||
BriefVorlage, CSVImport, Destinataer,
|
||
DestinataerEmailEingang, DestinataerNotiz,
|
||
DestinataerUnterstuetzung,
|
||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||
VierteljahresNachweis)
|
||
from stiftung.forms import (
|
||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||
PaechterForm, DokumentLinkForm,
|
||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||
BankTransactionForm, BankImportForm,
|
||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||
BackupTokenRegenerateForm, PersonForm,
|
||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||
)
|
||
|
||
|
||
@login_required
|
||
def veranstaltung_list(request):
|
||
"""Liste aller Veranstaltungen"""
|
||
veranstaltungen = Veranstaltung.objects.all()
|
||
return render(request, "stiftung/veranstaltung/list.html", {"veranstaltungen": veranstaltungen})
|
||
|
||
|
||
@login_required
|
||
def veranstaltung_detail(request, pk):
|
||
"""Detail-Ansicht einer Veranstaltung mit RSVP-Übersicht"""
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||
teilnehmer = veranstaltung.teilnehmer.all()
|
||
context = {
|
||
"veranstaltung": veranstaltung,
|
||
"teilnehmer": teilnehmer,
|
||
"zugesagte": teilnehmer.filter(rsvp_status="zugesagt"),
|
||
"abgesagte": teilnehmer.filter(rsvp_status="abgesagt"),
|
||
"keine_rueckmeldung": teilnehmer.filter(rsvp_status="keine_rueckmeldung"),
|
||
"eingeladen": teilnehmer.filter(rsvp_status="eingeladen"),
|
||
}
|
||
return render(request, "stiftung/veranstaltung/detail.html", context)
|
||
|
||
|
||
@login_required
|
||
def veranstaltung_serienbrief_pdf(request, pk):
|
||
"""Generiert Serienbrief-PDF für alle Teilnehmer einer Veranstaltung"""
|
||
from weasyprint import HTML
|
||
from stiftung.utils.vorlagen import render_vorlage
|
||
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||
teilnehmer = veranstaltung.teilnehmer.all().order_by("nachname", "vorname")
|
||
|
||
# Render HTML for all letters (DB-Vorlage first, file fallback)
|
||
html_string = render_vorlage(
|
||
"stiftung/veranstaltung/serienbrief_pdf.html",
|
||
{
|
||
"veranstaltung": veranstaltung,
|
||
"teilnehmer": teilnehmer,
|
||
},
|
||
)
|
||
pdf = HTML(string=html_string).write_pdf()
|
||
filename = f"einladungen_{veranstaltung.datum}_{veranstaltung.titel[:30].replace(' ', '_')}.pdf"
|
||
response = HttpResponse(pdf, content_type="application/pdf")
|
||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||
return response
|
||
|
||
|
||
@login_required
|
||
def veranstaltung_serienbrief_vorschau(request, pk):
|
||
"""HTML-Vorschau des Serienbriefs im Browser (kein PDF-Download)"""
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||
teilnehmer = veranstaltung.teilnehmer.all().order_by("nachname", "vorname")
|
||
return render(
|
||
request,
|
||
"stiftung/veranstaltung/serienbrief_vorschau.html",
|
||
{
|
||
"veranstaltung": veranstaltung,
|
||
"teilnehmer": teilnehmer,
|
||
},
|
||
)
|
||
|
||
|
||
@login_required
|
||
def veranstaltung_create(request):
|
||
"""Neue Veranstaltung erstellen"""
|
||
from stiftung.forms import VeranstaltungForm
|
||
|
||
if request.method == "POST":
|
||
form = VeranstaltungForm(request.POST)
|
||
if form.is_valid():
|
||
veranstaltung = form.save()
|
||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde erstellt.')
|
||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||
else:
|
||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||
else:
|
||
form = VeranstaltungForm()
|
||
|
||
return render(request, "stiftung/veranstaltung/form.html", {
|
||
"form": form,
|
||
"title": "Neue Veranstaltung erstellen",
|
||
})
|
||
|
||
|
||
@login_required
|
||
def veranstaltung_update(request, pk):
|
||
"""Veranstaltung bearbeiten (inkl. Serienbrief-Felder)"""
|
||
from stiftung.forms import VeranstaltungForm
|
||
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||
|
||
if request.method == "POST":
|
||
form = VeranstaltungForm(request.POST, instance=veranstaltung)
|
||
if form.is_valid():
|
||
form.save()
|
||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde aktualisiert.')
|
||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||
else:
|
||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||
else:
|
||
form = VeranstaltungForm(instance=veranstaltung)
|
||
|
||
return render(request, "stiftung/veranstaltung/form.html", {
|
||
"form": form,
|
||
"veranstaltung": veranstaltung,
|
||
"title": f"Veranstaltung bearbeiten: {veranstaltung.titel}",
|
||
})
|
||
|
||
|
||
@login_required
|
||
def veranstaltung_delete(request, pk):
|
||
"""Veranstaltung löschen"""
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||
|
||
if request.method == "POST":
|
||
titel = veranstaltung.titel
|
||
veranstaltung.delete()
|
||
messages.success(request, f'Veranstaltung "{titel}" wurde gelöscht.')
|
||
return redirect("stiftung:veranstaltung_list")
|
||
|
||
return render(request, "stiftung/veranstaltung/delete.html", {
|
||
"veranstaltung": veranstaltung,
|
||
})
|
||
|
||
|
||
@login_required
|
||
def teilnehmer_create(request, veranstaltung_pk):
|
||
"""Teilnehmer zu einer Veranstaltung hinzufügen"""
|
||
from stiftung.forms import VeranstaltungsteilnehmerForm
|
||
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||
|
||
if request.method == "POST":
|
||
form = VeranstaltungsteilnehmerForm(request.POST)
|
||
if form.is_valid():
|
||
teilnehmer = form.save(commit=False)
|
||
teilnehmer.veranstaltung = veranstaltung
|
||
teilnehmer.save()
|
||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde hinzugefügt.")
|
||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||
else:
|
||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||
else:
|
||
form = VeranstaltungsteilnehmerForm()
|
||
|
||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||
"form": form,
|
||
"veranstaltung": veranstaltung,
|
||
"title": "Teilnehmer hinzufügen",
|
||
})
|
||
|
||
|
||
@login_required
|
||
def teilnehmer_update(request, veranstaltung_pk, pk):
|
||
"""Teilnehmer bearbeiten"""
|
||
from stiftung.forms import VeranstaltungsteilnehmerForm
|
||
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||
|
||
if request.method == "POST":
|
||
form = VeranstaltungsteilnehmerForm(request.POST, instance=teilnehmer)
|
||
if form.is_valid():
|
||
form.save()
|
||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde aktualisiert.")
|
||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||
else:
|
||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||
else:
|
||
form = VeranstaltungsteilnehmerForm(instance=teilnehmer)
|
||
|
||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||
"form": form,
|
||
"veranstaltung": veranstaltung,
|
||
"teilnehmer": teilnehmer,
|
||
"title": f"Teilnehmer bearbeiten: {teilnehmer.vorname} {teilnehmer.nachname}",
|
||
})
|
||
|
||
|
||
@login_required
|
||
def teilnehmer_delete(request, veranstaltung_pk, pk):
|
||
"""Teilnehmer aus Veranstaltung entfernen"""
|
||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||
|
||
if request.method == "POST":
|
||
name = f"{teilnehmer.vorname} {teilnehmer.nachname}"
|
||
teilnehmer.delete()
|
||
messages.success(request, f"{name} wurde aus der Teilnehmerliste entfernt.")
|
||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||
|
||
return render(request, "stiftung/veranstaltung/teilnehmer_delete.html", {
|
||
"veranstaltung": veranstaltung,
|
||
"teilnehmer": teilnehmer,
|
||
})
|