620 lines
28 KiB
Python
620 lines
28 KiB
Python
from django import forms
|
|
from pathlib import Path
|
|
from datetime import timedelta
|
|
from django.contrib.auth import get_user_model, password_validation
|
|
from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm
|
|
from django.utils import timezone
|
|
from django.utils.translation import get_language, gettext as _, gettext_lazy
|
|
|
|
from .branding import get_company_email_domain
|
|
from .form_builder import apply_form_field_config
|
|
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, WorkflowConfig
|
|
from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role
|
|
|
|
|
|
YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')]
|
|
EMPLOYMENT_CHOICES = [('', 'Wählen Sie eine'), ('befristet', 'befristet'), ('unbefristet', 'unbefristet')]
|
|
GENDER_CHOICES = [('', 'Wählen Sie eine'), ('herr', 'Herr'), ('frau', 'Frau'), ('divers', 'Divers')]
|
|
DEPARTMENT_CHOICES = [
|
|
('Buchhaltung/Personalverwaltung', 'Buchhaltung/Personalverwaltung'),
|
|
('IT-Service', 'IT-Service'),
|
|
('Azubi', 'Azubi'),
|
|
('Marketing/Kommunikation', 'Marketing/Kommunikation'),
|
|
('Messe', 'Messe'),
|
|
('Kongresse', 'Kongresse'),
|
|
('TNB/Studiengänge', 'TNB/Studiengänge'),
|
|
('TUB-Academy', 'TUB-Academy'),
|
|
('Projekte TUB', 'Projekte TUB'),
|
|
('TU Berlin Summer + Winter School', 'TU Berlin Summer + Winter School'),
|
|
]
|
|
|
|
DEVICE_CHOICES = [
|
|
('Laptop', 'Laptop'),
|
|
('Docking-Station', 'Docking-Station'),
|
|
('Tastatur und Maus', 'Tastatur und Maus'),
|
|
('Kopfhörer', 'Kopfhörer'),
|
|
('Tragetasche', 'Tragetasche'),
|
|
('Monitor', 'Monitor'),
|
|
('Schlüssel', 'Schlüssel'),
|
|
('Tischtelefon', 'Tischtelefon'),
|
|
]
|
|
|
|
SOFTWARE_CHOICES = [
|
|
('eM Client', 'eM Client'),
|
|
('KeepassXC', 'KeepassXC'),
|
|
('Nextcloud', 'Nextcloud'),
|
|
('7-Zip', '7-Zip'),
|
|
('PDF Reader', 'PDF Reader'),
|
|
('PDF-Editor (Flexi PDF)', 'PDF-Editor (Flexi PDF)'),
|
|
('Firefox', 'Firefox'),
|
|
('Chrome', 'Chrome'),
|
|
('Backup Client', 'Backup Client'),
|
|
('MS Office', 'MS Office'),
|
|
('Zoom', 'Zoom'),
|
|
('Cisco VPN Client', 'Cisco VPN Client'),
|
|
]
|
|
|
|
ACCESS_CHOICES = [
|
|
('TU Konto', 'TU Konto'),
|
|
('HR Works', 'HR Works'),
|
|
('Datev', 'Datev'),
|
|
('Odoo', 'Odoo'),
|
|
]
|
|
|
|
WORKSPACE_GROUP_CHOICES = [
|
|
('Group-Academy', 'Group-Academy'),
|
|
('Group-BuSu', 'Group-BuSu'),
|
|
('Group-EIT-Urban-Mobility', 'Group-EIT-Urban-Mobility'),
|
|
('Group-EL', 'Group-EL'),
|
|
('Group-EM', 'Group-EM'),
|
|
('Group-IT-Services', 'Group-IT-Services'),
|
|
('Group-Kongresse-Events', 'Group-Kongresse-Events'),
|
|
('Group-MaCo', 'Group-MaCo'),
|
|
('Group-Messe', 'Group-Messe'),
|
|
('Group-Messe-Kongresse', 'Group-Messe-Kongresse'),
|
|
('Group-MSE', 'Group-MSE'),
|
|
('Group-SuMo', 'Group-SuMo'),
|
|
('Group-TNB', 'Group-TNB'),
|
|
('Group-TUBS', 'Group-TUBS'),
|
|
('Group-Wima', 'Group-Wima'),
|
|
('Group-Leitungsrunde', 'Group-Leitungsrunde'),
|
|
('Campus-Euref', 'Campus-Euref'),
|
|
('Group-Lohnbuchhaltung', 'Group-Lohnbuchhaltung'),
|
|
('Group-SU-WU', 'Group-SU-WU'),
|
|
('NWM', 'NWM'),
|
|
]
|
|
|
|
RESOURCE_CHOICES = [('Drucker HBS 5./6. OG', 'Drucker HBS 5./6. OG'), ('Drucker Euref', 'Drucker Euref')]
|
|
PHONE_CHOICES = [
|
|
('030 4472021 (0-9)', '030 4472021 (0-9)'),
|
|
('030 4472022 (0-9)', '030 4472022 (0-9)'),
|
|
('030 4472023 (0-9)', '030 4472023 (0-9)'),
|
|
('030 4472024 (0-9)', '030 4472024 (0-9)'),
|
|
('030 4472025 (0-9)', '030 4472025 (0-9)'),
|
|
('030 4472026 (0-9)', '030 4472026 (0-9)'),
|
|
('030 4472027 (0-9)', '030 4472027 (0-9)'),
|
|
('030 4472028 (0-9)', '030 4472028 (0-9)'),
|
|
]
|
|
|
|
HARDWARE_EXTRA_CHOICES = [('Smartphone', 'Smartphone'), ('Anderes', 'Anderes')]
|
|
SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)', 'Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'), ('Anderes', 'Anderes')]
|
|
|
|
|
|
class AppAuthenticationForm(AuthenticationForm):
|
|
username = forms.CharField(label=gettext_lazy('Benutzername'))
|
|
password = forms.CharField(label=gettext_lazy('Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}))
|
|
|
|
|
|
class AppPasswordResetForm(PasswordResetForm):
|
|
email = forms.EmailField(label=gettext_lazy('E-Mail-Adresse'))
|
|
|
|
|
|
class AppSetPasswordForm(SetPasswordForm):
|
|
new_password1 = forms.CharField(
|
|
label=gettext_lazy('Neues Passwort'),
|
|
strip=False,
|
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
|
help_text=password_validation.password_validators_help_text_html(),
|
|
)
|
|
new_password2 = forms.CharField(
|
|
label=gettext_lazy('Neues Passwort bestätigen'),
|
|
strip=False,
|
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
|
)
|
|
|
|
|
|
class UserManagementCreateForm(forms.Form):
|
|
first_name = forms.CharField(label=_('Vorname'), max_length=150, required=False)
|
|
last_name = forms.CharField(label=_('Nachname'), max_length=150, required=False)
|
|
username = forms.CharField(label=_('Benutzername'), max_length=150)
|
|
email = forms.EmailField(label=_('E-Mail-Adresse'))
|
|
role_key = forms.ChoiceField(label=_('Rolle'))
|
|
|
|
def __init__(self, *args, include_product_owner: bool = False, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.include_product_owner = include_product_owner
|
|
role_order = [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF]
|
|
if include_product_owner:
|
|
role_order = [ROLE_PLATFORM_OWNER] + role_order
|
|
self.fields['role_key'].choices = [(role_key, str(ROLE_LABELS[role_key])) for role_key in role_order]
|
|
|
|
def clean_username(self):
|
|
username = (self.cleaned_data.get('username') or '').strip()
|
|
user_model = get_user_model()
|
|
if user_model.objects.filter(username=username).exists():
|
|
raise forms.ValidationError(_('Dieser Benutzername ist bereits vergeben.'))
|
|
return username
|
|
|
|
def clean_email(self):
|
|
return (self.cleaned_data.get('email') or '').strip().lower()
|
|
|
|
def clean_role_key(self):
|
|
role_key = (self.cleaned_data.get('role_key') or '').strip()
|
|
if role_key not in ROLE_GROUP_NAMES:
|
|
raise forms.ValidationError(_('Ungültige Rolle.'))
|
|
if role_key == ROLE_PLATFORM_OWNER and not self.include_product_owner:
|
|
raise forms.ValidationError(_('Nur Platform Owner dürfen diese Rolle vergeben.'))
|
|
return role_key
|
|
|
|
def save(self):
|
|
user_model = get_user_model()
|
|
user = user_model.objects.create_user(
|
|
username=self.cleaned_data['username'],
|
|
email=self.cleaned_data['email'],
|
|
password=None,
|
|
first_name=self.cleaned_data.get('first_name', ''),
|
|
last_name=self.cleaned_data.get('last_name', ''),
|
|
is_active=True,
|
|
)
|
|
assign_user_role(user, self.cleaned_data['role_key'])
|
|
return user
|
|
|
|
|
|
class PortalBrandingForm(forms.ModelForm):
|
|
class Meta:
|
|
model = PortalBranding
|
|
fields = [
|
|
'portal_title',
|
|
'company_name',
|
|
'company_domain',
|
|
'support_email',
|
|
'sender_display_name',
|
|
'login_subtitle',
|
|
'footer_text',
|
|
'footer_text_en',
|
|
'legal_notice',
|
|
'legal_notice_en',
|
|
'default_language',
|
|
'logo_image',
|
|
'pdf_letterhead',
|
|
'favicon_image',
|
|
'primary_color',
|
|
'secondary_color',
|
|
]
|
|
labels = {
|
|
'portal_title': gettext_lazy('Portal-Titel'),
|
|
'company_name': gettext_lazy('Firmenname'),
|
|
'company_domain': gettext_lazy('Firmen-Domain'),
|
|
'support_email': gettext_lazy('Support-E-Mail'),
|
|
'sender_display_name': gettext_lazy('Absender-Anzeigename'),
|
|
'login_subtitle': gettext_lazy('Login-Untertitel'),
|
|
'footer_text': gettext_lazy('Footer-Text DE'),
|
|
'footer_text_en': gettext_lazy('Footer-Text EN'),
|
|
'legal_notice': gettext_lazy('Rechtlicher Hinweis DE'),
|
|
'legal_notice_en': gettext_lazy('Rechtlicher Hinweis EN'),
|
|
'default_language': gettext_lazy('Standardsprache'),
|
|
'logo_image': gettext_lazy('Logo'),
|
|
'pdf_letterhead': gettext_lazy('PDF-Briefkopf'),
|
|
'favicon_image': gettext_lazy('Favicon'),
|
|
'primary_color': gettext_lazy('Primärfarbe'),
|
|
'secondary_color': gettext_lazy('Sekundärfarbe'),
|
|
}
|
|
widgets = {
|
|
'primary_color': forms.TextInput(attrs={'type': 'color'}),
|
|
'secondary_color': forms.TextInput(attrs={'type': 'color'}),
|
|
'logo_image': forms.ClearableFileInput(attrs={'accept': '.svg,.png,.jpg,.jpeg,.webp'}),
|
|
'pdf_letterhead': forms.ClearableFileInput(attrs={'accept': '.pdf'}),
|
|
'favicon_image': forms.ClearableFileInput(attrs={'accept': '.ico,.png,.svg,.webp'}),
|
|
'legal_notice': forms.Textarea(attrs={'rows': 3}),
|
|
'legal_notice_en': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def clean_logo_image(self):
|
|
logo = self.cleaned_data.get('logo_image')
|
|
if not logo:
|
|
return logo
|
|
if getattr(logo, 'size', 0) > 5 * 1024 * 1024:
|
|
raise forms.ValidationError(_('Das Logo darf maximal 5 MB groß sein.'))
|
|
return logo
|
|
|
|
def clean_pdf_letterhead(self):
|
|
letterhead = self.cleaned_data.get('pdf_letterhead')
|
|
if not letterhead:
|
|
return letterhead
|
|
if getattr(letterhead, 'size', 0) > 10 * 1024 * 1024:
|
|
raise forms.ValidationError(_('Der PDF-Briefkopf darf maximal 10 MB groß sein.'))
|
|
return letterhead
|
|
|
|
def clean_favicon_image(self):
|
|
favicon = self.cleaned_data.get('favicon_image')
|
|
if not favicon:
|
|
return favicon
|
|
if getattr(favicon, 'size', 0) > 2 * 1024 * 1024:
|
|
raise forms.ValidationError(_('Das Favicon darf maximal 2 MB groß sein.'))
|
|
return favicon
|
|
|
|
|
|
class PortalCompanyConfigForm(forms.ModelForm):
|
|
class Meta:
|
|
model = PortalCompanyConfig
|
|
fields = [
|
|
'legal_company_name',
|
|
'street_address',
|
|
'postal_code',
|
|
'city',
|
|
'country',
|
|
'website_url',
|
|
'imprint_url',
|
|
'privacy_url',
|
|
'hr_contact_email',
|
|
'it_contact_email',
|
|
'operations_contact_email',
|
|
'phone_number',
|
|
'vat_id',
|
|
'registration_number',
|
|
]
|
|
labels = {
|
|
'legal_company_name': gettext_lazy('Rechtlicher Firmenname'),
|
|
'street_address': gettext_lazy('Straße und Hausnummer'),
|
|
'postal_code': gettext_lazy('Postleitzahl'),
|
|
'city': gettext_lazy('Stadt'),
|
|
'country': gettext_lazy('Land'),
|
|
'website_url': gettext_lazy('Website'),
|
|
'imprint_url': gettext_lazy('Impressum-URL'),
|
|
'privacy_url': gettext_lazy('Datenschutz-URL'),
|
|
'hr_contact_email': gettext_lazy('HR-Kontakt'),
|
|
'it_contact_email': gettext_lazy('IT-Kontakt'),
|
|
'operations_contact_email': gettext_lazy('Operations-Kontakt'),
|
|
'phone_number': gettext_lazy('Zentrale Telefonnummer'),
|
|
'vat_id': gettext_lazy('USt-IdNr.'),
|
|
'registration_number': gettext_lazy('Register- oder Handelsnummer'),
|
|
}
|
|
|
|
|
|
class OnboardingRequestForm(forms.ModelForm):
|
|
first_name = forms.CharField(label='Vorname', required=False)
|
|
last_name = forms.CharField(label='Nachname', required=False)
|
|
full_name = forms.CharField(required=False, widget=forms.HiddenInput())
|
|
gender = forms.ChoiceField(label='Anrede', choices=GENDER_CHOICES, required=True)
|
|
department = forms.ChoiceField(label='Abteilung', choices=DEPARTMENT_CHOICES, required=True)
|
|
work_email = forms.EmailField(
|
|
label='Gewünschte dienstliche E-Mail-Adresse',
|
|
help_text='Bitte nutzen Sie das Format name@tub.co.',
|
|
)
|
|
contract_start = forms.DateField(label='Vertragsbeginn', widget=forms.DateInput(attrs={'type': 'date'}))
|
|
employment_type = forms.ChoiceField(label='Beschäftigungsverhältnis', choices=EMPLOYMENT_CHOICES, required=True)
|
|
employment_end_date = forms.DateField(
|
|
label='Enddatum (nur bei befristet)',
|
|
required=False,
|
|
widget=forms.DateInput(attrs={'type': 'date'}),
|
|
)
|
|
handover_date = forms.DateField(label='Gewünschtes Übergabedatum der Geräte', required=False, widget=forms.DateInput(attrs={'type': 'date'}))
|
|
|
|
group_mailboxes_required_choice = forms.ChoiceField(label='Gruppenpostfächer erforderlich?', choices=YES_NO_CHOICES, required=False)
|
|
additional_software_needed_choice = forms.ChoiceField(label='Wird zusätzliche Software benötigt?', choices=YES_NO_CHOICES, required=False)
|
|
additional_hardware_needed_choice = forms.ChoiceField(label='Darüber hinaus wird weitere Hardware benötigt?', choices=YES_NO_CHOICES, required=False)
|
|
additional_access_needed_choice = forms.ChoiceField(label='Darüber hinaus werden weitere Zugänge benötigt?', choices=YES_NO_CHOICES, required=False)
|
|
successor_required_choice = forms.ChoiceField(label='Neue Mitarbeitende ist Nachfolge von?', choices=YES_NO_CHOICES, required=False)
|
|
inherit_phone_number_choice = forms.ChoiceField(label='Telefonnummer von Vorgängerperson übernehmen?', choices=YES_NO_CHOICES, required=False)
|
|
|
|
order_business_cards = forms.BooleanField(label='Visitenkarten bestellen?', required=False)
|
|
|
|
needed_devices_multi = forms.MultipleChoiceField(
|
|
label='Benötigte Geräte und Gegenstände',
|
|
choices=DEVICE_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
needed_software_multi = forms.MultipleChoiceField(
|
|
label='Benötigte Software',
|
|
choices=SOFTWARE_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
needed_accesses_multi = forms.MultipleChoiceField(
|
|
label='Benötigte Zugänge',
|
|
choices=ACCESS_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
needed_workspace_groups_multi = forms.MultipleChoiceField(
|
|
label='Benötigte Gruppen im Workspace',
|
|
choices=WORKSPACE_GROUP_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
needed_resources_multi = forms.MultipleChoiceField(
|
|
label='Benötigte Ressourcen',
|
|
choices=RESOURCE_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
phone_number_choice = forms.CharField(
|
|
label='TUB/CO-Telefon-Direktwahl-Nr. 030 447202 (10-89)',
|
|
required=False,
|
|
widget=forms.TextInput(attrs={'placeholder': 'z. B. 030 44720212'}),
|
|
)
|
|
additional_hardware_multi = forms.MultipleChoiceField(
|
|
label='Zusätzliche Hardware',
|
|
choices=HARDWARE_EXTRA_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
additional_software_multi = forms.MultipleChoiceField(
|
|
label='Zusätzlich gewünschte Software',
|
|
choices=SOFTWARE_EXTRA_CHOICES,
|
|
required=False,
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
|
|
agreement_confirm = forms.BooleanField(
|
|
label='Hiermit bestätige ich die Richtigkeit und Vollständigkeit meiner Angaben.',
|
|
required=True,
|
|
)
|
|
|
|
class Meta:
|
|
model = OnboardingRequest
|
|
fields = [
|
|
'full_name',
|
|
'gender',
|
|
'job_title',
|
|
'department',
|
|
'work_email',
|
|
'contract_start',
|
|
'employment_type',
|
|
'employment_end_date',
|
|
'handover_date',
|
|
'order_business_cards',
|
|
'business_card_name',
|
|
'business_card_title',
|
|
'business_card_email',
|
|
'business_card_phone',
|
|
'group_mailboxes',
|
|
'additional_software',
|
|
'additional_hardware_other',
|
|
'additional_access_text',
|
|
'needed_resources',
|
|
'successor_name',
|
|
'additional_notes',
|
|
'signature_image',
|
|
]
|
|
widgets = {
|
|
'group_mailboxes': forms.Textarea(attrs={'rows': 2}),
|
|
'additional_software': forms.Textarea(attrs={'rows': 2}),
|
|
'additional_hardware_other': forms.Textarea(attrs={'rows': 2}),
|
|
'additional_access_text': forms.Textarea(attrs={'rows': 2}),
|
|
'successor_name': forms.TextInput(),
|
|
'additional_notes': forms.Textarea(attrs={'rows': 4}),
|
|
}
|
|
|
|
@staticmethod
|
|
def _choices_from_options(category: str, fallback: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
options = FormOption.objects.filter(category=category, is_active=True).order_by('sort_order', 'label')
|
|
if not options.exists():
|
|
return fallback
|
|
language_code = get_language()
|
|
return [(o.value or o.label, o.translated_label(language_code)) for o in options]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.requester_email = (kwargs.pop('requester_email', '') or '').strip().lower()
|
|
super().__init__(*args, **kwargs)
|
|
self.email_domain = get_company_email_domain()
|
|
|
|
config = WorkflowConfig.objects.order_by('id').first()
|
|
self.handover_lead_days = max(0, int(getattr(config, 'device_handover_lead_days', 5) or 5))
|
|
minimum_handover_date = timezone.localdate() + timedelta(days=self.handover_lead_days)
|
|
self.fields['handover_date'].widget.attrs['min'] = minimum_handover_date.isoformat()
|
|
|
|
self.fields['full_name'].label = 'Name'
|
|
self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain}
|
|
full_name_initial = (self.initial.get('full_name') or '').strip()
|
|
if full_name_initial and not self.initial.get('first_name') and not self.initial.get('last_name'):
|
|
name_parts = full_name_initial.split()
|
|
if name_parts:
|
|
self.fields['first_name'].initial = name_parts[0]
|
|
self.fields['last_name'].initial = ' '.join(name_parts[1:]) if len(name_parts) > 1 else ''
|
|
self.fields['department'].choices = self._choices_from_options('department', DEPARTMENT_CHOICES)
|
|
self.fields['needed_devices_multi'].choices = self._choices_from_options('device', DEVICE_CHOICES)
|
|
self.fields['needed_software_multi'].choices = self._choices_from_options('software', SOFTWARE_CHOICES)
|
|
self.fields['needed_accesses_multi'].choices = self._choices_from_options('access', ACCESS_CHOICES)
|
|
self.fields['needed_workspace_groups_multi'].choices = self._choices_from_options('workspace_group', WORKSPACE_GROUP_CHOICES)
|
|
self.fields['needed_resources_multi'].choices = self._choices_from_options('resource', RESOURCE_CHOICES)
|
|
self.fields['signature_image'].required = False
|
|
apply_form_field_config('onboarding', self)
|
|
|
|
def clean_work_email(self):
|
|
value = (self.cleaned_data.get('work_email') or '').strip().lower()
|
|
if not value:
|
|
return value
|
|
expected_suffix = f'@{self.email_domain}'
|
|
if self.email_domain and not value.endswith(expected_suffix):
|
|
raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain})
|
|
return value
|
|
|
|
def clean_signature_image(self):
|
|
image = self.cleaned_data.get('signature_image')
|
|
if not image:
|
|
return image
|
|
max_size = 4 * 1024 * 1024 # 4 MB
|
|
if image.size > max_size:
|
|
raise forms.ValidationError('Die Signatur-Datei ist zu groß (max. 4 MB).')
|
|
content_type = (getattr(image, 'content_type', '') or '').lower().strip()
|
|
extension = Path(getattr(image, 'name', '')).suffix.lower()
|
|
allowed_content_types = {
|
|
'image/png',
|
|
'image/x-png',
|
|
'image/jpeg',
|
|
'image/jpg',
|
|
'image/pjpeg',
|
|
}
|
|
allowed_extensions = {'.png', '.jpg', '.jpeg'}
|
|
if content_type and not content_type.startswith('image/'):
|
|
raise forms.ValidationError('Bitte eine PNG- oder JPG-Datei hochladen.')
|
|
if content_type and content_type not in allowed_content_types and extension not in allowed_extensions:
|
|
raise forms.ValidationError('Bitte eine PNG- oder JPG-Datei hochladen.')
|
|
if not content_type and extension not in allowed_extensions:
|
|
raise forms.ValidationError('Bitte eine PNG- oder JPG-Datei hochladen.')
|
|
try:
|
|
header = image.read(16)
|
|
image.seek(0)
|
|
except Exception:
|
|
raise forms.ValidationError('Die Signatur-Datei konnte nicht gelesen werden.')
|
|
is_png = header.startswith(b'\x89PNG\r\n\x1a\n')
|
|
is_jpeg = header.startswith(b'\xff\xd8\xff')
|
|
if not (is_png or is_jpeg):
|
|
raise forms.ValidationError('Die Signatur-Datei ist kein gültiges PNG/JPG-Bild.')
|
|
return image
|
|
|
|
def clean(self):
|
|
cleaned = super().clean()
|
|
has = self.fields.__contains__
|
|
first_name = (cleaned.get('first_name') or '').strip()
|
|
last_name = (cleaned.get('last_name') or '').strip()
|
|
full_name = (cleaned.get('full_name') or '').strip()
|
|
|
|
if first_name and last_name:
|
|
cleaned['full_name'] = f'{first_name} {last_name}'.strip()
|
|
elif full_name:
|
|
parts = full_name.split()
|
|
cleaned['first_name'] = parts[0] if parts else ''
|
|
cleaned['last_name'] = ' '.join(parts[1:]) if len(parts) > 1 else ''
|
|
else:
|
|
if not first_name:
|
|
self.add_error('first_name', 'Bitte Vornamen eingeben.')
|
|
if not last_name:
|
|
self.add_error('last_name', 'Bitte Nachnamen eingeben.')
|
|
|
|
if has('employment_end_date') and cleaned.get('employment_type') == 'befristet' and not cleaned.get('employment_end_date'):
|
|
self.add_error('employment_end_date', 'Bei befristeter Beschäftigung ist ein Enddatum erforderlich.')
|
|
|
|
if cleaned.get('order_business_cards'):
|
|
for f in ['business_card_name', 'business_card_title', 'business_card_email', 'business_card_phone']:
|
|
if has(f) and not cleaned.get(f):
|
|
self.add_error(f, 'Dieses Feld ist für die Visitenkartenbestellung erforderlich.')
|
|
|
|
if has('group_mailboxes') and cleaned.get('group_mailboxes_required_choice') == 'ja' and not cleaned.get('group_mailboxes'):
|
|
self.add_error('group_mailboxes', 'Bitte Gruppenpostfächer eintragen.')
|
|
|
|
if has('additional_hardware_multi') and cleaned.get('additional_hardware_needed_choice') == 'ja' and not cleaned.get('additional_hardware_multi'):
|
|
self.add_error('additional_hardware_multi', 'Bitte mindestens eine Hardware-Option wählen.')
|
|
|
|
if has('additional_software_multi') and cleaned.get('additional_software_needed_choice') == 'ja' and not cleaned.get('additional_software_multi'):
|
|
self.add_error('additional_software_multi', 'Bitte mindestens eine Software-Option wählen.')
|
|
|
|
if has('additional_access_text') and cleaned.get('additional_access_needed_choice') == 'ja' and not cleaned.get('additional_access_text'):
|
|
self.add_error('additional_access_text', 'Bitte zusätzliche Zugänge eintragen.')
|
|
|
|
if has('successor_name') and cleaned.get('successor_required_choice') == 'ja' and not cleaned.get('successor_name'):
|
|
self.add_error('successor_name', 'Bitte Name der Vorgängerperson eintragen.')
|
|
|
|
handover_date = cleaned.get('handover_date')
|
|
if has('handover_date') and handover_date:
|
|
minimum_date = timezone.localdate() + timedelta(days=getattr(self, 'handover_lead_days', 5))
|
|
if handover_date < minimum_date:
|
|
self.add_error(
|
|
'handover_date',
|
|
_('Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen (frühestens %(date)s).') % {
|
|
'days': getattr(self, 'handover_lead_days', 5),
|
|
'date': minimum_date.strftime('%Y-%m-%d'),
|
|
},
|
|
)
|
|
|
|
return cleaned
|
|
|
|
def save(self, commit=True):
|
|
instance = super().save(commit=False)
|
|
if not (instance.preferred_language or '').strip():
|
|
instance.preferred_language = (get_language() or 'de').split('-')[0]
|
|
|
|
instance.group_mailboxes_required = self.cleaned_data.get('group_mailboxes_required_choice') == 'ja'
|
|
instance.additional_software_needed = self.cleaned_data.get('additional_software_needed_choice') == 'ja'
|
|
instance.additional_hardware_needed = self.cleaned_data.get('additional_hardware_needed_choice') == 'ja'
|
|
instance.additional_access_needed = self.cleaned_data.get('additional_access_needed_choice') == 'ja'
|
|
instance.successor_required = self.cleaned_data.get('successor_required_choice') == 'ja'
|
|
instance.inherit_phone_number = self.cleaned_data.get('inherit_phone_number_choice') == 'ja'
|
|
|
|
instance.needed_devices = '\n'.join(self.cleaned_data.get('needed_devices_multi', []))
|
|
instance.needed_software = '\n'.join(self.cleaned_data.get('needed_software_multi', []))
|
|
instance.needed_accesses = '\n'.join(self.cleaned_data.get('needed_accesses_multi', []))
|
|
instance.needed_workspace_groups = '\n'.join(self.cleaned_data.get('needed_workspace_groups_multi', []))
|
|
instance.needed_resources = '\n'.join(self.cleaned_data.get('needed_resources_multi', []))
|
|
instance.additional_hardware = '\n'.join(self.cleaned_data.get('additional_hardware_multi', []))
|
|
|
|
selected_extra_software = self.cleaned_data.get('additional_software_multi', [])
|
|
free_software = self.cleaned_data.get('additional_software', '').strip()
|
|
all_extra_software = list(selected_extra_software)
|
|
if free_software:
|
|
all_extra_software.append(free_software)
|
|
instance.additional_software = '\n'.join([s for s in all_extra_software if s])
|
|
|
|
if not instance.successor_required:
|
|
instance.successor_name = ''
|
|
instance.inherit_phone_number = False
|
|
|
|
if instance.inherit_phone_number:
|
|
instance.phone_number = ''
|
|
else:
|
|
instance.phone_number = self.cleaned_data.get('phone_number_choice', '')
|
|
|
|
instance.agreement = 'accepted' if self.cleaned_data.get('agreement_confirm') else ''
|
|
instance.onboarded_by_email = self.requester_email
|
|
|
|
if commit:
|
|
instance.save()
|
|
return instance
|
|
|
|
|
|
class OffboardingRequestForm(forms.ModelForm):
|
|
search_query = forms.CharField(
|
|
label='Mitarbeitende suchen (Name oder E-Mail)',
|
|
required=False,
|
|
help_text='Optional: Suche zur automatischen Vorbefüllung aus bereits onboardeten Personen.',
|
|
)
|
|
|
|
class Meta:
|
|
model = OffboardingRequest
|
|
fields = [
|
|
'full_name',
|
|
'work_email',
|
|
'department',
|
|
'job_title',
|
|
'last_working_day',
|
|
'notes',
|
|
]
|
|
widgets = {
|
|
'last_working_day': forms.DateInput(attrs={'type': 'date'}),
|
|
'notes': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
prefill_profile = kwargs.pop('prefill_profile', None)
|
|
super().__init__(*args, **kwargs)
|
|
self.email_domain = get_company_email_domain()
|
|
self.fields['full_name'].label = 'Vorname und Nachname'
|
|
self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain}
|
|
if prefill_profile:
|
|
self.fields['full_name'].initial = prefill_profile.full_name
|
|
self.fields['work_email'].initial = prefill_profile.work_email
|
|
self.fields['department'].initial = prefill_profile.department
|
|
self.fields['job_title'].initial = prefill_profile.job_title
|
|
apply_form_field_config('offboarding', self)
|
|
|
|
def clean_work_email(self):
|
|
value = (self.cleaned_data.get('work_email') or '').strip().lower()
|
|
if not value:
|
|
return value
|
|
expected_suffix = f'@{self.email_domain}'
|
|
if self.email_domain and not value.endswith(expected_suffix):
|
|
raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain})
|
|
return value
|