snapshot: extract remaining workflow view helper clusters
This commit is contained in:
109
backend/workflows/admin_section_builders.py
Normal file
109
backend/workflows/admin_section_builders.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
def build_branding_sections(form, branding):
|
||||
sections = [
|
||||
{
|
||||
'key': 'identity',
|
||||
'title': _('Identität'),
|
||||
'subtitle': _('Titel, Firmenname und zentrale Spracheinstellungen.'),
|
||||
'fields': ['portal_title', 'company_name', 'company_domain', 'default_language', 'login_subtitle'],
|
||||
'field_full': {'login_subtitle'},
|
||||
'hint_map': {
|
||||
'company_domain': _('Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. workdock.de.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'appearance',
|
||||
'title': _('Farben & Erscheinungsbild'),
|
||||
'subtitle': _('Zentrale visuelle Markenwerte und Browser-Icon.'),
|
||||
'fields': ['primary_color', 'secondary_color', 'logo_image', 'favicon_image'],
|
||||
'field_full': set(),
|
||||
'hint_map': {
|
||||
'logo_image': _('Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB.'),
|
||||
'favicon_image': _('Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'communication',
|
||||
'title': _('Kommunikation'),
|
||||
'subtitle': _('Absender, Support und PDF-Branding für ausgehende Kommunikation.'),
|
||||
'fields': ['support_email', 'sender_display_name', 'pdf_letterhead'],
|
||||
'field_full': {'pdf_letterhead'},
|
||||
'hint_map': {
|
||||
'sender_display_name': _('Wird für ausgehende System-E-Mails als Anzeigename verwendet.'),
|
||||
'pdf_letterhead': _('Erlaubtes Format: PDF. Maximal 10 MB.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'legal',
|
||||
'title': _('Footer & Rechtliches'),
|
||||
'subtitle': _('Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell.'),
|
||||
'fields': ['footer_text', 'legal_notice', 'footer_text_en', 'legal_notice_en'],
|
||||
'field_full': {'legal_notice', 'legal_notice_en'},
|
||||
'hint_map': {},
|
||||
},
|
||||
]
|
||||
for section in sections:
|
||||
rows = []
|
||||
for field_name in section['fields']:
|
||||
field = form[field_name]
|
||||
value = getattr(branding, field_name, '') or ''
|
||||
is_file = bool(getattr(field.field.widget, 'input_type', '') == 'file')
|
||||
rows.append(
|
||||
{
|
||||
'name': field_name,
|
||||
'bound_field': field,
|
||||
'label': field.label,
|
||||
'value': value,
|
||||
'is_file': is_file,
|
||||
'is_full': field_name in section.get('field_full', set()),
|
||||
'hint': section.get('hint_map', {}).get(field_name, ''),
|
||||
}
|
||||
)
|
||||
section['rows'] = rows
|
||||
return sections
|
||||
|
||||
|
||||
def build_company_config_sections(form, company_config):
|
||||
sections = [
|
||||
{
|
||||
'key': 'profile',
|
||||
'title': _('Firmenprofil'),
|
||||
'subtitle': _('Rechtlicher Name und zentrale Stammdaten der Firma.'),
|
||||
'fields': ['legal_company_name', 'phone_number', 'website_url', 'country'],
|
||||
},
|
||||
{
|
||||
'key': 'address',
|
||||
'title': _('Adresse & Register'),
|
||||
'subtitle': _('Anschrift sowie optionale Register- und Steuerangaben.'),
|
||||
'fields': ['street_address', 'postal_code', 'city', 'registration_number', 'vat_id'],
|
||||
},
|
||||
{
|
||||
'key': 'contacts',
|
||||
'title': _('Kontaktpunkte'),
|
||||
'subtitle': _('Zentrale Ansprechpartner für HR, IT und Operations.'),
|
||||
'fields': ['hr_contact_email', 'it_contact_email', 'operations_contact_email'],
|
||||
},
|
||||
{
|
||||
'key': 'public',
|
||||
'title': _('Recht & Öffentlichkeit'),
|
||||
'subtitle': _('Öffentliche Links für Website, Impressum und Datenschutz.'),
|
||||
'fields': ['imprint_url', 'privacy_url'],
|
||||
'hint': _('Diese Links können später im Portal-Footer oder in öffentlichen Seiten verwendet werden.'),
|
||||
},
|
||||
]
|
||||
for section in sections:
|
||||
rows = []
|
||||
for field_name in section['fields']:
|
||||
field = form[field_name]
|
||||
rows.append(
|
||||
{
|
||||
'name': field_name,
|
||||
'bound_field': field,
|
||||
'label': field.label,
|
||||
'value': getattr(company_config, field_name, '') or '',
|
||||
}
|
||||
)
|
||||
section['rows'] = rows
|
||||
return sections
|
||||
145
backend/workflows/admin_user_helpers.py
Normal file
145
backend/workflows/admin_user_helpers.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import render
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.utils.translation import gettext as _
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
|
||||
from .branding import get_branding_email_copy
|
||||
from .forms import UserManagementCreateForm
|
||||
from .models import AdminAuditLog
|
||||
from .emailing import send_system_email
|
||||
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, get_user_role_key
|
||||
|
||||
|
||||
def user_management_rows(*, display_user_name_fn):
|
||||
user_model = get_user_model()
|
||||
role_order = {
|
||||
ROLE_PLATFORM_OWNER: 0,
|
||||
ROLE_SUPER_ADMIN: 0,
|
||||
'admin': 1,
|
||||
'it_staff': 2,
|
||||
'staff': 3,
|
||||
}
|
||||
rows = []
|
||||
for user in user_model.objects.all().order_by('-is_active', 'username'):
|
||||
role_key = get_user_role_key(user)
|
||||
rows.append(
|
||||
{
|
||||
'user': user,
|
||||
'role_key': role_key,
|
||||
'role_label': str(ROLE_LABELS[role_key]),
|
||||
'role_sort': role_order.get(role_key, 99),
|
||||
'display_name': display_user_name_fn(user),
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda item: (not item['user'].is_active, item['role_sort'], item['user'].username.lower()))
|
||||
return rows
|
||||
|
||||
|
||||
def render_user_management(request, *, create_form=None, status_code: int = 200, audit_action_label_fn, display_user_name_fn):
|
||||
recent_user_events = list(
|
||||
AdminAuditLog.objects.select_related('actor')
|
||||
.filter(action__in=['user_created', 'user_updated', 'user_password_reset_sent', 'user_deleted'])
|
||||
.order_by('-created_at', '-id')[:12]
|
||||
)
|
||||
for row in recent_user_events:
|
||||
row.action_label = audit_action_label_fn(row.action)
|
||||
role_key = (row.details or {}).get('role')
|
||||
row.role_label = str(ROLE_LABELS[role_key]) if role_key in ROLE_LABELS else role_key
|
||||
include_product_owner = get_user_role_key(request.user) == ROLE_PLATFORM_OWNER
|
||||
return render(
|
||||
request,
|
||||
'workflows/user_management.html',
|
||||
{
|
||||
'create_form': create_form or UserManagementCreateForm(include_product_owner=include_product_owner),
|
||||
'rows': user_management_rows(display_user_name_fn=display_user_name_fn),
|
||||
'role_choices': [
|
||||
(key, str(ROLE_LABELS[key]))
|
||||
for key in ROLE_GROUP_NAMES
|
||||
if include_product_owner or key != ROLE_PLATFORM_OWNER
|
||||
],
|
||||
'include_product_owner': include_product_owner,
|
||||
'recent_user_events': recent_user_events,
|
||||
},
|
||||
status=status_code,
|
||||
)
|
||||
|
||||
|
||||
def platform_owner_user_count() -> int:
|
||||
user_model = get_user_model()
|
||||
return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_PLATFORM_OWNER and user.is_active)
|
||||
|
||||
|
||||
def super_admin_user_count() -> int:
|
||||
user_model = get_user_model()
|
||||
return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_SUPER_ADMIN and user.is_active)
|
||||
|
||||
|
||||
def would_remove_last_super_admin(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool:
|
||||
if get_user_role_key(user) != ROLE_SUPER_ADMIN or not user.is_active:
|
||||
return False
|
||||
if super_admin_user_count() > 1:
|
||||
return False
|
||||
if deleting:
|
||||
return True
|
||||
if new_role_key is not None and new_role_key != ROLE_SUPER_ADMIN:
|
||||
return True
|
||||
if new_is_active is not None and not new_is_active:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def would_remove_last_platform_owner(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool:
|
||||
if get_user_role_key(user) != ROLE_PLATFORM_OWNER or not user.is_active:
|
||||
return False
|
||||
if platform_owner_user_count() > 1:
|
||||
return False
|
||||
if deleting:
|
||||
return True
|
||||
if new_role_key is not None and new_role_key != ROLE_PLATFORM_OWNER:
|
||||
return True
|
||||
if new_is_active is not None and not new_is_active:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def send_user_access_email(request, target_user, *, invitation: bool, display_user_name_fn) -> None:
|
||||
email = (target_user.email or '').strip()
|
||||
if not email:
|
||||
raise ValueError(_('Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt.'))
|
||||
|
||||
uid = urlsafe_base64_encode(force_bytes(target_user.pk))
|
||||
token = default_token_generator.make_token(target_user)
|
||||
reset_path = reverse('password_reset_confirm', kwargs={'uidb64': uid, 'token': token})
|
||||
reset_url = request.build_absolute_uri(reset_path)
|
||||
branding_copy = get_branding_email_copy()
|
||||
|
||||
if invitation:
|
||||
subject = _('Zugangseinladung für %(username)s') % {'username': target_user.username}
|
||||
body = _(
|
||||
'Hallo %(name)s,\n\n'
|
||||
'für Sie wurde ein Benutzerkonto im %(portal_title)s angelegt.\n'
|
||||
'Bitte öffnen Sie den folgenden Link, um Ihr Passwort zu setzen:\n'
|
||||
'%(url)s\n\n'
|
||||
'Wenn Sie diese Einladung nicht erwartet haben, melden Sie sich bitte bei Ihrem Administrator.'
|
||||
) % {
|
||||
'name': display_user_name_fn(target_user),
|
||||
'portal_title': branding_copy['portal_title'],
|
||||
'url': reset_url,
|
||||
}
|
||||
else:
|
||||
subject = _('Passwort zurücksetzen für %(username)s') % {'username': target_user.username}
|
||||
body = _(
|
||||
'Hallo %(name)s,\n\n'
|
||||
'für Ihr Konto wurde ein Link zum Zurücksetzen des Passworts erstellt.\n'
|
||||
'Bitte öffnen Sie den folgenden Link:\n'
|
||||
'%(url)s\n\n'
|
||||
'Wenn Sie diese Anfrage nicht erwartet haben, können Sie diese E-Mail ignorieren.'
|
||||
) % {
|
||||
'name': display_user_name_fn(target_user),
|
||||
'url': reset_url,
|
||||
}
|
||||
|
||||
send_system_email(subject=subject, body=body, to=[email])
|
||||
810
backend/workflows/pdf_rendering.py
Normal file
810
backend/workflows/pdf_rendering.py
Normal file
@@ -0,0 +1,810 @@
|
||||
from pathlib import Path
|
||||
import base64
|
||||
import mimetypes
|
||||
import re
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _, get_language, override
|
||||
from jinja2 import Template
|
||||
from pypdf import PageObject, PdfReader, PdfWriter
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
from .branding import get_branding_email_copy, get_company_contact_copy, get_portal_letterhead_path
|
||||
from .models import IntroChecklistItem, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, WorkflowConfig
|
||||
from .forms import ACCESS_CHOICES, DEVICE_CHOICES, HARDWARE_EXTRA_CHOICES, OnboardingRequestForm, RESOURCE_CHOICES, SOFTWARE_CHOICES, SOFTWARE_EXTRA_CHOICES, WORKSPACE_GROUP_CHOICES
|
||||
from .pdf_sections import build_pdf_sections
|
||||
|
||||
|
||||
MANUAL_ONBOARDING_FIELD_SECTIONS = [
|
||||
(
|
||||
'Stammdaten',
|
||||
[
|
||||
'gender',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'job_title',
|
||||
'department',
|
||||
'work_email',
|
||||
'order_business_cards',
|
||||
'business_card_name',
|
||||
'business_card_title',
|
||||
'business_card_email',
|
||||
'business_card_phone',
|
||||
],
|
||||
),
|
||||
(
|
||||
'Vertrag',
|
||||
[
|
||||
'contract_start',
|
||||
'employment_type',
|
||||
'employment_end_date',
|
||||
'handover_date',
|
||||
],
|
||||
),
|
||||
(
|
||||
'IT-Setup',
|
||||
[
|
||||
'group_mailboxes_required_choice',
|
||||
'group_mailboxes',
|
||||
'additional_hardware_needed_choice',
|
||||
'additional_hardware_other',
|
||||
'additional_software_needed_choice',
|
||||
'additional_software',
|
||||
'additional_access_needed_choice',
|
||||
'additional_access_text',
|
||||
'successor_required_choice',
|
||||
'successor_name',
|
||||
'inherit_phone_number_choice',
|
||||
'phone_number_choice',
|
||||
],
|
||||
),
|
||||
(
|
||||
'Abschluss',
|
||||
[
|
||||
'additional_notes',
|
||||
'signature_image',
|
||||
'agreement_confirm',
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
def _split_name(full_name: str) -> tuple[str, str]:
|
||||
parts = full_name.split()
|
||||
if not parts:
|
||||
return '', ''
|
||||
return parts[0], ' '.join(parts[1:])
|
||||
|
||||
def _safe_filename_fragment(text: str, fallback: str = 'document') -> str:
|
||||
value = re.sub(r'[^A-Za-z0-9._-]+', '_', (text or '').strip()).strip('._')
|
||||
return value[:120] if value else fallback
|
||||
|
||||
def _resolve_user_display_name(email: str) -> str:
|
||||
email = (email or '').strip().lower()
|
||||
if not email:
|
||||
return ''
|
||||
user_model = get_user_model()
|
||||
user = user_model.objects.filter(email__iexact=email).first()
|
||||
if not user:
|
||||
return ''
|
||||
first_name = (getattr(user, 'first_name', '') or '').strip()
|
||||
last_name = (getattr(user, 'last_name', '') or '').strip()
|
||||
full_name = f'{first_name} {last_name}'.strip()
|
||||
if full_name:
|
||||
return full_name
|
||||
return (getattr(user, 'username', '') or '').strip()
|
||||
|
||||
def _chunk_list(data_list: list[str], chunk_size: int = 3) -> list[list[str]]:
|
||||
items = [i.strip() for i in data_list if i and i.strip()]
|
||||
chunks = []
|
||||
for i in range(0, len(items), chunk_size):
|
||||
chunks.append(items[i : i + chunk_size])
|
||||
return chunks
|
||||
|
||||
def _split_multiline(text: str) -> list[str]:
|
||||
return [line.strip() for line in (text or '').split('\n') if line.strip()]
|
||||
|
||||
def _chunk_choice_labels(choices: list[tuple[str, str]], chunk_size: int = 3) -> list[list[str]]:
|
||||
labels = [label for _, label in choices]
|
||||
return _chunk_list(labels, chunk_size=chunk_size)
|
||||
|
||||
def _normalized_lang(language_code: str | None) -> str:
|
||||
return (language_code or 'de').split('-')[0].lower() or 'de'
|
||||
|
||||
def _pdf_texts(language_code: str | None = None) -> dict[str, str]:
|
||||
lang = _normalized_lang(language_code)
|
||||
texts = {
|
||||
'de': {
|
||||
'lang': 'de',
|
||||
'not_available': 'Keine Angabe',
|
||||
'not_available_short': '-',
|
||||
'yes': 'Ja',
|
||||
'no': 'Nein',
|
||||
'onboarding_title': 'Onboarding-Unterlagen',
|
||||
'onboarding_staff_data': 'Personaldaten',
|
||||
'name': 'Name',
|
||||
'department': 'Abteilung',
|
||||
'job_title': 'Berufsbezeichnung',
|
||||
'work_email': 'Dienstliche E-Mail',
|
||||
'employment_type': 'Beschäftigungsverhältnis',
|
||||
'contract_start': 'Vertragsbeginn',
|
||||
'contract_end': 'Vertragsende',
|
||||
'handover_date': 'Übergabedatum',
|
||||
'equipment_access': 'Ausstattung und Zugänge',
|
||||
'devices': 'Benötigte Geräte und Gegenstände',
|
||||
'workspace_groups': 'Benötigte Gruppen im Workspace',
|
||||
'software': 'Benötigte Software',
|
||||
'accesses': 'Benötigte Zugänge',
|
||||
'resources': 'Benötigte Ressourcen',
|
||||
'group_mailboxes_required': 'Gruppenpostfächer erforderlich',
|
||||
'additional_hardware_needed': 'Darüber hinaus wird weitere Hardware benötigt',
|
||||
'additional_software_needed': 'Wird zusätzliche Software benötigt',
|
||||
'additional_access_needed': 'Darüber hinaus werden weitere Zugänge benötigt',
|
||||
'additional_details': 'Zusätzliche Angaben',
|
||||
'business_cards': 'Visitenkarten',
|
||||
'email': 'E-Mail',
|
||||
'phone': 'Telefon',
|
||||
'additional_hardware_other': 'Weitere Hardware (Freitext)',
|
||||
'successor_phone': 'Nachfolge und Telefon',
|
||||
'successor_of': 'Nachfolge von',
|
||||
'inherit_phone_number': 'Telefon von Vorgänger übernehmen',
|
||||
'direct_extension': 'Direktwahl',
|
||||
'notes': 'Notizen',
|
||||
'confirmation': 'Bestätigung',
|
||||
'requested_by_name': 'Angefordert von (Name)',
|
||||
'requested_by_email': 'Angefordert von (E-Mail)',
|
||||
'signature': 'Unterschrift',
|
||||
'signature_alt': 'Unterschrift',
|
||||
'onboarding_note': 'Hinweis: Dieses Formular dient als interne Prozessgrundlage für das Onboarding.',
|
||||
'offboarding_title': 'Offboarding-Unterlagen',
|
||||
'employee_info': 'Mitarbeitenden-Informationen',
|
||||
'last_working_day': 'Letzter Arbeitstag',
|
||||
'offboarding_requester': 'Offboarding-Anfordernde Person',
|
||||
'it_hardware_status': 'IT-Hardware-Status (aus Onboarding)',
|
||||
'hardware_check': 'Hardware-Check',
|
||||
'no_onboarding_hardware': 'Keine Onboarding-Hardwaredaten gefunden.',
|
||||
'manual_return_overview': 'Manuelle Rückgabeübersicht',
|
||||
'manual_return_note': 'Es wurden keine gespeicherten Onboarding-Daten zu dieser Person gefunden. Die folgenden Listen dienen als manuelle Rückgabe- und Prüfübersicht.',
|
||||
'returned_devices': 'Zurückgegebene Geräte und Artikel',
|
||||
'returned_software': 'Zurückgegebene bzw. deaktivierte Software',
|
||||
'removed_workspace_groups': 'Entfernte Gruppen im Workspace',
|
||||
'removed_accesses': 'Entfernte Zugänge',
|
||||
'returned_extra_it': 'Zurückgegebene zusätzliche Hardware / Software',
|
||||
'it_signatures': 'IT-Section: Signaturen',
|
||||
'it_checked_by': 'IT geprüft am durch:',
|
||||
'it_signature': 'IT-Unterschrift:',
|
||||
'return_complete': 'Rückgabe vollständig:',
|
||||
'offboarding_note': 'Hinweis: Dieses Formular dient als interne Prozessgrundlage für das Offboarding.',
|
||||
'intro_title': 'Einweisungs- und Übergabeprotokoll',
|
||||
'intro_sub': 'Gesprächsleitfaden für die persönliche Einführung neuer Mitarbeitender.',
|
||||
'base_data': 'Basisdaten',
|
||||
'start_date': 'Startdatum',
|
||||
'introduced_by': 'Einweisung durch',
|
||||
'intro_note': 'Dieses Dokument dient als Gesprächsleitfaden für die persönliche Einweisung. Die Felder können während des Termins manuell abgehakt und anschließend unterschrieben werden.',
|
||||
'employee_signature': 'Unterschrift Mitarbeitende Person:',
|
||||
'trainer_signature': 'Unterschrift Einweisende Person:',
|
||||
'intro_completed_at': 'Einweisung durchgeführt am:',
|
||||
'open_questions': 'Rückfragen offen / Nacharbeit erforderlich:',
|
||||
'live_intro_title': 'Einweisungsprotokoll',
|
||||
'live_intro_sub': 'Export des aktuellen Live-Status aus der webbasierten Einweisung.',
|
||||
'employment_start': 'Vertragsbeginn',
|
||||
'employee_signature_block': 'Unterschrift Mitarbeitende Person',
|
||||
},
|
||||
'en': {
|
||||
'lang': 'en',
|
||||
'not_available': 'Not provided',
|
||||
'not_available_short': '-',
|
||||
'yes': 'Yes',
|
||||
'no': 'No',
|
||||
'onboarding_title': 'Onboarding Documents',
|
||||
'onboarding_staff_data': 'Employee Details',
|
||||
'name': 'Name',
|
||||
'department': 'Department',
|
||||
'job_title': 'Job title',
|
||||
'work_email': 'Work email',
|
||||
'employment_type': 'Employment type',
|
||||
'contract_start': 'Contract start',
|
||||
'contract_end': 'Contract end',
|
||||
'handover_date': 'Handover date',
|
||||
'equipment_access': 'Equipment and access',
|
||||
'devices': 'Required devices and items',
|
||||
'workspace_groups': 'Required workspace groups',
|
||||
'software': 'Required software',
|
||||
'accesses': 'Required accesses',
|
||||
'resources': 'Required resources',
|
||||
'group_mailboxes_required': 'Group mailboxes required',
|
||||
'additional_hardware_needed': 'Additional hardware required',
|
||||
'additional_software_needed': 'Additional software required',
|
||||
'additional_access_needed': 'Additional accesses required',
|
||||
'additional_details': 'Additional details',
|
||||
'business_cards': 'Business cards',
|
||||
'email': 'Email',
|
||||
'phone': 'Phone',
|
||||
'additional_hardware_other': 'Additional hardware (free text)',
|
||||
'successor_phone': 'Successor and phone',
|
||||
'successor_of': 'Successor to',
|
||||
'inherit_phone_number': 'Take over predecessor phone number',
|
||||
'direct_extension': 'Direct extension',
|
||||
'notes': 'Notes',
|
||||
'confirmation': 'Confirmation',
|
||||
'requested_by_name': 'Requested by (name)',
|
||||
'requested_by_email': 'Requested by (email)',
|
||||
'signature': 'Signature',
|
||||
'signature_alt': 'Signature',
|
||||
'onboarding_note': 'Note: This form serves as the internal process basis for onboarding.',
|
||||
'offboarding_title': 'Offboarding Documents',
|
||||
'employee_info': 'Employee information',
|
||||
'last_working_day': 'Last working day',
|
||||
'offboarding_requester': 'Offboarding requester',
|
||||
'it_hardware_status': 'IT hardware status (from onboarding)',
|
||||
'hardware_check': 'Hardware check',
|
||||
'no_onboarding_hardware': 'No onboarding hardware data found.',
|
||||
'manual_return_overview': 'Manual return overview',
|
||||
'manual_return_note': 'No stored onboarding data was found for this person. The following lists serve as a manual return and review overview.',
|
||||
'returned_devices': 'Returned devices and items',
|
||||
'returned_software': 'Returned or disabled software',
|
||||
'removed_workspace_groups': 'Removed workspace groups',
|
||||
'removed_accesses': 'Removed accesses',
|
||||
'returned_extra_it': 'Returned additional hardware / software',
|
||||
'it_signatures': 'IT section: signatures',
|
||||
'it_checked_by': 'Checked by IT on:',
|
||||
'it_signature': 'IT signature:',
|
||||
'return_complete': 'Return complete:',
|
||||
'offboarding_note': 'Note: This form serves as the internal process basis for offboarding.',
|
||||
'intro_title': 'Introduction and Handover Protocol',
|
||||
'intro_sub': 'Conversation guide for the personal introduction of new employees.',
|
||||
'base_data': 'Basic data',
|
||||
'start_date': 'Start date',
|
||||
'introduced_by': 'Introduction by',
|
||||
'intro_note': 'This document serves as a conversation guide for the personal introduction. The fields can be checked manually during the meeting and signed afterwards.',
|
||||
'employee_signature': 'Employee signature:',
|
||||
'trainer_signature': 'Trainer signature:',
|
||||
'intro_completed_at': 'Introduction completed on:',
|
||||
'open_questions': 'Open questions / follow-up required:',
|
||||
'live_intro_title': 'Introduction Protocol',
|
||||
'live_intro_sub': 'Export of the current live status from the web-based introduction.',
|
||||
'employment_start': 'Contract start',
|
||||
'employee_signature_block': 'Employee signature',
|
||||
},
|
||||
}
|
||||
return texts.get(lang, texts['de'])
|
||||
|
||||
def _manual_onboarding_field_sections() -> list[dict]:
|
||||
fields = OnboardingRequestForm.base_fields
|
||||
sections = []
|
||||
for title, field_names in MANUAL_ONBOARDING_FIELD_SECTIONS:
|
||||
labels = [str(fields[name].label or name) for name in field_names if name in fields]
|
||||
if not labels:
|
||||
continue
|
||||
sections.append({'title': title, 'rows': _chunk_list(labels, chunk_size=2)})
|
||||
return sections
|
||||
|
||||
def _matches_intro_condition(request_obj: OnboardingRequest, item: IntroChecklistItem) -> bool:
|
||||
operator = (item.condition_operator or 'always').strip()
|
||||
field_name = (item.condition_field or '').strip()
|
||||
expected = (item.condition_value or '').strip()
|
||||
|
||||
if operator == 'always' or not field_name:
|
||||
return True
|
||||
|
||||
raw_value = getattr(request_obj, field_name, '')
|
||||
if raw_value is None:
|
||||
raw_value = ''
|
||||
|
||||
if operator == 'is_true':
|
||||
return bool(raw_value)
|
||||
if operator == 'is_false':
|
||||
return not bool(raw_value)
|
||||
|
||||
text_value = str(raw_value).strip()
|
||||
if operator == 'equals':
|
||||
return text_value.lower() == expected.lower()
|
||||
if operator == 'contains':
|
||||
return expected.lower() in text_value.lower()
|
||||
return True
|
||||
|
||||
def _build_intro_sections_from_admin(request_obj: OnboardingRequest, language_code: str | None = None) -> dict[str, list[str]]:
|
||||
items = list(IntroChecklistItem.objects.filter(is_active=True).order_by('section', 'sort_order', 'label'))
|
||||
if not items:
|
||||
return {}
|
||||
|
||||
section_map = {key: [] for key, _label in IntroChecklistItem.SECTION_CHOICES}
|
||||
for item in items:
|
||||
if item.section not in section_map:
|
||||
continue
|
||||
if _matches_intro_condition(request_obj, item):
|
||||
section_map[item.section].append(item.translated_label(language_code))
|
||||
return {key: values for key, values in section_map.items() if values}
|
||||
|
||||
def build_intro_sections_for_request(request_obj: OnboardingRequest, language_code: str | None = None) -> list[dict]:
|
||||
lang = _normalized_lang(language_code or get_language())
|
||||
with override(lang):
|
||||
section_titles = {
|
||||
'workplace': _('Geräte und Arbeitsplatz'),
|
||||
'accounts': _('Konten und Berechtigungen'),
|
||||
'software': _('Software und Tools'),
|
||||
'process': _('Prozesse und Hinweise'),
|
||||
}
|
||||
devices = _split_multiline(request_obj.needed_devices)
|
||||
software = _split_multiline(request_obj.needed_software)
|
||||
accesses = _split_multiline(request_obj.needed_accesses)
|
||||
groups = _split_multiline(request_obj.needed_workspace_groups)
|
||||
resources = _split_multiline(request_obj.needed_resources)
|
||||
extra_hardware = _split_multiline(request_obj.additional_hardware)
|
||||
extra_software = _split_multiline(request_obj.additional_software)
|
||||
group_mailboxes = _split_multiline(request_obj.group_mailboxes)
|
||||
|
||||
workplace_items = []
|
||||
for item in devices:
|
||||
workplace_items.append(_('%(item)s übergeben und Grundfunktionen erklärt') % {'item': item})
|
||||
for item in resources:
|
||||
workplace_items.append(_('%(item)s gezeigt bzw. Nutzung erklärt') % {'item': item})
|
||||
if request_obj.phone_number:
|
||||
workplace_items.append(_('Telefonnummer / Direktwahl erklärt: %(value)s') % {'value': request_obj.phone_number})
|
||||
if not workplace_items:
|
||||
workplace_items.append(_('Arbeitsplatz, Geräte und allgemeine Nutzung besprochen'))
|
||||
|
||||
account_items = [_('%(item)s Zugang erklärt') % {'item': item} for item in accesses]
|
||||
account_items.extend([_('%(item)s Gruppe / Berechtigung erläutert') % {'item': item} for item in groups])
|
||||
if request_obj.work_email:
|
||||
account_items.insert(0, _('Dienstliche E-Mail-Adresse erläutert: %(value)s') % {'value': request_obj.work_email})
|
||||
if group_mailboxes:
|
||||
account_items.extend([_('Gruppenpostfach erklärt: %(item)s') % {'item': item} for item in group_mailboxes])
|
||||
if not account_items:
|
||||
account_items.append(_('Zugänge, Konten und Anmeldelogik besprochen'))
|
||||
|
||||
software_items = [_('%(item)s Einführung durchgeführt') % {'item': item} for item in software]
|
||||
software_items.extend([_('%(item)s zusätzlich besprochen') % {'item': item} for item in extra_software])
|
||||
if not software_items:
|
||||
software_items.append(_('Benötigte Standardsoftware und tägliche Nutzung erklärt'))
|
||||
|
||||
process_items = [
|
||||
_('Passwortregeln und sicherer Umgang besprochen'),
|
||||
_('Dateiablage, Nextcloud und Freigaben erklärt'),
|
||||
_('Kommunikationswege und Support-Prozess erklärt'),
|
||||
]
|
||||
if extra_hardware:
|
||||
process_items.extend([_('%(item)s als zusätzliche Ausstattung besprochen') % {'item': item} for item in extra_hardware])
|
||||
if request_obj.additional_access_text:
|
||||
process_items.extend([_('Zusätzlicher Zugang besprochen: %(item)s') % {'item': item} for item in _split_multiline(request_obj.additional_access_text)])
|
||||
if request_obj.successor_name:
|
||||
process_items.append(_('Übergabe-/Nachfolgekontext besprochen: %(value)s') % {'value': request_obj.successor_name})
|
||||
|
||||
custom_intro_items = _build_intro_sections_from_admin(request_obj, lang)
|
||||
intro_sections_raw = [
|
||||
('workplace', section_titles['workplace'], workplace_items),
|
||||
('accounts', section_titles['accounts'], account_items),
|
||||
('software', section_titles['software'], software_items),
|
||||
('process', section_titles['process'], process_items),
|
||||
]
|
||||
|
||||
sections = []
|
||||
for key, title, default_items in intro_sections_raw:
|
||||
merged_items = list(default_items)
|
||||
merged_items.extend(custom_intro_items.get(key, []))
|
||||
section_items = []
|
||||
for idx, label in enumerate(merged_items, start=1):
|
||||
section_items.append({'id': f'{key}_{idx}', 'label': label})
|
||||
if section_items:
|
||||
sections.append({'key': key, 'title': title, 'items': section_items})
|
||||
return sections
|
||||
|
||||
def _render_html(template_path: Path, context: dict) -> str:
|
||||
with template_path.open('r', encoding='utf-8') as handle:
|
||||
template = Template(handle.read())
|
||||
return template.render(context)
|
||||
|
||||
def _generate_content_pdf(html_content: str, output_pdf: Path) -> None:
|
||||
page_style = (
|
||||
'<style>'
|
||||
'@page { size: A4; margin: 58mm 16mm 16mm 16mm; }'
|
||||
'body { margin: 0; }'
|
||||
'</style>'
|
||||
)
|
||||
if '<head>' in html_content:
|
||||
html_content = html_content.replace('<head>', f'<head>{page_style}', 1)
|
||||
else:
|
||||
html_content = page_style + html_content
|
||||
|
||||
output_pdf.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output_pdf.open('wb') as fp:
|
||||
result = pisa.CreatePDF(
|
||||
src=html_content,
|
||||
dest=fp,
|
||||
encoding='utf-8',
|
||||
)
|
||||
if result.err:
|
||||
raise RuntimeError(f'Failed to render PDF content for {output_pdf.name}')
|
||||
|
||||
def _overlay_with_letterhead(content_pdf: Path, letterhead_pdf: Path, output_pdf: Path) -> None:
|
||||
letterhead_reader = PdfReader(str(letterhead_pdf))
|
||||
content_reader = PdfReader(str(content_pdf))
|
||||
writer = PdfWriter()
|
||||
|
||||
letterhead_page = letterhead_reader.pages[0]
|
||||
for page in content_reader.pages:
|
||||
merged = PageObject.create_blank_page(
|
||||
width=letterhead_page.mediabox.width,
|
||||
height=letterhead_page.mediabox.height,
|
||||
)
|
||||
merged.merge_page(letterhead_page)
|
||||
merged.merge_page(page)
|
||||
writer.add_page(merged)
|
||||
|
||||
with output_pdf.open('wb') as fp:
|
||||
writer.write(fp)
|
||||
|
||||
def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path:
|
||||
lang = _normalized_lang(request_obj.preferred_language)
|
||||
t = _pdf_texts(lang)
|
||||
first_name, last_name = _split_name(request_obj.full_name)
|
||||
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_{request_obj.id}')
|
||||
output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_letter_{safe_name}.pdf'
|
||||
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_{safe_name}.pdf'
|
||||
|
||||
template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_template.html'
|
||||
letterhead_path = get_portal_letterhead_path()
|
||||
company_contact = get_company_contact_copy()
|
||||
|
||||
devices = _split_multiline(request_obj.needed_devices)
|
||||
software = _split_multiline(request_obj.needed_software)
|
||||
accesses = _split_multiline(request_obj.needed_accesses)
|
||||
groups = _split_multiline(request_obj.needed_workspace_groups)
|
||||
resources = _split_multiline(request_obj.needed_resources)
|
||||
group_mailboxes_list = _split_multiline(request_obj.group_mailboxes or '')
|
||||
additional_hardware_list = _split_multiline(request_obj.additional_hardware or '')
|
||||
additional_software_list = _split_multiline(request_obj.additional_software or '')
|
||||
additional_access_list = _split_multiline(request_obj.additional_access_text or '')
|
||||
|
||||
signature_src = ''
|
||||
signature_note = t['not_available_short']
|
||||
if getattr(request_obj, 'signature_image', None):
|
||||
try:
|
||||
signature_path = Path(request_obj.signature_image.path).resolve()
|
||||
with signature_path.open('rb') as sig_fp:
|
||||
encoded = base64.b64encode(sig_fp.read()).decode('ascii')
|
||||
mime_type = mimetypes.guess_type(signature_path.name)[0] or 'image/png'
|
||||
signature_src = f"data:{mime_type};base64,{encoded}"
|
||||
signature_note = 'Digital signature stored as image file.' if lang == 'en' else 'Digitale Signatur als Bilddatei hinterlegt.'
|
||||
except Exception:
|
||||
signature_src = ''
|
||||
signature_note = request_obj.signature_url or t['not_available_short']
|
||||
elif request_obj.signature_url:
|
||||
signature_note = request_obj.signature_url
|
||||
|
||||
requester_email = request_obj.onboarded_by_email or t['not_available_short']
|
||||
requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or t['not_available_short']
|
||||
gender = (request_obj.get_gender_display() or t['not_available_short']).strip() or t['not_available_short']
|
||||
employment_type = (request_obj.employment_type or t['not_available_short']).strip() or t['not_available_short']
|
||||
employment_end = request_obj.employment_end_date or t['not_available_short']
|
||||
order_business_cards = bool(request_obj.order_business_cards)
|
||||
group_mailboxes = (request_obj.group_mailboxes or '').strip()
|
||||
additional_hardware_other = (request_obj.additional_hardware_other or '').strip()
|
||||
additional_hardware = (request_obj.additional_hardware or '').strip()
|
||||
additional_software = (request_obj.additional_software or '').strip()
|
||||
additional_access_text = (request_obj.additional_access_text or '').strip()
|
||||
successor_name = (request_obj.successor_name or '').strip()
|
||||
additional_notes = (request_obj.additional_notes or '').strip()
|
||||
phone_number = (request_obj.phone_number or '').strip()
|
||||
display_name = f"{gender} {first_name} {last_name}".strip() if gender and gender != '-' else f"{first_name} {last_name}".strip()
|
||||
|
||||
pdf_sections = build_pdf_sections('onboarding', request_obj, lang)
|
||||
pdf_section_map = {section['key']: section for section in pdf_sections if section.get('has_content')}
|
||||
pdf_field_map = {}
|
||||
for section in pdf_sections:
|
||||
for field in section.get('render_fields', []):
|
||||
pdf_field_map[field['name']] = field
|
||||
|
||||
def _field_text(name: str, fallback):
|
||||
field = pdf_field_map.get(name)
|
||||
if not field:
|
||||
return fallback
|
||||
value = field.get('display_value')
|
||||
if isinstance(value, list):
|
||||
return ' | '.join(value) if value else fallback
|
||||
return value or fallback
|
||||
|
||||
def _field_list(name: str) -> list[str]:
|
||||
field = pdf_field_map.get(name)
|
||||
if not field:
|
||||
return []
|
||||
value = field.get('display_value')
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
text = str(value or '').strip()
|
||||
return [text] if text else []
|
||||
|
||||
devices_visible = _field_list('needed_devices_multi')
|
||||
groups_visible = _field_list('needed_workspace_groups_multi')
|
||||
software_visible = _field_list('needed_software_multi')
|
||||
accesses_visible = _field_list('needed_accesses_multi')
|
||||
resources_visible = _field_list('needed_resources_multi')
|
||||
group_mailboxes_visible = _field_list('group_mailboxes')
|
||||
additional_hardware_visible = _field_list('additional_hardware_multi')
|
||||
additional_software_visible = _field_list('additional_software_multi')
|
||||
additional_access_visible = _field_list('additional_access_text')
|
||||
job_title_visible = 'job_title' in pdf_field_map
|
||||
itsetup_visible = 'itsetup' in pdf_section_map
|
||||
|
||||
context = {
|
||||
'T': t,
|
||||
'PDF_LANG': lang,
|
||||
'PDF_SECTIONS': pdf_sections,
|
||||
'VORNAME': first_name,
|
||||
'NACHNAME': last_name,
|
||||
'DISPLAY_NAME': display_name or request_obj.full_name,
|
||||
'ANREDE': _field_text('gender', gender),
|
||||
'JOB_TITLE_VISIBLE': job_title_visible,
|
||||
'BERUFSBEZEICHNUNG': _field_text('job_title', t['not_available']),
|
||||
'ABTEILUNG': _field_text('department', t['not_available']),
|
||||
'EMAIL': _field_text('work_email', t['not_available']),
|
||||
'VERTRAGSBEGINN': _field_text('contract_start', request_obj.contract_start),
|
||||
'BESCHAEFTIGUNG': _field_text('employment_type', employment_type),
|
||||
'VERTRAGSENDE': _field_text('employment_end_date', employment_end),
|
||||
'UEBERGABEDATUM': _field_text('handover_date', request_obj.handover_date or t['not_available_short']),
|
||||
'ARBEITSGERAETE_TEXT': ' | '.join(devices_visible) if devices_visible else t['not_available'],
|
||||
'WORKSPACE_GROUPS_TEXT': ' | '.join(groups_visible) if groups_visible else t['not_available'],
|
||||
'SOFTWARE_TEXT': ' | '.join(software_visible) if software_visible else t['not_available'],
|
||||
'ZUGAENGE_TEXT': ' | '.join(accesses_visible) if accesses_visible else t['not_available'],
|
||||
'RESSOURCEN_TEXT': ' | '.join(resources_visible) if resources_visible else t['not_available'],
|
||||
'VISITENKARTE_BESTELLT': order_business_cards,
|
||||
'HAS_VISITENKARTE_DATEN': order_business_cards and ('business_card_name' in pdf_field_map or 'business_card_title' in pdf_field_map or 'business_card_email' in pdf_field_map or 'business_card_phone' in pdf_field_map) and any(
|
||||
[
|
||||
_field_text('business_card_name', '').strip(),
|
||||
_field_text('business_card_title', '').strip(),
|
||||
_field_text('business_card_email', '').strip(),
|
||||
_field_text('business_card_phone', '').strip(),
|
||||
]
|
||||
),
|
||||
'VISITENKARTE_NAME': _field_text('business_card_name', t['not_available_short']),
|
||||
'VISITENKARTE_TITEL': _field_text('business_card_title', t['not_available_short']),
|
||||
'VISITENKARTE_EMAIL': _field_text('business_card_email', t['not_available_short']),
|
||||
'VISITENKARTE_TELEFON': _field_text('business_card_phone', t['not_available_short']),
|
||||
'GROUP_MAILBOXES': _field_text('group_mailboxes', group_mailboxes or t['not_available']),
|
||||
'ADDITIONAL_HARDWARE_OTHER': _field_text('additional_hardware_other', additional_hardware_other or t['not_available']),
|
||||
'ADDITIONAL_HARDWARE': _field_text('additional_hardware_other', additional_hardware or t['not_available']),
|
||||
'ADDITIONAL_SOFTWARE': _field_text('additional_software', additional_software or t['not_available']),
|
||||
'ADDITIONAL_ACCESS_TEXT': _field_text('additional_access_text', additional_access_text or t['not_available']),
|
||||
'SUCCESSOR_NAME': _field_text('successor_name', successor_name or t['not_available']),
|
||||
'PHONE_NUMBER': _field_text('phone_number_choice', phone_number or t['not_available_short']),
|
||||
'INHERIT_PHONE_NUMBER': _field_text('inherit_phone_number_choice', t['yes'] if request_obj.inherit_phone_number else t['no']),
|
||||
'ADDITIONAL_NOTES': _field_text('additional_notes', additional_notes or t['not_available']),
|
||||
'GROUP_MAILBOXES_REQUIRED': 'group_mailboxes_required_choice' in pdf_field_map and bool(group_mailboxes_visible),
|
||||
'ADDITIONAL_HARDWARE_NEEDED': 'additional_hardware_needed_choice' in pdf_field_map and bool(additional_hardware_visible),
|
||||
'ADDITIONAL_SOFTWARE_NEEDED': 'additional_software_needed_choice' in pdf_field_map and bool(additional_software_visible),
|
||||
'ADDITIONAL_ACCESS_NEEDED': 'additional_access_needed_choice' in pdf_field_map and bool(additional_access_visible),
|
||||
'HAS_DEVICES': itsetup_visible and bool(devices_visible),
|
||||
'HAS_GROUPS': itsetup_visible and bool(groups_visible),
|
||||
'HAS_SOFTWARE': itsetup_visible and bool(software_visible),
|
||||
'HAS_ACCESSES': itsetup_visible and bool(accesses_visible),
|
||||
'HAS_RESOURCES': itsetup_visible and bool(resources_visible),
|
||||
'HAS_GROUP_MAILBOXES': bool(group_mailboxes_visible),
|
||||
'HAS_ADDITIONAL_HARDWARE': bool(additional_hardware_visible),
|
||||
'HAS_ADDITIONAL_SOFTWARE': bool(additional_software_visible),
|
||||
'HAS_ADDITIONAL_ACCESS': bool(additional_access_visible),
|
||||
'HAS_ADDITIONAL_HARDWARE_OTHER': bool(_field_text('additional_hardware_other', '').strip()),
|
||||
'HAS_SUCCESSOR_INFO': bool(_field_text('successor_name', '').strip()) or 'inherit_phone_number_choice' in pdf_field_map or bool(_field_text('phone_number_choice', '').strip()),
|
||||
'HAS_ADDITIONAL_NOTES': bool(_field_text('additional_notes', '').strip()),
|
||||
'GROUP_MAILBOXES_LIST': _chunk_list(group_mailboxes_visible),
|
||||
'ADDITIONAL_HARDWARE_LIST': _chunk_list(additional_hardware_visible),
|
||||
'ADDITIONAL_SOFTWARE_LIST': _chunk_list(additional_software_visible),
|
||||
'ADDITIONAL_ACCESS_LIST': _chunk_list(additional_access_visible),
|
||||
'ZUGAENGE_LIST': _chunk_list(groups_visible),
|
||||
'ARBEITSGERÄTE_LIST': _chunk_list(devices_visible),
|
||||
'SOFTWARE_LIST': _chunk_list(software_visible),
|
||||
'ACCOUNT_LIST': _chunk_list(accesses_visible),
|
||||
'STANDARD_RESSOURCEN': _chunk_list(resources_visible),
|
||||
'UNTERSCHRIFT': signature_src,
|
||||
'UNTERSCHRIFT_HINWEIS': signature_note,
|
||||
'REQUESTED_BY_NAME': requester_name,
|
||||
'REQUESTED_BY_EMAIL': requester_email,
|
||||
'COMPANY_LEGAL_NAME': company_contact['legal_company_name'] or company_contact['company_name'],
|
||||
'COMPANY_ADDRESS': company_contact['address'] or t['not_available_short'],
|
||||
'COMPANY_IT_CONTACT': company_contact['it_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_HR_CONTACT': company_contact['hr_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_PHONE': company_contact['phone_number'] or t['not_available_short'],
|
||||
}
|
||||
|
||||
html = _render_html(template_path, context)
|
||||
_generate_content_pdf(html, temp_pdf)
|
||||
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
|
||||
|
||||
if temp_pdf.exists():
|
||||
temp_pdf.unlink(missing_ok=True)
|
||||
return output_pdf
|
||||
|
||||
def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest, language_code: str | None = None) -> Path:
|
||||
lang = _normalized_lang(language_code or request_obj.preferred_language)
|
||||
t = _pdf_texts(lang)
|
||||
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_{request_obj.id}')
|
||||
output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_{safe_name}.pdf'
|
||||
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_{safe_name}.pdf'
|
||||
|
||||
template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_template.html'
|
||||
letterhead_path = get_portal_letterhead_path()
|
||||
company_contact = get_company_contact_copy()
|
||||
|
||||
salutation = (request_obj.get_gender_display() or '').strip()
|
||||
display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name
|
||||
intro_sections = [
|
||||
{
|
||||
'title': section['title'],
|
||||
'rows': _chunk_list([item['label'] for item in section['items']], chunk_size=2),
|
||||
}
|
||||
for section in build_intro_sections_for_request(request_obj, language_code=language_code)
|
||||
]
|
||||
|
||||
requester_email = request_obj.onboarded_by_email or '-'
|
||||
requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or '-'
|
||||
|
||||
context = {
|
||||
'T': t,
|
||||
'DISPLAY_NAME': display_name,
|
||||
'ABTEILUNG': request_obj.department or t['not_available_short'],
|
||||
'BERUFSBEZEICHNUNG': request_obj.job_title or t['not_available_short'],
|
||||
'VERTRAGSBEGINN': request_obj.contract_start,
|
||||
'EMAIL': request_obj.work_email or t['not_available_short'],
|
||||
'REQUESTED_BY_NAME': requester_name,
|
||||
'REQUESTED_BY_EMAIL': requester_email,
|
||||
'INTRO_SECTIONS': intro_sections,
|
||||
'COMPANY_LEGAL_NAME': company_contact['legal_company_name'] or company_contact['company_name'],
|
||||
'COMPANY_ADDRESS': company_contact['address'] or t['not_available_short'],
|
||||
'COMPANY_IT_CONTACT': company_contact['it_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_HR_CONTACT': company_contact['hr_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_PHONE': company_contact['phone_number'] or t['not_available_short'],
|
||||
}
|
||||
|
||||
html = _render_html(template_path, context)
|
||||
_generate_content_pdf(html, temp_pdf)
|
||||
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
|
||||
|
||||
if temp_pdf.exists():
|
||||
temp_pdf.unlink(missing_ok=True)
|
||||
return output_pdf
|
||||
|
||||
def _generate_onboarding_intro_session_pdf(
|
||||
session: OnboardingIntroductionSession,
|
||||
admin_signature_name: str = '-',
|
||||
language_code: str | None = None,
|
||||
) -> Path:
|
||||
request_obj = session.onboarding_request
|
||||
lang = _normalized_lang(language_code or request_obj.preferred_language)
|
||||
t = _pdf_texts(lang)
|
||||
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_session_{request_obj.id}')
|
||||
version = timezone.now().strftime('%Y%m%d%H%M%S')
|
||||
output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_session_{safe_name}_{version}.pdf'
|
||||
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_session_{safe_name}_{version}.pdf'
|
||||
|
||||
template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_session_pdf.html'
|
||||
letterhead_path = get_portal_letterhead_path()
|
||||
company_contact = get_company_contact_copy()
|
||||
|
||||
salutation = (request_obj.get_gender_display() or '').strip()
|
||||
display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name
|
||||
|
||||
raw_sections = build_intro_sections_for_request(request_obj, language_code=language_code)
|
||||
checked_map = session.checklist_state or {}
|
||||
exported_sections = []
|
||||
checked_count = 0
|
||||
total_count = 0
|
||||
for section in raw_sections:
|
||||
checked_items = []
|
||||
for item in section['items']:
|
||||
checked = bool(checked_map.get(item['id']))
|
||||
total_count += 1
|
||||
if checked:
|
||||
checked_count += 1
|
||||
checked_items.append({'label': item['label']})
|
||||
if checked_items:
|
||||
exported_sections.append({
|
||||
'title': section['title'],
|
||||
'rows': [checked_items[i:i + 2] for i in range(0, len(checked_items), 2)],
|
||||
})
|
||||
|
||||
requester_email = request_obj.onboarded_by_email or '-'
|
||||
requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or '-'
|
||||
|
||||
context = {
|
||||
'T': t,
|
||||
'DISPLAY_NAME': display_name,
|
||||
'ABTEILUNG': request_obj.department or t['not_available_short'],
|
||||
'BERUFSBEZEICHNUNG': request_obj.job_title or t['not_available_short'],
|
||||
'VERTRAGSBEGINN': request_obj.contract_start,
|
||||
'EMAIL': request_obj.work_email or t['not_available_short'],
|
||||
'REQUESTED_BY_NAME': requester_name,
|
||||
'REQUESTED_BY_EMAIL': requester_email,
|
||||
'SESSION_STATUS': session.get_status_display(),
|
||||
'SESSION_COMPLETED_BY': session.completed_by_name or '-',
|
||||
'SESSION_COMPLETED_AT': session.completed_at or '-',
|
||||
'SESSION_UPDATED_AT': session.updated_at,
|
||||
'SESSION_NOTES': session.notes or t['not_available_short'],
|
||||
'INTRO_SECTIONS': exported_sections,
|
||||
'COMPANY_LEGAL_NAME': company_contact['legal_company_name'] or company_contact['company_name'],
|
||||
'COMPANY_ADDRESS': company_contact['address'] or t['not_available_short'],
|
||||
'COMPANY_IT_CONTACT': company_contact['it_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_HR_CONTACT': company_contact['hr_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_PHONE': company_contact['phone_number'] or t['not_available_short'],
|
||||
}
|
||||
|
||||
html = _render_html(template_path, context)
|
||||
_generate_content_pdf(html, temp_pdf)
|
||||
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
|
||||
|
||||
if temp_pdf.exists():
|
||||
temp_pdf.unlink(missing_ok=True)
|
||||
return output_pdf
|
||||
|
||||
def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path:
|
||||
lang = _normalized_lang(request_obj.preferred_language)
|
||||
t = _pdf_texts(lang)
|
||||
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'offboarding_{request_obj.id}')
|
||||
output_pdf = settings.PDF_OUTPUT_DIR / f'offboarding_letter_{safe_name}.pdf'
|
||||
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_offboarding_{safe_name}.pdf'
|
||||
|
||||
template_path = settings.PDF_TEMPLATES_DIR / 'offboarding_template.html'
|
||||
letterhead_path = get_portal_letterhead_path()
|
||||
company_contact = get_company_contact_copy()
|
||||
latest_onboarding = (
|
||||
OnboardingRequest.objects.filter(work_email=request_obj.work_email)
|
||||
.order_by('-created_at')
|
||||
.first()
|
||||
)
|
||||
has_onboarding_data = latest_onboarding is not None
|
||||
onboarding_hardware = _split_multiline(latest_onboarding.needed_devices) if latest_onboarding else []
|
||||
selected_set = {item.lower() for item in onboarding_hardware}
|
||||
hardware_catalog = [
|
||||
'Laptop',
|
||||
'Docking-Station',
|
||||
'Tastatur und Maus',
|
||||
'Kopfhörer',
|
||||
'Tragetasche',
|
||||
'Monitor',
|
||||
'Schlüssel',
|
||||
'Tischtelefon',
|
||||
]
|
||||
checklist = [{'label': item, 'selected': item.lower() in selected_set} for item in hardware_catalog]
|
||||
extra_selected = [item for item in onboarding_hardware if item.lower() not in {x.lower() for x in hardware_catalog}]
|
||||
for item in extra_selected:
|
||||
checklist.append({'label': item, 'selected': True})
|
||||
|
||||
requester_email = request_obj.requested_by_email or t['not_available_short']
|
||||
requester_name = request_obj.requested_by_name or _resolve_user_display_name(request_obj.requested_by_email) or t['not_available_short']
|
||||
|
||||
context = {
|
||||
'T': t,
|
||||
'PDF_LANG': lang,
|
||||
'PDF_SECTIONS': build_pdf_sections('offboarding', request_obj, lang),
|
||||
'FULL_NAME': request_obj.full_name,
|
||||
'EMAIL': request_obj.work_email,
|
||||
'DEPARTMENT': request_obj.department or t['not_available_short'],
|
||||
'JOB_TITLE': request_obj.job_title or t['not_available_short'],
|
||||
'LAST_WORKING_DAY': request_obj.last_working_day,
|
||||
'NOTES': request_obj.notes or t['not_available_short'],
|
||||
'REQUESTED_BY': requester_email,
|
||||
'REQUESTED_BY_NAME': requester_name,
|
||||
'HAS_ONBOARDING_DATA': has_onboarding_data,
|
||||
'ONBOARDING_HARDWARE': onboarding_hardware,
|
||||
'HARDWARE_CHECKLIST': checklist,
|
||||
'MANUAL_FIELD_SECTIONS': _manual_onboarding_field_sections(),
|
||||
'MANUAL_DEVICES': _chunk_choice_labels(DEVICE_CHOICES),
|
||||
'MANUAL_SOFTWARE': _chunk_choice_labels(SOFTWARE_CHOICES),
|
||||
'MANUAL_ACCESSES': _chunk_choice_labels(ACCESS_CHOICES),
|
||||
'MANUAL_WORKSPACE_GROUPS': _chunk_choice_labels(WORKSPACE_GROUP_CHOICES),
|
||||
'MANUAL_RESOURCES': _chunk_choice_labels(RESOURCE_CHOICES),
|
||||
'MANUAL_EXTRA_HARDWARE': _chunk_choice_labels(HARDWARE_EXTRA_CHOICES),
|
||||
'MANUAL_EXTRA_SOFTWARE': _chunk_choice_labels(SOFTWARE_EXTRA_CHOICES),
|
||||
'COMPANY_LEGAL_NAME': company_contact['legal_company_name'] or company_contact['company_name'],
|
||||
'COMPANY_ADDRESS': company_contact['address'] or t['not_available_short'],
|
||||
'COMPANY_IT_CONTACT': company_contact['it_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_HR_CONTACT': company_contact['hr_contact_email'] or t['not_available_short'],
|
||||
'COMPANY_PHONE': company_contact['phone_number'] or t['not_available_short'],
|
||||
}
|
||||
|
||||
html = _render_html(template_path, context)
|
||||
_generate_content_pdf(html, temp_pdf)
|
||||
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
|
||||
|
||||
if temp_pdf.exists():
|
||||
temp_pdf.unlink(missing_ok=True)
|
||||
return output_pdf
|
||||
@@ -34,6 +34,16 @@ from .backup_ops import (
|
||||
)
|
||||
from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates, get_portal_trial_config, is_trial_expired
|
||||
from . import account_views, admin_config_views, integrations_views, request_views
|
||||
from .admin_section_builders import (
|
||||
build_branding_sections as _build_branding_sections,
|
||||
build_company_config_sections as _build_company_config_sections,
|
||||
)
|
||||
from .admin_user_helpers import (
|
||||
render_user_management as _render_user_management,
|
||||
send_user_access_email as _send_user_access_email,
|
||||
would_remove_last_platform_owner as _would_remove_last_platform_owner,
|
||||
would_remove_last_super_admin as _would_remove_last_super_admin,
|
||||
)
|
||||
from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
|
||||
from .form_builder import (
|
||||
DEFAULT_FIELD_ORDER,
|
||||
@@ -196,141 +206,18 @@ def save_portal_app_registry(request):
|
||||
return admin_config_views.save_portal_app_registry_impl(request, audit_fn=_audit)
|
||||
|
||||
|
||||
def _user_management_rows():
|
||||
user_model = get_user_model()
|
||||
role_order = {
|
||||
ROLE_PLATFORM_OWNER: 0,
|
||||
ROLE_SUPER_ADMIN: 0,
|
||||
'admin': 1,
|
||||
'it_staff': 2,
|
||||
'staff': 3,
|
||||
}
|
||||
rows = []
|
||||
for user in user_model.objects.all().order_by('-is_active', 'username'):
|
||||
role_key = get_user_role_key(user)
|
||||
rows.append(
|
||||
{
|
||||
'user': user,
|
||||
'role_key': role_key,
|
||||
'role_label': str(ROLE_LABELS[role_key]),
|
||||
'role_sort': role_order.get(role_key, 99),
|
||||
'display_name': _display_user_name(user),
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda item: (not item['user'].is_active, item['role_sort'], item['user'].username.lower()))
|
||||
return rows
|
||||
|
||||
|
||||
def _render_user_management(request, create_form=None, status_code: int = 200):
|
||||
recent_user_events = list(
|
||||
AdminAuditLog.objects.select_related('actor')
|
||||
.filter(action__in=['user_created', 'user_updated', 'user_password_reset_sent', 'user_deleted'])
|
||||
.order_by('-created_at', '-id')[:12]
|
||||
)
|
||||
for row in recent_user_events:
|
||||
row.action_label = _audit_action_label(row.action)
|
||||
role_key = (row.details or {}).get('role')
|
||||
row.role_label = str(ROLE_LABELS[role_key]) if role_key in ROLE_LABELS else role_key
|
||||
include_product_owner = get_user_role_key(request.user) == ROLE_PLATFORM_OWNER
|
||||
return render(
|
||||
request,
|
||||
'workflows/user_management.html',
|
||||
{
|
||||
'create_form': create_form or UserManagementCreateForm(include_product_owner=include_product_owner),
|
||||
'rows': _user_management_rows(),
|
||||
'role_choices': [
|
||||
(key, str(ROLE_LABELS[key]))
|
||||
for key in ROLE_GROUP_NAMES
|
||||
if include_product_owner or key != ROLE_PLATFORM_OWNER
|
||||
],
|
||||
'include_product_owner': include_product_owner,
|
||||
'recent_user_events': recent_user_events,
|
||||
},
|
||||
status=status_code,
|
||||
)
|
||||
|
||||
|
||||
def _platform_owner_user_count() -> int:
|
||||
user_model = get_user_model()
|
||||
return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_PLATFORM_OWNER and user.is_active)
|
||||
|
||||
|
||||
def _super_admin_user_count() -> int:
|
||||
user_model = get_user_model()
|
||||
return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_SUPER_ADMIN and user.is_active)
|
||||
|
||||
|
||||
def _would_remove_last_super_admin(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool:
|
||||
if get_user_role_key(user) != ROLE_SUPER_ADMIN or not user.is_active:
|
||||
return False
|
||||
if _super_admin_user_count() > 1:
|
||||
return False
|
||||
if deleting:
|
||||
return True
|
||||
if new_role_key is not None and new_role_key != ROLE_SUPER_ADMIN:
|
||||
return True
|
||||
if new_is_active is not None and not new_is_active:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _would_remove_last_platform_owner(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool:
|
||||
if get_user_role_key(user) != ROLE_PLATFORM_OWNER or not user.is_active:
|
||||
return False
|
||||
if _platform_owner_user_count() > 1:
|
||||
return False
|
||||
if deleting:
|
||||
return True
|
||||
if new_role_key is not None and new_role_key != ROLE_PLATFORM_OWNER:
|
||||
return True
|
||||
if new_is_active is not None and not new_is_active:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _send_user_access_email(request, target_user, *, invitation: bool) -> None:
|
||||
email = (target_user.email or '').strip()
|
||||
if not email:
|
||||
raise ValueError(_('Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt.'))
|
||||
|
||||
uid = urlsafe_base64_encode(force_bytes(target_user.pk))
|
||||
token = default_token_generator.make_token(target_user)
|
||||
reset_path = reverse('password_reset_confirm', kwargs={'uidb64': uid, 'token': token})
|
||||
reset_url = request.build_absolute_uri(reset_path)
|
||||
branding_copy = get_branding_email_copy()
|
||||
|
||||
if invitation:
|
||||
subject = _('Zugangseinladung für %(username)s') % {'username': target_user.username}
|
||||
body = _(
|
||||
'Hallo %(name)s,\n\n'
|
||||
'für Sie wurde ein Benutzerkonto im %(portal_title)s angelegt.\n'
|
||||
'Bitte öffnen Sie den folgenden Link, um Ihr Passwort zu setzen:\n'
|
||||
'%(url)s\n\n'
|
||||
'Wenn Sie diese Einladung nicht erwartet haben, melden Sie sich bitte bei Ihrem Administrator.'
|
||||
) % {
|
||||
'name': _display_user_name(target_user),
|
||||
'portal_title': branding_copy['portal_title'],
|
||||
'url': reset_url,
|
||||
}
|
||||
else:
|
||||
subject = _('Passwort zurücksetzen für %(username)s') % {'username': target_user.username}
|
||||
body = _(
|
||||
'Hallo %(name)s,\n\n'
|
||||
'für Ihr Konto wurde ein Link zum Zurücksetzen des Passworts erstellt.\n'
|
||||
'Bitte öffnen Sie den folgenden Link:\n'
|
||||
'%(url)s\n\n'
|
||||
'Wenn Sie diese Anfrage nicht erwartet haben, können Sie diese E-Mail ignorieren.'
|
||||
) % {
|
||||
'name': _display_user_name(target_user),
|
||||
'url': reset_url,
|
||||
}
|
||||
|
||||
send_system_email(subject=subject, body=body, to=[email])
|
||||
|
||||
|
||||
@_require_capability('manage_users')
|
||||
def user_management_page(request):
|
||||
return admin_config_views.user_management_page_impl(request, render_user_management_fn=_render_user_management)
|
||||
return admin_config_views.user_management_page_impl(
|
||||
request,
|
||||
render_user_management_fn=lambda req, create_form=None, status_code=200: _render_user_management(
|
||||
req,
|
||||
create_form=create_form,
|
||||
status_code=status_code,
|
||||
audit_action_label_fn=_audit_action_label,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@_require_capability('manage_product_branding')
|
||||
@@ -344,70 +231,6 @@ def save_portal_branding(request):
|
||||
return admin_config_views.save_portal_branding_impl(request, audit_fn=_audit, build_branding_sections_fn=_build_branding_sections)
|
||||
|
||||
|
||||
def _build_branding_sections(form, branding):
|
||||
sections = [
|
||||
{
|
||||
'key': 'identity',
|
||||
'title': _('Identität'),
|
||||
'subtitle': _('Titel, Firmenname und zentrale Spracheinstellungen.'),
|
||||
'fields': ['portal_title', 'company_name', 'company_domain', 'default_language', 'login_subtitle'],
|
||||
'field_full': {'login_subtitle'},
|
||||
'hint_map': {
|
||||
'company_domain': _('Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. workdock.de.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'appearance',
|
||||
'title': _('Farben & Erscheinungsbild'),
|
||||
'subtitle': _('Zentrale visuelle Markenwerte und Browser-Icon.'),
|
||||
'fields': ['primary_color', 'secondary_color', 'logo_image', 'favicon_image'],
|
||||
'field_full': set(),
|
||||
'hint_map': {
|
||||
'logo_image': _('Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB.'),
|
||||
'favicon_image': _('Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'communication',
|
||||
'title': _('Kommunikation'),
|
||||
'subtitle': _('Absender, Support und PDF-Branding für ausgehende Kommunikation.'),
|
||||
'fields': ['support_email', 'sender_display_name', 'pdf_letterhead'],
|
||||
'field_full': {'pdf_letterhead'},
|
||||
'hint_map': {
|
||||
'sender_display_name': _('Wird für ausgehende System-E-Mails als Anzeigename verwendet.'),
|
||||
'pdf_letterhead': _('Erlaubtes Format: PDF. Maximal 10 MB.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'legal',
|
||||
'title': _('Footer & Rechtliches'),
|
||||
'subtitle': _('Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell.'),
|
||||
'fields': ['footer_text', 'legal_notice', 'footer_text_en', 'legal_notice_en'],
|
||||
'field_full': {'legal_notice', 'legal_notice_en'},
|
||||
'hint_map': {},
|
||||
},
|
||||
]
|
||||
for section in sections:
|
||||
rows = []
|
||||
for field_name in section['fields']:
|
||||
field = form[field_name]
|
||||
value = getattr(branding, field_name, '') or ''
|
||||
is_file = bool(getattr(field.field.widget, 'input_type', '') == 'file')
|
||||
rows.append(
|
||||
{
|
||||
'name': field_name,
|
||||
'bound_field': field,
|
||||
'label': field.label,
|
||||
'value': value,
|
||||
'is_file': is_file,
|
||||
'is_full': field_name in section.get('field_full', set()),
|
||||
'hint': section.get('hint_map', {}).get(field_name, ''),
|
||||
}
|
||||
)
|
||||
section['rows'] = rows
|
||||
return sections
|
||||
|
||||
|
||||
@_require_capability('manage_company_config')
|
||||
def portal_company_config_page(request):
|
||||
return admin_config_views.portal_company_config_page_impl(request, build_company_config_sections_fn=_build_company_config_sections)
|
||||
@@ -419,50 +242,6 @@ def save_portal_company_config(request):
|
||||
return admin_config_views.save_portal_company_config_impl(request, audit_fn=_audit, build_company_config_sections_fn=_build_company_config_sections)
|
||||
|
||||
|
||||
def _build_company_config_sections(form, company_config):
|
||||
sections = [
|
||||
{
|
||||
'key': 'profile',
|
||||
'title': _('Firmenprofil'),
|
||||
'subtitle': _('Rechtlicher Name und zentrale Stammdaten der Firma.'),
|
||||
'fields': ['legal_company_name', 'phone_number', 'website_url', 'country'],
|
||||
},
|
||||
{
|
||||
'key': 'address',
|
||||
'title': _('Adresse & Register'),
|
||||
'subtitle': _('Anschrift sowie optionale Register- und Steuerangaben.'),
|
||||
'fields': ['street_address', 'postal_code', 'city', 'registration_number', 'vat_id'],
|
||||
},
|
||||
{
|
||||
'key': 'contacts',
|
||||
'title': _('Kontaktpunkte'),
|
||||
'subtitle': _('Zentrale Ansprechpartner für HR, IT und Operations.'),
|
||||
'fields': ['hr_contact_email', 'it_contact_email', 'operations_contact_email'],
|
||||
},
|
||||
{
|
||||
'key': 'public',
|
||||
'title': _('Recht & Öffentlichkeit'),
|
||||
'subtitle': _('Öffentliche Links für Website, Impressum und Datenschutz.'),
|
||||
'fields': ['imprint_url', 'privacy_url'],
|
||||
'hint': _('Diese Links können später im Portal-Footer oder in öffentlichen Seiten verwendet werden.'),
|
||||
},
|
||||
]
|
||||
for section in sections:
|
||||
rows = []
|
||||
for field_name in section['fields']:
|
||||
field = form[field_name]
|
||||
rows.append(
|
||||
{
|
||||
'name': field_name,
|
||||
'bound_field': field,
|
||||
'label': field.label,
|
||||
'value': getattr(company_config, field_name, '') or '',
|
||||
}
|
||||
)
|
||||
section['rows'] = rows
|
||||
return sections
|
||||
|
||||
|
||||
@_require_capability('manage_trial_lifecycle')
|
||||
def portal_trial_config_page(request):
|
||||
return admin_config_views.portal_trial_config_page_impl(request)
|
||||
@@ -479,8 +258,19 @@ def save_portal_trial_config(request):
|
||||
def create_user_from_admin(request):
|
||||
return admin_config_views.create_user_from_admin_impl(
|
||||
request,
|
||||
render_user_management_fn=_render_user_management,
|
||||
send_user_access_email_fn=_send_user_access_email,
|
||||
render_user_management_fn=lambda req, create_form=None, status_code=200: _render_user_management(
|
||||
req,
|
||||
create_form=create_form,
|
||||
status_code=status_code,
|
||||
audit_action_label_fn=_audit_action_label,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
send_user_access_email_fn=lambda req, target_user, invitation: _send_user_access_email(
|
||||
req,
|
||||
target_user,
|
||||
invitation=invitation,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
audit_fn=_audit,
|
||||
display_user_name_fn=_display_user_name,
|
||||
)
|
||||
@@ -505,7 +295,12 @@ def send_password_reset_from_admin(request, user_id: int):
|
||||
return admin_config_views.send_password_reset_from_admin_impl(
|
||||
request,
|
||||
user_id,
|
||||
send_user_access_email_fn=_send_user_access_email,
|
||||
send_user_access_email_fn=lambda req, target_user, invitation: _send_user_access_email(
|
||||
req,
|
||||
target_user,
|
||||
invitation=invitation,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
audit_fn=_audit,
|
||||
display_user_name_fn=_display_user_name,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user