- 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>
191 lines
8.4 KiB
Python
191 lines
8.4 KiB
Python
from django import forms
|
||
from django.contrib import admin
|
||
from django.utils.html import format_html
|
||
|
||
from ..models import BriefVorlage, Veranstaltung, Veranstaltungsteilnehmer
|
||
|
||
|
||
class VeranstaltungsteilnehmerInline(admin.TabularInline):
|
||
model = Veranstaltungsteilnehmer
|
||
extra = 1
|
||
fields = [
|
||
"anrede", "vorname", "nachname", "strasse", "plz", "ort",
|
||
"email", "rsvp_status", "bemerkungen",
|
||
]
|
||
|
||
|
||
class BriefVorlageWidget(forms.Textarea):
|
||
"""Erweitertes Textarea-Widget für HTML-Briefvorlagen mit Editor-Panel und Platzhalter-Hilfe."""
|
||
|
||
class Media:
|
||
js = ["stiftung/js/briefvorlage_editor.js"]
|
||
|
||
def __init__(self, attrs=None):
|
||
default_attrs = {"rows": 18, "cols": 80, "class": "briefvorlage-textarea", "style": "font-family: monospace; font-size: 13px;"}
|
||
if attrs:
|
||
default_attrs.update(attrs)
|
||
super().__init__(attrs=default_attrs)
|
||
|
||
|
||
class VeranstaltungAdminForm(forms.ModelForm):
|
||
class Meta:
|
||
model = Veranstaltung
|
||
fields = "__all__"
|
||
widgets = {
|
||
"briefvorlage": BriefVorlageWidget(),
|
||
}
|
||
|
||
|
||
@admin.register(Veranstaltung)
|
||
class VeranstaltungAdmin(admin.ModelAdmin):
|
||
form = VeranstaltungAdminForm
|
||
list_display = [
|
||
"titel", "datum", "uhrzeit", "ort", "status",
|
||
"get_teilnehmer_count", "get_zugesagte_count", "budget_pro_person",
|
||
]
|
||
list_filter = ["status", "datum"]
|
||
search_fields = ["titel", "ort", "beschreibung"]
|
||
ordering = ["-datum"]
|
||
readonly_fields = ["id", "erstellt_am", "aktualisiert_am", "serienbrief_aktionen", "platzhalter_dokumentation"]
|
||
inlines = [VeranstaltungsteilnehmerInline]
|
||
|
||
fieldsets = (
|
||
("Grunddaten", {"fields": ("titel", "datum", "uhrzeit", "status")}),
|
||
("Veranstaltungsort", {"fields": ("ort", "adresse")}),
|
||
("Details", {"fields": ("beschreibung", "budget_pro_person")}),
|
||
(
|
||
"Serienbrief – Vorlage",
|
||
{
|
||
"fields": (
|
||
"platzhalter_dokumentation",
|
||
"betreff",
|
||
"briefvorlage",
|
||
),
|
||
},
|
||
),
|
||
(
|
||
"Serienbrief – Unterschriften & Aktionen",
|
||
{
|
||
"fields": (
|
||
"unterschrift_1_name", "unterschrift_1_titel",
|
||
"unterschrift_2_name", "unterschrift_2_titel",
|
||
"serienbrief_aktionen",
|
||
),
|
||
},
|
||
),
|
||
("System", {"fields": ("id", "erstellt_am", "aktualisiert_am"), "classes": ("collapse",)}),
|
||
)
|
||
|
||
def get_teilnehmer_count(self, obj):
|
||
return obj.get_teilnehmer_count()
|
||
get_teilnehmer_count.short_description = "Teilnehmer gesamt"
|
||
|
||
def get_zugesagte_count(self, obj):
|
||
return obj.get_zugesagte_count()
|
||
get_zugesagte_count.short_description = "Zugesagt"
|
||
|
||
def platzhalter_dokumentation(self, obj):
|
||
return format_html(
|
||
"""<div class="help" style="background:#f8f9fa;border:1px solid #dee2e6;border-radius:4px;padding:10px 14px;margin-bottom:4px;">
|
||
<strong>Verfügbare Platzhalter im Brieftext:</strong><br>
|
||
<table style="margin-top:6px;border-collapse:collapse;font-size:13px;">
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ anrede }}}}</td><td>Anredetitel (Herr / Frau)</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ vorname }}}}</td><td>Vorname des Empfängers</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ nachname }}}}</td><td>Nachname des Empfängers</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ strasse }}}}</td><td>Straße und Hausnummer</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ plz }}}}</td><td>Postleitzahl</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ ort }}}}</td><td>Wohnort des Empfängers</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ datum }}}}</td><td>Datum der Veranstaltung (z.B. Freitag, 17. April 2026)</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ uhrzeit }}}}</td><td>Uhrzeit der Veranstaltung (z.B. 19:00 Uhr)</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ veranstaltungsort }}}}</td><td>Name des Veranstaltungsorts / Gasthaus</td></tr>
|
||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ gasthaus_adresse }}}}</td><td>Adresse des Gasthauses</td></tr>
|
||
</table>
|
||
<div style="margin-top:8px;font-size:12px;color:#6c757d;">
|
||
Platzhalter werden beim PDF-Export automatisch mit den Empfänger- und Veranstaltungsdaten befüllt.
|
||
Tipp: Vorlagen unter <a href="/admin/stiftung/briefvorlage/" target="_blank">Verwaltung → Briefvorlagen</a> speichern und wiederverwenden.
|
||
</div>
|
||
</div>"""
|
||
)
|
||
platzhalter_dokumentation.short_description = "Platzhalter-Dokumentation"
|
||
platzhalter_dokumentation.allow_tags = True
|
||
|
||
def serienbrief_aktionen(self, obj):
|
||
if obj.pk:
|
||
from django.urls import reverse as url_reverse
|
||
pdf_url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[obj.pk])
|
||
vorschau_url = url_reverse("stiftung:veranstaltung_serienbrief_vorschau", args=[obj.pk])
|
||
return format_html(
|
||
'<a href="{}" target="_blank" class="button" style="margin-right:8px;">Serienbrief-PDF generieren</a>'
|
||
'<a href="{}" target="_blank" class="button default">Vorschau im Browser</a>',
|
||
pdf_url, vorschau_url,
|
||
)
|
||
return "–"
|
||
serienbrief_aktionen.short_description = "Aktionen"
|
||
|
||
actions = ["generate_serienbrief"]
|
||
|
||
def generate_serienbrief(self, request, queryset):
|
||
if queryset.count() != 1:
|
||
self.message_user(
|
||
request,
|
||
"Bitte genau eine Veranstaltung auswählen.",
|
||
level="error",
|
||
)
|
||
return
|
||
from django.urls import reverse as url_reverse
|
||
from django.shortcuts import redirect
|
||
veranstaltung = queryset.first()
|
||
url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[veranstaltung.pk])
|
||
return redirect(url)
|
||
generate_serienbrief.short_description = "Serienbrief-PDF generieren"
|
||
|
||
|
||
@admin.register(BriefVorlage)
|
||
class BriefVorlageAdmin(admin.ModelAdmin):
|
||
list_display = ["name", "beschreibung_kurz", "erstellt_am", "aktualisiert_am"]
|
||
search_fields = ["name", "beschreibung"]
|
||
ordering = ["name"]
|
||
readonly_fields = ["erstellt_am", "aktualisiert_am"]
|
||
|
||
fieldsets = (
|
||
(None, {"fields": ("name", "beschreibung")}),
|
||
(
|
||
"Briefinhalt",
|
||
{
|
||
"fields": ("betreff", "briefvorlage"),
|
||
"description": (
|
||
"Verfügbare Platzhalter: {{ anrede }}, {{ vorname }}, {{ nachname }}, "
|
||
"{{ strasse }}, {{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
|
||
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||
),
|
||
},
|
||
),
|
||
("System", {"fields": ("erstellt_am", "aktualisiert_am"), "classes": ("collapse",)}),
|
||
)
|
||
|
||
def beschreibung_kurz(self, obj):
|
||
return obj.beschreibung[:80] + "…" if len(obj.beschreibung) > 80 else obj.beschreibung
|
||
beschreibung_kurz.short_description = "Beschreibung"
|
||
|
||
|
||
@admin.register(Veranstaltungsteilnehmer)
|
||
class VeranstaltungsteilnehmerAdmin(admin.ModelAdmin):
|
||
list_display = [
|
||
"nachname", "vorname", "anrede", "veranstaltung", "rsvp_status", "ort",
|
||
]
|
||
list_filter = ["rsvp_status", "veranstaltung", "anrede"]
|
||
search_fields = ["vorname", "nachname", "ort", "email"]
|
||
ordering = ["veranstaltung", "nachname", "vorname"]
|
||
readonly_fields = ["id", "erstellt_am"]
|
||
|
||
fieldsets = (
|
||
("Zuordnung", {"fields": ("veranstaltung", "paechter", "destinataer")}),
|
||
(
|
||
"Persönliche Daten",
|
||
{"fields": ("anrede", "vorname", "nachname", "email")},
|
||
),
|
||
("Adresse", {"fields": ("strasse", "plz", "ort")}),
|
||
("RSVP", {"fields": ("rsvp_status", "bemerkungen")}),
|
||
("System", {"fields": ("id", "erstellt_am"), "classes": ("collapse",)}),
|
||
)
|