import json from pathlib import Path from tempfile import NamedTemporaryFile from celery import current_app from django.conf import settings from django.contrib import messages from django.http import JsonResponse from django.shortcuts import redirect, render from django.utils import timezone from django.utils.translation import gettext as _ from .branding import get_default_notification_templates from .form_builder import DEFAULT_FIELD_ORDER, get_default_page_map, get_section_order from .models import FormCustomFieldConfig, FormFieldConfig, NotificationRule, NotificationTemplate, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig from .notifications import notify_user from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud from .tasks import send_scheduled_welcome_email from .emailing import send_system_email def integrations_setup_page_impl(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') kind = (request.GET.get('kind') or 'nextcloud').strip().lower() if kind not in {'nextcloud', 'mail', 'emails', 'rules', 'backup'}: kind = 'nextcloud' templates = list(NotificationTemplate.objects.all().order_by('key')) system_email_config = ( SystemEmailConfig.objects.filter(is_active=True).order_by('-updated_at').first() or SystemEmailConfig.objects.filter(name='Default SMTP').first() ) return render( request, 'workflows/integrations_setup.html', { 'workflow_config': config, 'system_email_config': system_email_config, 'nextcloud_enabled': is_nextcloud_enabled(), 'email_test_mode': is_email_test_mode(), 'kind': kind, 'templates': templates, 'notification_rules': NotificationRule.objects.all().order_by('event_type', 'sort_order', 'id'), 'rule_event_choices': NotificationRule.EVENT_CHOICES, 'rule_operator_choices': NotificationRule.OPERATOR_CHOICES, 'template_choices': NotificationTemplate.TEMPLATE_CHOICES, 'remote_backup_target_choices': WorkflowConfig.REMOTE_BACKUP_TARGET_CHOICES, }, ) def welcome_emails_page_impl(request): rows = ScheduledWelcomeEmail.objects.select_related('onboarding_request').order_by('-send_at', '-id')[:200] config, _ = WorkflowConfig.objects.get_or_create(name='Default') welcome_template = NotificationTemplate.objects.filter(key='onboarding_welcome').first() default_welcome = get_default_notification_templates().get('onboarding_welcome', {}) default_subject = (default_welcome.get('subject') or '').strip() default_body = (default_welcome.get('body') or '').strip() default_subject_en = (default_welcome.get('subject_en') or '').strip() default_body_en = (default_welcome.get('body_en') or '').strip() subject_value = (welcome_template.subject_template if welcome_template else '').strip() or default_subject body_value = (welcome_template.body_template if welcome_template else '').strip() or default_body subject_value_en = (welcome_template.subject_template_en if welcome_template else '').strip() or default_subject_en body_value_en = (welcome_template.body_template_en if welcome_template else '').strip() or default_body_en return render( request, 'workflows/welcome_emails.html', { 'rows': rows, 'workflow_config': config, 'welcome_template': welcome_template, 'welcome_subject_value': subject_value, 'welcome_body_value': body_value, 'welcome_subject_value_en': subject_value_en, 'welcome_body_value_en': body_value_en, 'welcome_keywords': ['{{ FULL_NAME }}', '{{ VORNAME }}', '{{ NACHNAME }}', '{{ DEPARTMENT }}', '{{ CONTRACT_START }}', '{{ EMAIL }}', '{{ REQUESTED_BY }}'], }, ) def trigger_welcome_email_now_impl(request, schedule_id: int, *, audit_fn): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() if not scheduled: messages.error(request, f'Geplanter Welcome-Eintrag #{schedule_id} nicht gefunden.') return redirect('welcome_emails_page') if scheduled.status == 'cancelled': messages.error(request, f'Welcome E-Mail #{schedule_id} ist abgebrochen und kann nicht gesendet werden.') return redirect('welcome_emails_page') async_result = send_scheduled_welcome_email.delay(scheduled.id, True) scheduled.celery_task_id = async_result.id or scheduled.celery_task_id scheduled.status = 'scheduled' scheduled.last_error = '' scheduled.save(update_fields=['celery_task_id', 'status', 'last_error', 'updated_at']) audit_fn(request, 'welcome_email_triggered_now', target_type='welcome_email', target_id=scheduled.id, target_label=scheduled.recipient_email) messages.success(request, f'Welcome E-Mail #{schedule_id} wurde sofort angestoßen.') return redirect('welcome_emails_page') def save_welcome_email_settings_impl(request, *, audit_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') try: delay_days = int(request.POST.get('welcome_email_delay_days', config.welcome_email_delay_days or 5)) except ValueError: messages.error(request, 'Ungültige Zahl bei der Welcome-Verzögerung.') return redirect('welcome_emails_page') config.welcome_email_delay_days = max(0, delay_days) config.welcome_sender_email = request.POST.get('welcome_sender_email', '').strip() config.welcome_include_pdf = request.POST.get('welcome_include_pdf') == 'on' config.save(update_fields=['welcome_email_delay_days', 'welcome_sender_email', 'welcome_include_pdf']) subject = request.POST.get('welcome_subject') body = request.POST.get('welcome_body') subject_en = request.POST.get('welcome_subject_en') body_en = request.POST.get('welcome_body_en') if subject is not None or body is not None or subject_en is not None or body_en is not None: default_welcome = get_default_notification_templates().get('onboarding_welcome', {}) default_subject = (default_welcome.get('subject') or '').strip() default_body = (default_welcome.get('body') or '').strip() default_subject_en = (default_welcome.get('subject_en') or '').strip() default_body_en = (default_welcome.get('body_en') or '').strip() subject_clean = (subject or '').strip() or default_subject body_clean = (body or '').strip() or default_body subject_clean_en = (subject_en or '').strip() or default_subject_en body_clean_en = (body_en or '').strip() or default_body_en template, _ = NotificationTemplate.objects.get_or_create( key='onboarding_welcome', defaults={ 'subject_template': subject_clean, 'body_template': body_clean, 'subject_template_en': subject_clean_en, 'body_template_en': body_clean_en, 'is_active': True, }, ) changes = [] if template.subject_template != subject_clean: template.subject_template = subject_clean changes.append('subject_template') if template.body_template != body_clean: template.body_template = body_clean changes.append('body_template') if template.subject_template_en != subject_clean_en: template.subject_template_en = subject_clean_en changes.append('subject_template_en') if template.body_template_en != body_clean_en: template.body_template_en = body_clean_en changes.append('body_template_en') if not template.is_active: template.is_active = True changes.append('is_active') if changes: template.save(update_fields=changes) audit_fn( request, 'welcome_email_settings_saved', target_type='welcome_email_settings', target_label='onboarding_welcome', details={ 'delay_days': config.welcome_email_delay_days, 'sender_email': config.welcome_sender_email, 'include_pdf': config.welcome_include_pdf, }, ) messages.success(request, 'Welcome-E-Mail Einstellungen wurden gespeichert.') return redirect('welcome_emails_page') def _revoke_celery_task(task_id: str) -> None: if not task_id: return try: current_app.control.revoke(task_id, terminate=False) except Exception: return def _parse_selected_schedule_ids(raw: str) -> list[int]: if not raw: return [] parsed: list[int] = [] seen: set[int] = set() for token in raw.split(','): token = token.strip() if not token: continue try: schedule_id = int(token) except ValueError: continue if schedule_id in seen: continue seen.add(schedule_id) parsed.append(schedule_id) return parsed def bulk_welcome_email_action_impl(request, *, audit_fn): action = (request.POST.get('bulk_action') or '').strip().lower() selected_ids = _parse_selected_schedule_ids(request.POST.get('selected_ids', '')) if action not in {'pause', 'send_now', 'delete'}: messages.error(request, 'Ungültige Bulk-Aktion.') return redirect('welcome_emails_page') if not selected_ids: messages.warning(request, 'Keine Welcome-Einträge ausgewählt.') return redirect('welcome_emails_page') rows = list(ScheduledWelcomeEmail.objects.filter(id__in=selected_ids).order_by('id')) if not rows: messages.warning(request, 'Keine passenden Welcome-Einträge gefunden.') return redirect('welcome_emails_page') success_count = 0 skipped_count = 0 for scheduled in rows: if action == 'pause': if scheduled.status in {'sent', 'cancelled'}: skipped_count += 1 continue _revoke_celery_task(scheduled.celery_task_id) scheduled.status = 'paused' scheduled.save(update_fields=['status', 'updated_at']) success_count += 1 continue if action == 'send_now': if scheduled.status == 'cancelled': skipped_count += 1 continue async_result = send_scheduled_welcome_email.delay(scheduled.id, True) scheduled.celery_task_id = async_result.id or scheduled.celery_task_id scheduled.status = 'scheduled' scheduled.last_error = '' scheduled.save(update_fields=['celery_task_id', 'status', 'last_error', 'updated_at']) success_count += 1 continue if action == 'delete': if scheduled.status == 'scheduled': _revoke_celery_task(scheduled.celery_task_id) scheduled.delete() success_count += 1 action_label = { 'pause': 'pausiert', 'send_now': 'sofort angestoßen', 'delete': 'gelöscht', }[action] if success_count: audit_fn( request, 'welcome_email_bulk_action', target_type='welcome_email', target_label=action, details={'selected_ids': selected_ids, 'success_count': success_count, 'skipped_count': skipped_count}, ) messages.success(request, f'{success_count} Welcome-Eintrag/Einträge {action_label}.') if skipped_count: messages.warning(request, f'{skipped_count} Eintrag/Einträge wurden übersprungen (Status nicht geeignet).') return redirect('welcome_emails_page') def pause_welcome_email_impl(request, schedule_id: int, *, audit_fn): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() if not scheduled: messages.error(request, f'Geplanter Welcome-Eintrag #{schedule_id} nicht gefunden.') return redirect('welcome_emails_page') if scheduled.status in {'sent', 'cancelled'}: messages.error(request, f'Welcome E-Mail #{schedule_id} kann nicht pausiert werden.') return redirect('welcome_emails_page') _revoke_celery_task(scheduled.celery_task_id) scheduled.status = 'paused' scheduled.save(update_fields=['status', 'updated_at']) audit_fn(request, 'welcome_email_paused', target_type='welcome_email', target_id=scheduled.id, target_label=scheduled.recipient_email) messages.success(request, f'Welcome E-Mail #{schedule_id} wurde pausiert.') return redirect('welcome_emails_page') def resume_welcome_email_impl(request, schedule_id: int, *, audit_fn): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() if not scheduled: messages.error(request, f'Geplanter Welcome-Eintrag #{schedule_id} nicht gefunden.') return redirect('welcome_emails_page') if scheduled.status != 'paused': messages.error(request, f'Welcome E-Mail #{schedule_id} ist nicht pausiert.') return redirect('welcome_emails_page') eta = scheduled.send_at if timezone.now() < scheduled.send_at else None async_result = send_scheduled_welcome_email.apply_async(args=[scheduled.id], eta=eta) scheduled.celery_task_id = async_result.id or scheduled.celery_task_id scheduled.status = 'scheduled' scheduled.last_error = '' scheduled.save(update_fields=['celery_task_id', 'status', 'last_error', 'updated_at']) audit_fn(request, 'welcome_email_resumed', target_type='welcome_email', target_id=scheduled.id, target_label=scheduled.recipient_email) messages.success(request, f'Welcome E-Mail #{schedule_id} wurde fortgesetzt.') return redirect('welcome_emails_page') def cancel_welcome_email_impl(request, schedule_id: int, *, audit_fn): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() if not scheduled: messages.error(request, f'Geplanter Welcome-Eintrag #{schedule_id} nicht gefunden.') return redirect('welcome_emails_page') if scheduled.status == 'sent': messages.error(request, f'Welcome E-Mail #{schedule_id} wurde bereits gesendet.') return redirect('welcome_emails_page') _revoke_celery_task(scheduled.celery_task_id) scheduled.status = 'cancelled' scheduled.last_error = '' scheduled.save(update_fields=['status', 'last_error', 'updated_at']) audit_fn(request, 'welcome_email_cancelled', target_type='welcome_email', target_id=scheduled.id, target_label=scheduled.recipient_email) messages.success(request, f'Welcome E-Mail #{schedule_id} wurde abgebrochen.') return redirect('welcome_emails_page') def form_builder_save_order_impl(request, *, audit_fn): try: payload = json.loads(request.body.decode('utf-8')) except (json.JSONDecodeError, UnicodeDecodeError): return JsonResponse({'ok': False, 'error': _('Ungültige JSON-Daten.')}, status=400) form_type = payload.get('form_type') if form_type not in DEFAULT_FIELD_ORDER: return JsonResponse({'ok': False, 'error': _('Ungültiger Formulartyp.')}, status=400) default_page_map = get_default_page_map(form_type) columns = payload.get('columns') if not isinstance(columns, dict): return JsonResponse({'ok': False, 'error': _('Spalten-Daten fehlen.')}, status=400) configs = list(FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name')) custom_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_key')) allowed_names = {cfg.field_name for cfg in configs} | {f'custom__{cfg.field_key}' for cfg in custom_configs} seen = set() allowed_columns = get_section_order(form_type) fallback_section = allowed_columns[-1] if allowed_columns else '' name_to_cfg = {cfg.field_name: cfg for cfg in configs} custom_name_to_cfg = {f'custom__{cfg.field_key}': cfg for cfg in custom_configs} sort_order = 0 for column_key in allowed_columns: names = columns.get(column_key, []) if not isinstance(names, list): return JsonResponse({'ok': False, 'error': _('Ungültige Spalte: %(column)s') % {'column': column_key}}, status=400) for name in names: if not isinstance(name, str): continue if name not in allowed_names or name in seen: continue seen.add(name) if name in name_to_cfg: cfg = name_to_cfg[name] cfg.sort_order = sort_order cfg.page_key = column_key else: cfg = custom_name_to_cfg[name] cfg.sort_order = sort_order cfg.section_key = column_key sort_order += 1 missing = [cfg.field_name for cfg in configs if cfg.field_name not in seen] for name in missing: cfg = name_to_cfg[name] cfg.sort_order = sort_order sort_order += 1 cfg.page_key = cfg.page_key or default_page_map.get(name, fallback_section) missing_custom = [name for name in custom_name_to_cfg.keys() if name not in seen] for name in missing_custom: cfg = custom_name_to_cfg[name] cfg.sort_order = sort_order sort_order += 1 cfg.section_key = cfg.section_key or fallback_section FormFieldConfig.objects.bulk_update(configs, ['sort_order', 'page_key']) if custom_configs: FormCustomFieldConfig.objects.bulk_update(custom_configs, ['sort_order', 'section_key']) saved_count = len(configs) + len(custom_configs) audit_fn(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': saved_count}) return JsonResponse({'ok': True, 'saved_count': saved_count}) def send_test_email_impl(request, *, audit_fn, redirect_back_fn): mode = 'TEST_MODE_ON' if is_email_test_mode() else 'TEST_MODE_OFF' redirect_email = get_email_test_redirect() try: send_system_email( subject=f'SMTP test from onboarding/offboarding v2 ({mode})', body=( 'This is a test email. If you see this, SMTP is configured correctly.\n' f'EMAIL_TEST_MODE={is_email_test_mode()}\n' f'EMAIL_TEST_REDIRECT={redirect_email}\n' ), to=[settings.TEST_NOTIFICATION_EMAIL], ) audit_fn(request, 'smtp_test_sent', target_type='system_email', target_label=settings.TEST_NOTIFICATION_EMAIL, details={'email_test_mode': is_email_test_mode()}) notify_user( user=request.user, title=_('SMTP-Test erfolgreich'), body=_('Die SMTP-Testmail wurde erfolgreich gesendet.'), level=UserNotification.LEVEL_SUCCESS, link_url='/admin-tools/integrations/', event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS, ) messages.success(request, f'SMTP-Testmail wurde gesendet ({mode}).') except Exception as exc: notify_user( user=request.user, title=_('SMTP-Test fehlgeschlagen'), body=str(exc), level=UserNotification.LEVEL_ERROR, link_url='/admin-tools/integrations/', event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS, ) messages.error(request, _('SMTP-Testmail konnte nicht gesendet werden: %(error)s') % {'error': exc}) return redirect_back_fn(request, 'home') def nextcloud_test_upload_impl(request, *, audit_fn, redirect_back_fn): filename = f"nextcloud_test_{timezone.now().strftime('%Y%m%d_%H%M%S')}.txt" content = ( "Nextcloud test upload from onboarding/offboarding system.\n" f"Time: {timezone.now().isoformat()}\n" f"User: {request.user.username}\n" ) temp_path = None try: with NamedTemporaryFile('w', suffix='.txt', delete=False, encoding='utf-8') as tf: tf.write(content) temp_path = Path(tf.name) ok = upload_to_nextcloud(temp_path, filename) if ok: audit_fn(request, 'nextcloud_test_upload', target_type='nextcloud', target_label=filename, details={'result': 'success'}) notify_user( user=request.user, title=_('Nextcloud-Test erfolgreich'), body=_('Der Testupload nach Nextcloud war erfolgreich.'), level=UserNotification.LEVEL_SUCCESS, link_url='/admin-tools/integrations/', event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS, ) messages.success(request, f'Nextcloud-Testupload erfolgreich: {filename}') else: audit_fn(request, 'nextcloud_test_upload', target_type='nextcloud', target_label=filename, details={'result': 'error'}) notify_user( user=request.user, title=_('Nextcloud-Test fehlgeschlagen'), body=_('Der Testupload nach Nextcloud ist fehlgeschlagen.'), level=UserNotification.LEVEL_ERROR, link_url='/admin-tools/integrations/', event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS, ) messages.error(request, 'Nextcloud-Testupload fehlgeschlagen. Bitte Konfiguration prüfen.') except Exception as exc: notify_user( user=request.user, title=_('Nextcloud-Test fehlgeschlagen'), body=str(exc), level=UserNotification.LEVEL_ERROR, link_url='/admin-tools/integrations/', event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS, ) messages.error(request, f'Nextcloud-Testupload fehlgeschlagen: {exc}') finally: if temp_path and temp_path.exists(): temp_path.unlink(missing_ok=True) return redirect_back_fn(request, 'home') def toggle_nextcloud_enabled_impl(request, *, audit_fn, redirect_back_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') currently_enabled = is_nextcloud_enabled() config.nextcloud_enabled_override = not currently_enabled config.save(update_fields=['nextcloud_enabled_override']) audit_fn(request, 'nextcloud_mode_toggled', target_type='workflow_config', target_label='nextcloud', details={'enabled': config.nextcloud_enabled_override}) state = 'aktiviert' if config.nextcloud_enabled_override else 'deaktiviert' messages.success(request, f'Nextcloud Upload wurde {state}.') return redirect_back_fn(request, 'home') def toggle_email_mode_impl(request, *, audit_fn, redirect_back_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') currently_test_mode = is_email_test_mode() config.email_test_mode_override = not currently_test_mode config.save(update_fields=['email_test_mode_override']) audit_fn(request, 'email_mode_toggled', target_type='workflow_config', target_label='email_mode', details={'test_mode': config.email_test_mode_override}) state = 'Testmodus (Umleitung)' if config.email_test_mode_override else 'Produktionsmodus' messages.success(request, f'E-Mail-Modus wurde auf {state} gesetzt.') return redirect_back_fn(request, 'home') def save_integrations_settings_impl(request, *, audit_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') try: sync_interval = int(request.POST.get('sync_interval_seconds', config.sync_interval_seconds or 60)) smtp_port = int(request.POST.get('smtp_port', config.smtp_port or 465)) except ValueError: messages.error(request, 'Ungültige Zahl bei Sync-Intervall oder SMTP Port.') return redirect('home') config.nextcloud_base_url_override = request.POST.get('nextcloud_base_url_override', '').strip() config.nextcloud_username_override = request.POST.get('nextcloud_username_override', '').strip() config.nextcloud_directory_override = request.POST.get('nextcloud_directory_override', '').strip() config.sync_interval_seconds = max(10, sync_interval) config.imap_server = request.POST.get('imap_server', '').strip() config.mailbox = request.POST.get('mailbox', '').strip() or 'INBOX' config.smtp_server = request.POST.get('smtp_server', '').strip() config.smtp_port = max(1, smtp_port) config.email_account = request.POST.get('email_account', '').strip() config.smtp_use_ssl = request.POST.get('smtp_use_ssl') == 'on' config.smtp_use_tls = request.POST.get('smtp_use_tls') == 'on' nextcloud_password = request.POST.get('nextcloud_password_override', '').strip() if nextcloud_password: config.nextcloud_password_override = nextcloud_password email_password = request.POST.get('email_password', '').strip() if email_password: config.email_password = email_password config.save() audit_fn(request, 'integrations_saved', target_type='workflow_config', target_label='all_integrations') messages.success(request, 'Integrations-Einstellungen wurden gespeichert.') return redirect('home') def save_nextcloud_settings_impl(request, *, audit_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') try: sync_interval = int(request.POST.get('sync_interval_seconds', config.sync_interval_seconds or 60)) except ValueError: messages.error(request, 'Ungültige Zahl beim Sync-Intervall.') return redirect('home') config.nextcloud_base_url_override = request.POST.get('nextcloud_base_url_override', '').strip() config.nextcloud_username_override = request.POST.get('nextcloud_username_override', '').strip() config.nextcloud_directory_override = request.POST.get('nextcloud_directory_override', '').strip() config.sync_interval_seconds = max(10, sync_interval) nextcloud_password = request.POST.get('nextcloud_password_override', '').strip() if nextcloud_password: config.nextcloud_password_override = nextcloud_password config.save() audit_fn(request, 'nextcloud_settings_saved', target_type='workflow_config', target_label='nextcloud') messages.success(request, 'Nextcloud-Einstellungen wurden gespeichert.') return redirect('/admin-tools/integrations/?kind=nextcloud') def save_workflow_rules_impl(request, *, audit_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') try: handover_lead_days = int( request.POST.get( 'device_handover_lead_days', config.device_handover_lead_days or 5, ) ) except ValueError: messages.error(request, 'Ungültige Zahl beim Hardware-Vorlauf.') return redirect('/admin-tools/integrations/?kind=rules') config.device_handover_lead_days = max(0, handover_lead_days) config.save(update_fields=['device_handover_lead_days']) audit_fn( request, 'workflow_rules_saved', target_type='workflow_config', target_label='workflow_rules', details={ 'device_handover_lead_days': config.device_handover_lead_days, }, ) messages.success(request, 'Workflow-Regeln wurden gespeichert.') return redirect('/admin-tools/integrations/?kind=rules') def save_backup_settings_impl(request, *, audit_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') target_type = (request.POST.get('remote_backup_target_type') or config.remote_backup_target_type or 'nextcloud').strip().lower() if target_type not in {choice for choice, _ in WorkflowConfig.REMOTE_BACKUP_TARGET_CHOICES}: target_type = 'nextcloud' remote_backup_enabled = request.POST.get('remote_backup_enabled') == 'on' remote_backup_nextcloud_directory = request.POST.get('remote_backup_nextcloud_directory', '').strip() primary_nextcloud_directory = ( (config.nextcloud_directory_override or '').strip() or settings.NEXTCLOUD_DIRECTORY.strip() ).strip('/') if remote_backup_enabled and target_type == 'nextcloud': if not remote_backup_nextcloud_directory: messages.error(request, 'Bitte ein separates Nextcloud Backup-Verzeichnis angeben.') return redirect('/admin-tools/integrations/?kind=backup') if remote_backup_nextcloud_directory.strip('/') == primary_nextcloud_directory: messages.error(request, 'Das Backup-Verzeichnis muss vom normalen Nextcloud Dokumentenordner getrennt sein.') return redirect('/admin-tools/integrations/?kind=backup') config.remote_backup_enabled = remote_backup_enabled config.remote_backup_target_type = target_type config.remote_backup_nextcloud_directory = remote_backup_nextcloud_directory config.remote_backup_s3_bucket = request.POST.get('remote_backup_s3_bucket', '').strip() config.remote_backup_nfs_path = request.POST.get('remote_backup_nfs_path', '').strip() config.save( update_fields=[ 'device_handover_lead_days', 'remote_backup_enabled', 'remote_backup_target_type', 'remote_backup_nextcloud_directory', 'remote_backup_s3_bucket', 'remote_backup_nfs_path', ] ) audit_fn( request, 'backup_settings_saved', target_type='workflow_config', target_label='backup_settings', details={ 'remote_backup_enabled': config.remote_backup_enabled, 'remote_backup_target_type': config.remote_backup_target_type, 'remote_backup_nextcloud_directory': config.remote_backup_nextcloud_directory, }, ) messages.success(request, 'Backup-Einstellungen wurden gespeichert.') return redirect('/admin-tools/integrations/?kind=backup') def save_mail_settings_impl(request, *, audit_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') try: smtp_port = int(request.POST.get('smtp_port', config.smtp_port or 465)) except ValueError: messages.error(request, 'Ungültige Zahl beim SMTP Port.') return redirect('home') config.imap_server = request.POST.get('imap_server', '').strip() config.mailbox = request.POST.get('mailbox', '').strip() or 'INBOX' config.smtp_server = request.POST.get('smtp_server', '').strip() config.smtp_port = max(1, smtp_port) config.email_account = request.POST.get('email_account', '').strip() config.smtp_use_ssl = request.POST.get('smtp_use_ssl') == 'on' config.smtp_use_tls = request.POST.get('smtp_use_tls') == 'on' email_password = request.POST.get('email_password', '').strip() if email_password: config.email_password = email_password config.save() smtp_cfg, _ = SystemEmailConfig.objects.get_or_create(name='Default SMTP') SystemEmailConfig.objects.exclude(id=smtp_cfg.id).update(is_active=False) smtp_cfg.is_active = True smtp_cfg.host = config.smtp_server smtp_cfg.port = config.smtp_port smtp_cfg.username = config.email_account if email_password: smtp_cfg.password = email_password smtp_cfg.use_ssl = config.smtp_use_ssl smtp_cfg.use_tls = config.smtp_use_tls smtp_cfg.from_email = request.POST.get('from_email', '').strip() smtp_cfg.save() audit_fn(request, 'mail_settings_saved', target_type='workflow_config', target_label='mail') messages.success(request, 'Mail-Einstellungen wurden gespeichert.') return redirect('/admin-tools/integrations/?kind=mail') def save_email_routing_settings_impl(request, *, audit_fn): config, _ = WorkflowConfig.objects.get_or_create(name='Default') config.it_onboarding_email = request.POST.get('it_onboarding_email', '').strip() config.general_info_email = request.POST.get('general_info_email', '').strip() config.business_card_email = request.POST.get('business_card_email', '').strip() config.hr_works_email = request.POST.get('hr_works_email', '').strip() config.key_notification_email = request.POST.get('key_notification_email', '').strip() config.save( update_fields=[ 'it_onboarding_email', 'general_info_email', 'business_card_email', 'hr_works_email', 'key_notification_email', ] ) known_keys = {k for k, _ in NotificationTemplate.TEMPLATE_CHOICES} for key in known_keys: subject = request.POST.get(f'subject_{key}') body = request.POST.get(f'body_{key}') subject_en = request.POST.get(f'subject_en_{key}') body_en = request.POST.get(f'body_en_{key}') if subject is None and body is None and subject_en is None and body_en is None: continue subject = (subject or '').strip() body = (body or '').strip() subject_en = (subject_en or '').strip() body_en = (body_en or '').strip() if not subject and not body and not subject_en and not body_en: continue obj, _ = NotificationTemplate.objects.get_or_create( key=key, defaults={ 'subject_template': subject or f'[{key}]', 'body_template': body or '-', 'subject_template_en': subject_en, 'body_template_en': body_en, 'is_active': True, }, ) changed = [] if subject and obj.subject_template != subject: obj.subject_template = subject changed.append('subject_template') if body and obj.body_template != body: obj.body_template = body changed.append('body_template') if obj.subject_template_en != subject_en: obj.subject_template_en = subject_en changed.append('subject_template_en') if obj.body_template_en != body_en: obj.body_template_en = body_en changed.append('body_template_en') if not obj.is_active: obj.is_active = True changed.append('is_active') if changed: obj.save(update_fields=changed) audit_fn(request, 'email_routing_saved', target_type='workflow_config', target_label='email_routing') messages.success(request, 'E-Mail Routing und Vorlagen wurden gespeichert.') return redirect('/admin-tools/integrations/?kind=emails') def save_notification_rules_impl(request, *, audit_fn): rule_ids = request.POST.getlist('rule_ids') for position, raw_id in enumerate(rule_ids): rule = NotificationRule.objects.filter(id=raw_id).first() if not rule: continue if request.POST.get(f'delete_{rule.id}') == 'on': rule.delete() continue rule.name = request.POST.get(f'name_{rule.id}', '').strip() or rule.name event_type = request.POST.get(f'event_type_{rule.id}', '').strip() if event_type in {'onboarding', 'offboarding'}: rule.event_type = event_type operator = request.POST.get(f'operator_{rule.id}', '').strip() if operator in {x[0] for x in NotificationRule.OPERATOR_CHOICES}: rule.operator = operator rule.field_name = request.POST.get(f'field_name_{rule.id}', '').strip() rule.expected_value = request.POST.get(f'expected_value_{rule.id}', '').strip() rule.recipients = request.POST.get(f'recipients_{rule.id}', '').strip() rule.template_key = request.POST.get(f'template_key_{rule.id}', '').strip() rule.custom_subject = request.POST.get(f'custom_subject_{rule.id}', '').strip() rule.custom_body = request.POST.get(f'custom_body_{rule.id}', '').strip() rule.custom_subject_en = request.POST.get(f'custom_subject_en_{rule.id}', '').strip() rule.custom_body_en = request.POST.get(f'custom_body_en_{rule.id}', '').strip() rule.include_pdf_attachment = request.POST.get(f'include_pdf_{rule.id}') == 'on' rule.is_active = request.POST.get(f'active_{rule.id}') == 'on' rule.sort_order = position rule.save() new_name = request.POST.get('new_name', '').strip() new_recipients = request.POST.get('new_recipients', '').strip() if new_name and new_recipients: new_event = request.POST.get('new_event_type', 'onboarding').strip() if new_event not in {'onboarding', 'offboarding'}: new_event = 'onboarding' new_operator = request.POST.get('new_operator', 'always').strip() if new_operator not in {x[0] for x in NotificationRule.OPERATOR_CHOICES}: new_operator = 'always' NotificationRule.objects.create( name=new_name, event_type=new_event, field_name=request.POST.get('new_field_name', '').strip(), operator=new_operator, expected_value=request.POST.get('new_expected_value', '').strip(), recipients=new_recipients, template_key=request.POST.get('new_template_key', '').strip(), custom_subject=request.POST.get('new_custom_subject', '').strip(), custom_body=request.POST.get('new_custom_body', '').strip(), custom_subject_en=request.POST.get('new_custom_subject_en', '').strip(), custom_body_en=request.POST.get('new_custom_body_en', '').strip(), include_pdf_attachment=request.POST.get('new_include_pdf') == 'on', is_active=True, sort_order=NotificationRule.objects.filter(event_type=new_event).count() + 1, ) audit_fn(request, 'notification_rules_saved', target_type='notification_rule') messages.success(request, 'Benachrichtigungsregeln wurden gespeichert.') return redirect('/admin-tools/integrations/?kind=emails')