- 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>
461 lines
16 KiB
Python
461 lines
16 KiB
Python
import re
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
|
|
from ..models import Person
|
|
|
|
|
|
class UserCreationForm(forms.Form):
|
|
"""Form für die Erstellung neuer Benutzer"""
|
|
|
|
username = forms.CharField(
|
|
label="Benutzername",
|
|
max_length=150,
|
|
help_text="Eindeutiger Benutzername für die Anmeldung",
|
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
email = forms.EmailField(
|
|
label="E-Mail-Adresse",
|
|
help_text="E-Mail-Adresse des Benutzers",
|
|
widget=forms.EmailInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
first_name = forms.CharField(
|
|
label="Vorname",
|
|
max_length=30,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
last_name = forms.CharField(
|
|
label="Nachname",
|
|
max_length=150,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
password1 = forms.CharField(
|
|
label="Passwort",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Mindestens 8 Zeichen",
|
|
)
|
|
|
|
password2 = forms.CharField(
|
|
label="Passwort bestätigen",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Geben Sie das Passwort zur Bestätigung erneut ein",
|
|
)
|
|
|
|
is_active = forms.BooleanField(
|
|
label="Aktiv",
|
|
required=False,
|
|
initial=True,
|
|
help_text="Benutzer kann sich anmelden",
|
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
)
|
|
|
|
is_staff = forms.BooleanField(
|
|
label="Staff-Status",
|
|
required=False,
|
|
help_text="Benutzer kann auf Django Admin zugreifen",
|
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
)
|
|
|
|
def clean_username(self):
|
|
username = self.cleaned_data["username"]
|
|
from django.contrib.auth.models import User
|
|
|
|
if User.objects.filter(username=username).exists():
|
|
raise forms.ValidationError(
|
|
"Ein Benutzer mit diesem Namen existiert bereits."
|
|
)
|
|
return username
|
|
|
|
def clean_email(self):
|
|
email = self.cleaned_data["email"]
|
|
from django.contrib.auth.models import User
|
|
|
|
if User.objects.filter(email=email).exists():
|
|
raise forms.ValidationError(
|
|
"Ein Benutzer mit dieser E-Mail-Adresse existiert bereits."
|
|
)
|
|
return email
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
password1 = cleaned_data.get("password1")
|
|
password2 = cleaned_data.get("password2")
|
|
|
|
if password1 and password2:
|
|
if password1 != password2:
|
|
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
|
|
if len(password1) < 8:
|
|
raise forms.ValidationError(
|
|
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class UserUpdateForm(forms.ModelForm):
|
|
"""Form für die Bearbeitung bestehender Benutzer"""
|
|
|
|
class Meta:
|
|
from django.contrib.auth.models import User
|
|
|
|
model = User
|
|
fields = [
|
|
"username",
|
|
"email",
|
|
"first_name",
|
|
"last_name",
|
|
"is_active",
|
|
"is_staff",
|
|
]
|
|
widgets = {
|
|
"username": forms.TextInput(attrs={"class": "form-control"}),
|
|
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
|
"first_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"last_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"is_staff": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
}
|
|
labels = {
|
|
"username": "Benutzername",
|
|
"email": "E-Mail-Adresse",
|
|
"first_name": "Vorname",
|
|
"last_name": "Nachname",
|
|
"is_active": "Aktiv",
|
|
"is_staff": "Staff-Status",
|
|
}
|
|
help_texts = {
|
|
"username": "Eindeutiger Benutzername für die Anmeldung",
|
|
"email": "E-Mail-Adresse des Benutzers",
|
|
"is_active": "Benutzer kann sich anmelden",
|
|
"is_staff": "Benutzer kann auf Django Admin zugreifen",
|
|
}
|
|
|
|
|
|
class PasswordChangeForm(forms.Form):
|
|
"""Form für Passwort-Änderungen"""
|
|
|
|
new_password1 = forms.CharField(
|
|
label="Neues Passwort",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Mindestens 8 Zeichen",
|
|
)
|
|
|
|
new_password2 = forms.CharField(
|
|
label="Neues Passwort bestätigen",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Geben Sie das neue Passwort zur Bestätigung erneut ein",
|
|
)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
password1 = cleaned_data.get("new_password1")
|
|
password2 = cleaned_data.get("new_password2")
|
|
|
|
if password1 and password2:
|
|
if password1 != password2:
|
|
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
|
|
if len(password1) < 8:
|
|
raise forms.ValidationError(
|
|
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class UserPermissionForm(forms.Form):
|
|
"""Form für die Zuweisung von Berechtigungen"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop("user", None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
from django.contrib.auth.models import Permission
|
|
|
|
# Get all custom permissions for stiftung app
|
|
app_permissions = Permission.objects.filter(
|
|
content_type__app_label="stiftung"
|
|
).order_by("name")
|
|
|
|
# Create checkbox fields for each permission
|
|
for perm in app_permissions:
|
|
field_name = f"perm_{perm.id}"
|
|
self.fields[field_name] = forms.BooleanField(
|
|
label=perm.name,
|
|
required=False,
|
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
)
|
|
|
|
# Set initial values if user is provided
|
|
if user:
|
|
self.fields[field_name].initial = user.has_perm(
|
|
f"stiftung.{perm.codename}"
|
|
)
|
|
|
|
def get_permission_groups(self):
|
|
"""Group permissions by functionality for template rendering"""
|
|
from django.contrib.auth.models import Permission
|
|
|
|
groups = {
|
|
"entities": {
|
|
"name": "Entitäten verwalten",
|
|
"permissions": [],
|
|
"icon": "fas fa-users",
|
|
},
|
|
"documents": {
|
|
"name": "Dokumentenverwaltung",
|
|
"permissions": [],
|
|
"icon": "fas fa-folder-open",
|
|
},
|
|
"financial": {
|
|
"name": "Finanzverwaltung",
|
|
"permissions": [],
|
|
"icon": "fas fa-euro-sign",
|
|
},
|
|
"administration": {
|
|
"name": "Administration",
|
|
"permissions": [],
|
|
"icon": "fas fa-cogs",
|
|
},
|
|
"system": {"name": "System", "permissions": [], "icon": "fas fa-server"},
|
|
}
|
|
|
|
# Get all permissions to properly categorize them
|
|
for field_name, field in self.fields.items():
|
|
if field_name.startswith("perm_"):
|
|
# Extract permission ID from field name
|
|
perm_id = field_name.replace("perm_", "")
|
|
try:
|
|
permission = Permission.objects.get(id=perm_id)
|
|
label = permission.name.lower()
|
|
codename = permission.codename.lower()
|
|
|
|
# Get bound field for proper template rendering
|
|
bound_field = self[field_name]
|
|
|
|
# More precise categorization based on both name and codename
|
|
if (
|
|
any(
|
|
word in codename
|
|
for word in [
|
|
"destinataer",
|
|
"land",
|
|
"paechter",
|
|
"verpachtung",
|
|
"foerderung",
|
|
]
|
|
)
|
|
and "manage_" in codename
|
|
or "view_" in codename
|
|
):
|
|
groups["entities"]["permissions"].append(
|
|
(field_name, bound_field, permission)
|
|
)
|
|
elif (
|
|
any(
|
|
word in codename for word in ["documents", "link_documents"]
|
|
)
|
|
or "dokument" in label
|
|
):
|
|
groups["documents"]["permissions"].append(
|
|
(field_name, bound_field, permission)
|
|
)
|
|
elif any(
|
|
word in codename
|
|
for word in [
|
|
"verwaltungskosten",
|
|
"konten",
|
|
"rentmeister",
|
|
"approve_payments",
|
|
]
|
|
) or any(
|
|
word in label
|
|
for word in [
|
|
"verwaltungskosten",
|
|
"konto",
|
|
"rentmeister",
|
|
"zahlung",
|
|
]
|
|
):
|
|
groups["financial"]["permissions"].append(
|
|
(field_name, bound_field, permission)
|
|
)
|
|
elif any(
|
|
word in codename
|
|
for word in [
|
|
"administration",
|
|
"audit",
|
|
"backup",
|
|
"manage_users",
|
|
"manage_permissions",
|
|
]
|
|
) or any(
|
|
word in label
|
|
for word in [
|
|
"administration",
|
|
"audit",
|
|
"backup",
|
|
"benutzer",
|
|
"berechtigung",
|
|
]
|
|
):
|
|
groups["administration"]["permissions"].append(
|
|
(field_name, bound_field, permission)
|
|
)
|
|
else:
|
|
groups["system"]["permissions"].append(
|
|
(field_name, bound_field, permission)
|
|
)
|
|
except Permission.DoesNotExist:
|
|
# Create a fallback permission-like object with proper display
|
|
class FallbackPermission:
|
|
def __init__(self, field_name):
|
|
self.name = field_name.replace('_', ' ').title()
|
|
self.codename = field_name
|
|
|
|
fallback_perm = FallbackPermission(field_name)
|
|
bound_field = self[field_name] # Get bound field for exception case too
|
|
groups["system"]["permissions"].append((field_name, bound_field, fallback_perm))
|
|
|
|
return groups
|
|
|
|
|
|
class TwoFactorSetupForm(forms.Form):
|
|
"""Form for setting up 2FA with TOTP verification"""
|
|
token = forms.CharField(
|
|
max_length=6,
|
|
min_length=6,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control text-center',
|
|
'placeholder': '000000',
|
|
'autocomplete': 'off',
|
|
'pattern': '[0-9]{6}',
|
|
'inputmode': 'numeric'
|
|
}),
|
|
label='Bestätigungscode',
|
|
help_text='6-stelliger Code aus Ihrer Authenticator-App'
|
|
)
|
|
|
|
def clean_token(self):
|
|
token = self.cleaned_data.get('token')
|
|
if token and not token.isdigit():
|
|
raise ValidationError('Der Code darf nur Zahlen enthalten.')
|
|
return token
|
|
|
|
|
|
class TwoFactorVerifyForm(forms.Form):
|
|
"""Form for verifying 2FA during login"""
|
|
otp_token = forms.CharField(
|
|
max_length=8,
|
|
min_length=6,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control form-control-lg text-center',
|
|
'placeholder': '000000',
|
|
'autocomplete': 'off',
|
|
'autofocus': True
|
|
}),
|
|
label='Authentifizierungscode',
|
|
help_text='6-stelliger Code aus der App oder 8-stelliger Backup-Code'
|
|
)
|
|
|
|
def clean_otp_token(self):
|
|
token = self.cleaned_data.get('otp_token')
|
|
if token:
|
|
token = token.strip().lower()
|
|
# Allow 6-digit TOTP codes or 8-character backup codes
|
|
if len(token) == 6 and token.isdigit():
|
|
return token
|
|
elif len(token) == 8 and re.match(r'^[a-f0-9]{8}$', token):
|
|
return token
|
|
else:
|
|
raise ValidationError(
|
|
'Bitte geben Sie einen 6-stelligen Authenticator-Code oder 8-stelligen Backup-Code ein.'
|
|
)
|
|
return token
|
|
|
|
|
|
class TwoFactorDisableForm(forms.Form):
|
|
"""Form for disabling 2FA with password confirmation"""
|
|
password = forms.CharField(
|
|
widget=forms.PasswordInput(attrs={
|
|
'class': 'form-control',
|
|
'autocomplete': 'current-password',
|
|
'autofocus': True
|
|
}),
|
|
label='Passwort',
|
|
help_text='Geben Sie Ihr aktuelles Passwort zur Bestätigung ein'
|
|
)
|
|
|
|
|
|
class BackupTokenRegenerateForm(forms.Form):
|
|
"""Form for regenerating backup tokens"""
|
|
password = forms.CharField(
|
|
widget=forms.PasswordInput(attrs={
|
|
'class': 'form-control',
|
|
'autocomplete': 'current-password'
|
|
}),
|
|
label='Passwort',
|
|
help_text='Geben Sie Ihr Passwort ein, um neue Backup-Codes zu generieren'
|
|
)
|
|
|
|
|
|
class PersonForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Personen (Legacy)"""
|
|
|
|
class Meta:
|
|
model = Person
|
|
fields = [
|
|
"familienzweig",
|
|
"vorname",
|
|
"nachname",
|
|
"geburtsdatum",
|
|
"email",
|
|
"telefon",
|
|
"iban",
|
|
"adresse",
|
|
"notizen",
|
|
"aktiv",
|
|
]
|
|
|
|
widgets = {
|
|
"familienzweig": forms.Select(attrs={"class": "form-select"}),
|
|
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"geburtsdatum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
|
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
|
"iban": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
|
|
),
|
|
"adresse": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
}
|
|
|
|
labels = {
|
|
"familienzweig": "Familienzweig",
|
|
"vorname": "Vorname *",
|
|
"nachname": "Nachname *",
|
|
"geburtsdatum": "Geburtsdatum",
|
|
"email": "E-Mail",
|
|
"telefon": "Telefon",
|
|
"iban": "IBAN",
|
|
"adresse": "Adresse",
|
|
"notizen": "Notizen",
|
|
"aktiv": "Aktiv",
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Markiere Pflichtfelder
|
|
self.fields["vorname"].required = True
|
|
self.fields["nachname"].required = True
|