# 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("stiftung: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("stiftung:email_eingang_list") elif action == "save_notizen": eingang.notizen = request.POST.get("notizen", "") eingang.save() messages.success(request, "Notizen gespeichert.") return redirect("stiftung: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("stiftung:email_eingang_list") # ============================================================ # Veranstaltungsmodul # ============================================================