Files
stiftung-management-system/app/stiftung/views/geschichte.py
SysAdmin Agent 3ca2706e5d Phase 0: forms.py, admin.py und views.py in Domain-Packages aufteilen
- forms.py → forms/ Package (8 Domänen: destinataere, land, finanzen,
  foerderung, dokumente, veranstaltung, system, geschichte)
- admin.py → admin/ Package (7 Domänen, alle 22 @admin.register dekoriert)
- views.py (8845 Zeilen) → views/ Package (10 Domänen: dashboard, destinataere,
  land, paechter, finanzen, foerderung, dokumente, unterstuetzungen,
  veranstaltung, geschichte, system)
- __init__.py in jedem Package re-exportiert alle Symbole für Rückwärtskompatibilität
- urls.py bleibt unverändert (funktioniert durch Re-Exports)
- Django system check: 0 Fehler, alle URL-Auflösungen funktionieren

Keine funktionalen Änderungen – reine Strukturverbesserung für Vision 2026.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 09:55:15 +00:00

711 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# views/geschichte.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 geschichte_list(request):
"""List all published history pages"""
seiten = GeschichteSeite.objects.filter(ist_veroeffentlicht=True).order_by('sortierung', 'titel')
context = {
'seiten': seiten,
'title': 'Geschichte der Stiftung'
}
return render(request, 'stiftung/geschichte/liste.html', context)
@login_required
def geschichte_detail(request, slug):
"""Display a specific history page"""
seite = get_object_or_404(GeschichteSeite, slug=slug, ist_veroeffentlicht=True)
bilder = seite.bilder.all().order_by('sortierung', 'titel')
context = {
'seite': seite,
'bilder': bilder,
'title': seite.titel
}
return render(request, 'stiftung/geschichte/detail.html', context)
@login_required
def geschichte_create(request):
"""Create a new history page"""
if not request.user.has_perm('stiftung.add_geschichteseite'):
messages.error(request, 'Sie haben keine Berechtigung, neue Geschichtsseiten zu erstellen.')
return redirect('stiftung:geschichte_list')
if request.method == 'POST':
form = GeschichteSeiteForm(request.POST)
if form.is_valid():
seite = form.save(commit=False)
seite.erstellt_von = request.user
seite.aktualisiert_von = request.user
seite.save()
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich erstellt.')
return redirect('stiftung:geschichte_detail', slug=seite.slug)
else:
form = GeschichteSeiteForm()
context = {
'form': form,
'title': 'Neue Geschichtsseite'
}
return render(request, 'stiftung/geschichte/form.html', context)
@login_required
def geschichte_edit(request, slug):
"""Edit an existing history page"""
seite = get_object_or_404(GeschichteSeite, slug=slug)
if not request.user.has_perm('stiftung.change_geschichteseite'):
messages.error(request, 'Sie haben keine Berechtigung, diese Geschichtsseite zu bearbeiten.')
return redirect('stiftung:geschichte_detail', slug=slug)
if request.method == 'POST':
form = GeschichteSeiteForm(request.POST, instance=seite)
if form.is_valid():
seite = form.save(commit=False)
seite.aktualisiert_von = request.user
seite.save()
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich aktualisiert.')
return redirect('stiftung:geschichte_detail', slug=seite.slug)
else:
form = GeschichteSeiteForm(instance=seite)
context = {
'form': form,
'seite': seite,
'title': f'Bearbeiten: {seite.titel}'
}
return render(request, 'stiftung/geschichte/form.html', context)
@login_required
def geschichte_bild_upload(request, slug):
"""Upload images to a history page"""
seite = get_object_or_404(GeschichteSeite, slug=slug)
if not request.user.has_perm('stiftung.add_geschichtebild'):
messages.error(request, 'Sie haben keine Berechtigung, Bilder hochzuladen.')
return redirect('stiftung:geschichte_detail', slug=slug)
if request.method == 'POST':
form = GeschichteBildForm(request.POST, request.FILES)
if form.is_valid():
bild = form.save(commit=False)
bild.seite = seite
bild.hochgeladen_von = request.user
bild.save()
messages.success(request, f'Bild "{bild.titel}" wurde erfolgreich hochgeladen.')
return redirect('stiftung:geschichte_detail', slug=slug)
else:
form = GeschichteBildForm()
context = {
'form': form,
'seite': seite,
'title': f'Bild hochladen: {seite.titel}'
}
return render(request, 'stiftung/geschichte/bild_form.html', context)
@login_required
def geschichte_bild_delete(request, slug, bild_id):
"""Delete an image from a history page"""
seite = get_object_or_404(GeschichteSeite, slug=slug)
bild = get_object_or_404(GeschichteBild, id=bild_id, seite=seite)
if not request.user.has_perm('stiftung.delete_geschichtebild'):
messages.error(request, 'Sie haben keine Berechtigung, Bilder zu löschen.')
return redirect('stiftung:geschichte_detail', slug=slug)
if request.method == 'POST':
bild_titel = bild.titel
bild.delete()
messages.success(request, f'Bild "{bild_titel}" wurde erfolgreich gelöscht.')
return redirect('stiftung:geschichte_detail', slug=slug)
context = {
'bild': bild,
'seite': seite,
'title': f'Bild löschen: {bild.titel}'
}
return render(request, 'stiftung/geschichte/bild_delete.html', context)
# Calendar Views
@login_required
def kalender_view(request):
"""Main calendar view with different view types"""
from stiftung.services.calendar_service import StiftungsKalenderService
import calendar as cal
calendar_service = StiftungsKalenderService()
# Get current date and view parameters
today = timezone.now().date()
view_type = request.GET.get('view', 'month') # month, week, list, agenda
year = int(request.GET.get('year', today.year))
month = int(request.GET.get('month', today.month))
# Calculate date ranges based on view type
if view_type == 'month':
# Get events for the entire month
start_date = date(year, month, 1)
_, last_day = cal.monthrange(year, month)
end_date = date(year, month, last_day)
title_suffix = f"{cal.month_name[month]} {year}"
elif view_type == 'week':
# Get current week
week_start = today - timedelta(days=today.weekday())
start_date = week_start
end_date = week_start + timedelta(days=6)
title_suffix = f"Woche vom {start_date.strftime('%d.%m')} - {end_date.strftime('%d.%m.%Y')}"
elif view_type == 'agenda':
# Next 30 days
start_date = today
end_date = today + timedelta(days=30)
title_suffix = "Nächste 30 Tage"
else: # list view
# Next 90 days
start_date = today
end_date = today + timedelta(days=90)
title_suffix = "Liste (nächste 90 Tage)"
# Get events for the date range
events = calendar_service.get_all_events(start_date, end_date)
# Generate calendar grid for month view
calendar_grid = None
if view_type == 'month':
calendar_grid = []
first_day = date(year, month, 1)
month_cal = cal.monthcalendar(year, month)
for week in month_cal:
week_data = []
for day in week:
if day == 0:
week_data.append(None)
else:
day_date = date(year, month, day)
day_events = [e for e in events if e.date == day_date]
week_data.append({
'day': day,
'date': day_date,
'is_today': day_date == today,
'events': day_events[:3], # Show max 3 events per day
'event_count': len(day_events)
})
calendar_grid.append(week_data)
# Navigation dates for month view
if month > 1:
prev_month = month - 1
prev_year = year
else:
prev_month = 12
prev_year = year - 1
if month < 12:
next_month = month + 1
next_year = year
else:
next_month = 1
next_year = year + 1
context = {
'title': f'Kalender - {title_suffix}',
'events': events,
'calendar_grid': calendar_grid,
'view_type': view_type,
'year': year,
'month': month,
'today': today,
'start_date': start_date,
'end_date': end_date,
'prev_year': prev_year,
'prev_month': prev_month,
'next_year': next_year,
'next_month': next_month,
'month_name': cal.month_name[month],
'weekdays': ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
}
# Choose template based on view type
if view_type == 'month':
template = 'stiftung/kalender/month_view.html'
elif view_type == 'week':
template = 'stiftung/kalender/week_view.html'
elif view_type == 'agenda':
template = 'stiftung/kalender/agenda_view.html'
else:
template = 'stiftung/kalender/list_view.html'
return render(request, template, context)
@login_required
def kalender_create(request):
"""Create new calendar event"""
from stiftung.models import StiftungsKalenderEintrag
if request.method == 'POST':
# Simple form handling - you can enhance this with Django forms
titel = request.POST.get('titel')
beschreibung = request.POST.get('beschreibung', '')
datum = request.POST.get('datum')
kategorie = request.POST.get('kategorie', 'termin')
prioritaet = request.POST.get('prioritaet', 'normal')
if titel and datum:
zeit_str = request.POST.get('zeit')
uhrzeit = zeit_str if zeit_str else None
ganztags = not bool(zeit_str)
StiftungsKalenderEintrag.objects.create(
titel=titel,
beschreibung=beschreibung,
datum=datum,
uhrzeit=uhrzeit,
ganztags=ganztags,
kategorie=kategorie,
prioritaet=prioritaet,
erstellt_von=request.user.username
)
messages.success(request, 'Kalendereintrag wurde erfolgreich erstellt.')
return redirect('stiftung:kalender')
else:
messages.error(request, 'Titel und Datum sind erforderlich.')
context = {
'title': 'Neuer Kalendereintrag',
}
return render(request, 'stiftung/kalender/create.html', context)
@login_required
def kalender_detail(request, pk):
"""Calendar event detail view"""
from stiftung.models import StiftungsKalenderEintrag
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
context = {
'title': f'Kalendereintrag: {event.titel}',
'event': event,
}
return render(request, 'stiftung/kalender/detail.html', context)
@login_required
def kalender_edit(request, pk):
"""Edit calendar event"""
from stiftung.models import StiftungsKalenderEintrag
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
if request.method == 'POST':
event.titel = request.POST.get('titel', event.titel)
event.beschreibung = request.POST.get('beschreibung', event.beschreibung)
event.datum = request.POST.get('datum', event.datum)
zeit_str = request.POST.get('zeit')
if zeit_str:
event.uhrzeit = zeit_str
event.ganztags = False
else:
event.uhrzeit = None
event.ganztags = True
event.kategorie = request.POST.get('kategorie', event.kategorie)
event.prioritaet = request.POST.get('prioritaet', event.prioritaet)
event.erledigt = 'erledigt' in request.POST
event.save()
messages.success(request, 'Kalendereintrag wurde aktualisiert.')
return redirect('stiftung:kalender_detail', pk=pk)
context = {
'title': f'Bearbeiten: {event.titel}',
'event': event,
}
return render(request, 'stiftung/kalender/edit.html', context)
@login_required
def kalender_delete(request, pk):
"""Delete calendar event"""
from stiftung.models import StiftungsKalenderEintrag
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
if request.method == 'POST':
event_titel = event.titel
event.delete()
messages.success(request, f'Kalendereintrag "{event_titel}" wurde gelöscht.')
return redirect('stiftung:kalender')
context = {
'title': f'Löschen: {event.titel}',
'event': event,
}
return render(request, 'stiftung/kalender/delete_confirm.html', context)
@login_required
def kalender_admin(request):
"""Calendar administration with event sources and management"""
from stiftung.models import StiftungsKalenderEintrag
from stiftung.services.calendar_service import StiftungsKalenderService
# Get filter parameters
show_custom = request.GET.get('show_custom', 'true') == 'true'
show_payments = request.GET.get('show_payments', 'true') == 'true'
show_leases = request.GET.get('show_leases', 'true') == 'true'
show_birthdays = request.GET.get('show_birthdays', 'true') == 'true'
category_filter = request.GET.get('category', '')
priority_filter = request.GET.get('priority', '')
# Initialize calendar service
calendar_service = StiftungsKalenderService()
# Get events based on filters
from datetime import date, timedelta
start_date = date.today() - timedelta(days=30)
end_date = date.today() + timedelta(days=90)
all_events = []
# Custom calendar entries
if show_custom:
custom_events = calendar_service.get_calendar_events(start_date, end_date)
all_events.extend(custom_events)
# Payment events
if show_payments:
payment_events = calendar_service.get_support_payment_events(start_date, end_date)
all_events.extend(payment_events)
# Lease events
if show_leases:
lease_events = calendar_service.get_lease_events(start_date, end_date)
all_events.extend(lease_events)
# Birthday events
if show_birthdays:
birthday_events = calendar_service.get_birthday_events(start_date, end_date)
all_events.extend(birthday_events)
# Filter by category and priority if specified
if category_filter:
all_events = [e for e in all_events if getattr(e, 'category', '') == category_filter]
if priority_filter:
all_events = [e for e in all_events if getattr(e, 'priority', '') == priority_filter]
# Sort events by date
all_events.sort(key=lambda x: x.date)
# Get statistics
custom_count = StiftungsKalenderEintrag.objects.count()
total_events = len(all_events)
# Event source statistics
stats = {
'custom_events': len([e for e in all_events if getattr(e, 'source', '') == 'custom']),
'payment_events': len([e for e in all_events if getattr(e, 'source', '') == 'payment']),
'lease_events': len([e for e in all_events if getattr(e, 'source', '') == 'lease']),
'birthday_events': len([e for e in all_events if getattr(e, 'source', '') == 'birthday']),
'total_events': total_events,
'custom_count': custom_count,
}
context = {
'title': 'Kalender Administration',
'events': all_events,
'stats': stats,
'show_custom': show_custom,
'show_payments': show_payments,
'show_leases': show_leases,
'show_birthdays': show_birthdays,
'category_filter': category_filter,
'priority_filter': priority_filter,
'categories': StiftungsKalenderEintrag.KATEGORIE_CHOICES,
'priorities': StiftungsKalenderEintrag.PRIORITAET_CHOICES,
}
return render(request, 'stiftung/kalender/admin.html', context)
@login_required
def kalender_api_events(request):
"""API endpoint for calendar events (JSON)"""
from django.http import JsonResponse
from stiftung.services.calendar_service import StiftungsKalenderService
from datetime import datetime
calendar_service = StiftungsKalenderService()
# Get date range from request
start_date = request.GET.get('start')
end_date = request.GET.get('end')
if start_date and end_date:
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
except ValueError:
return JsonResponse({'error': 'Invalid date format'}, status=400)
events = calendar_service.get_all_events(start_date, end_date)
else:
events = calendar_service.get_all_events()
# Convert to FullCalendar format
calendar_events = []
for event in events:
calendar_events.append({
'id': getattr(event, 'id', str(event.title)),
'title': event.title,
'start': event.date.strftime('%Y-%m-%d'),
'description': getattr(event, 'description', ''),
'className': f"event-{event.category}",
'backgroundColor': f"var(--bs-{event.color})",
'borderColor': f"var(--bs-{event.color})",
})
return JsonResponse(calendar_events, safe=False)
# Calendar Views
@login_required
def kalender_view(request):
"""Full calendar view with all events"""
from stiftung.services.calendar_service import StiftungsKalenderService
calendar_service = StiftungsKalenderService()
# Get current month events by default
today = timezone.now().date()
events = calendar_service.get_events_for_month(today.year, today.month)
context = {
'events': events,
'title': 'Stiftungskalender',
'current_month': today.strftime('%B %Y'),
}
return render(request, 'stiftung/kalender/kalender.html', context)
context = {
'title': 'Kalendereintrag löschen'
}
return render(request, 'stiftung/kalender/delete.html', context)
# =============================================================================
# E-Mail-Eingang Destinatäre
# =============================================================================
@login_required
def email_eingang_list(request):
"""
Übersicht aller eingegangenen E-Mails von Destinatären.
Zeigt ungeklärte Absender zuerst, dann chronologisch absteigend.
"""
status_filter = request.GET.get("status", "")
search = request.GET.get("q", "").strip()
qs = DestinataerEmailEingang.objects.select_related("destinataer", "quartalsnachweis")
if status_filter:
qs = qs.filter(status=status_filter)
if search:
qs = qs.filter(
Q(absender_email__icontains=search)
| Q(absender_name__icontains=search)
| Q(betreff__icontains=search)
| Q(destinataer__vorname__icontains=search)
| Q(destinataer__nachname__icontains=search)
)
# Unbekannte Absender zuerst, dann nach Datum absteigend
qs = qs.order_by(
"status", # "unbekannt" kommt alphabetisch vor "verarbeitet" / "zugewiesen"
"-eingangsdatum",
)
paginator = Paginator(qs, 30)
page_obj = paginator.get_page(request.GET.get("page"))
context = {
"title": "E-Mail-Eingang (Destinatäre)",
"page_obj": page_obj,
"status_filter": status_filter,
"search": search,
"status_choices": DestinataerEmailEingang.STATUS_CHOICES,
"counts": {
"gesamt": DestinataerEmailEingang.objects.count(),
"neu": DestinataerEmailEingang.objects.filter(status="neu").count(),
"unbekannt": DestinataerEmailEingang.objects.filter(status="unbekannt").count(),
"fehler": DestinataerEmailEingang.objects.filter(status="fehler").count(),
},
}
return render(request, "stiftung/email_eingang/list.html", context)
@login_required
def email_eingang_detail(request, pk):
"""Detailansicht einer eingegangenen E-Mail mit Möglichkeit zur manuellen Zuordnung."""
eingang = get_object_or_404(DestinataerEmailEingang, pk=pk)
if request.method == "POST":
action = request.POST.get("action")
if action == "assign_destinataer":
dest_id = request.POST.get("destinataer_id")
if dest_id:
try:
destinataer = Destinataer.objects.get(pk=dest_id)
eingang.destinataer = destinataer
eingang.status = "zugewiesen"
eingang.save()
messages.success(
request,
f"E-Mail wurde {destinataer} zugeordnet.",
)
except Destinataer.DoesNotExist:
messages.error(request, "Destinatär nicht gefunden.")
return redirect("email_eingang_detail", pk=pk)
elif action == "mark_verarbeitet":
eingang.status = "verarbeitet"
eingang.notizen = request.POST.get("notizen", eingang.notizen)
eingang.save()
messages.success(request, "E-Mail als verarbeitet markiert.")
return redirect("email_eingang_list")
elif action == "save_notizen":
eingang.notizen = request.POST.get("notizen", "")
eingang.save()
messages.success(request, "Notizen gespeichert.")
return redirect("email_eingang_detail", pk=pk)
# Paperless-Links zusammenstellen
paperless_links = eingang.get_paperless_links()
# DokumentLinks für diese E-Mail (über paperless_dokument_ids)
dokument_links = []
if eingang.paperless_dokument_ids:
dokument_links = DokumentLink.objects.filter(
paperless_document_id__in=eingang.paperless_dokument_ids
)
# Alle aktiven Destinatäre für manuelle Zuordnung
alle_destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
context = {
"title": f"E-Mail-Eingang: {eingang}",
"eingang": eingang,
"paperless_links": paperless_links,
"dokument_links": dokument_links,
"alle_destinataere": alle_destinataere,
}
return render(request, "stiftung/email_eingang/detail.html", context)
@login_required
def email_eingang_poll_trigger(request):
"""Löst den IMAP-Poll-Task manuell aus (für Tests und manuelle Verarbeitung)."""
if request.method == "POST":
from stiftung.tasks import poll_destinataer_emails
try:
task = poll_destinataer_emails.delay()
messages.success(
request,
f"E-Mail-Abruf wurde gestartet (Task-ID: {task.id}). "
"Bitte Seite in ca. 30 Sekunden neu laden.",
)
except Exception as exc:
messages.error(request, f"Fehler beim Starten des Tasks: {exc}")
return redirect("email_eingang_list")
# ============================================================
# Veranstaltungsmodul
# ============================================================