snapshot: modularize integrations and builder order flows
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.utils.translation import get_language, gettext as _
|
from django.utils.translation import get_language, gettext as _
|
||||||
|
|
||||||
from .forms import OffboardingRequestForm, OnboardingRequestForm
|
from .forms import OffboardingRequestForm, OnboardingRequestForm
|
||||||
from .form_builder import (
|
from .form_builder import (
|
||||||
DEFAULT_FIELD_ORDER,
|
DEFAULT_FIELD_ORDER,
|
||||||
@@ -27,7 +32,6 @@ from .models import (
|
|||||||
FormOption,
|
FormOption,
|
||||||
)
|
)
|
||||||
from .roles import ROLE_PLATFORM_OWNER, get_user_role_key
|
from .roles import ROLE_PLATFORM_OWNER, get_user_role_key
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def form_builder_page_impl(
|
def form_builder_page_impl(
|
||||||
@@ -482,6 +486,7 @@ def form_builder_page_impl(
|
|||||||
if form_type == 'onboarding'
|
if form_type == 'onboarding'
|
||||||
else OffboardingRequestForm.base_fields.keys()
|
else OffboardingRequestForm.base_fields.keys()
|
||||||
)
|
)
|
||||||
|
|
||||||
for name in existing_names:
|
for name in existing_names:
|
||||||
if name not in default_names:
|
if name not in default_names:
|
||||||
default_names.append(name)
|
default_names.append(name)
|
||||||
@@ -891,3 +896,73 @@ def form_builder_page_impl(
|
|||||||
'can_override_locked_builder_rules': can_override_locked_builder_rules,
|
'can_override_locked_builder_rules': can_override_locked_builder_rules,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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})
|
||||||
|
|||||||
459
backend/workflows/integration_admin_views.py
Normal file
459
backend/workflows/integration_admin_views.py
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from .models import NotificationRule, NotificationTemplate, 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 .branding import get_default_notification_templates
|
||||||
|
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 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')
|
||||||
@@ -1,783 +1,45 @@
|
|||||||
import json
|
from .integration_admin_views import (
|
||||||
from pathlib import Path
|
integrations_setup_page_impl,
|
||||||
from tempfile import NamedTemporaryFile
|
nextcloud_test_upload_impl,
|
||||||
|
save_backup_settings_impl,
|
||||||
from celery import current_app
|
save_email_routing_settings_impl,
|
||||||
from django.conf import settings
|
save_integrations_settings_impl,
|
||||||
from django.contrib import messages
|
save_mail_settings_impl,
|
||||||
from django.http import JsonResponse
|
save_nextcloud_settings_impl,
|
||||||
from django.shortcuts import redirect, render
|
save_notification_rules_impl,
|
||||||
from django.utils import timezone
|
save_workflow_rules_impl,
|
||||||
from django.utils.translation import gettext as _
|
send_test_email_impl,
|
||||||
|
toggle_email_mode_impl,
|
||||||
from .branding import get_default_notification_templates
|
toggle_nextcloud_enabled_impl,
|
||||||
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 .welcome_email_views import (
|
||||||
from .notifications import notify_user
|
bulk_welcome_email_action_impl,
|
||||||
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
cancel_welcome_email_impl,
|
||||||
from .tasks import send_scheduled_welcome_email
|
pause_welcome_email_impl,
|
||||||
from .emailing import send_system_email
|
resume_welcome_email_impl,
|
||||||
|
save_welcome_email_settings_impl,
|
||||||
|
trigger_welcome_email_now_impl,
|
||||||
def integrations_setup_page_impl(request):
|
welcome_emails_page_impl,
|
||||||
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'}:
|
__all__ = [
|
||||||
kind = 'nextcloud'
|
'integrations_setup_page_impl',
|
||||||
templates = list(NotificationTemplate.objects.all().order_by('key'))
|
'welcome_emails_page_impl',
|
||||||
system_email_config = (
|
'trigger_welcome_email_now_impl',
|
||||||
SystemEmailConfig.objects.filter(is_active=True).order_by('-updated_at').first()
|
'save_welcome_email_settings_impl',
|
||||||
or SystemEmailConfig.objects.filter(name='Default SMTP').first()
|
'bulk_welcome_email_action_impl',
|
||||||
)
|
'pause_welcome_email_impl',
|
||||||
return render(
|
'resume_welcome_email_impl',
|
||||||
request,
|
'cancel_welcome_email_impl',
|
||||||
'workflows/integrations_setup.html',
|
'send_test_email_impl',
|
||||||
{
|
'nextcloud_test_upload_impl',
|
||||||
'workflow_config': config,
|
'toggle_nextcloud_enabled_impl',
|
||||||
'system_email_config': system_email_config,
|
'toggle_email_mode_impl',
|
||||||
'nextcloud_enabled': is_nextcloud_enabled(),
|
'save_integrations_settings_impl',
|
||||||
'email_test_mode': is_email_test_mode(),
|
'save_nextcloud_settings_impl',
|
||||||
'kind': kind,
|
'save_workflow_rules_impl',
|
||||||
'templates': templates,
|
'save_backup_settings_impl',
|
||||||
'notification_rules': NotificationRule.objects.all().order_by('event_type', 'sort_order', 'id'),
|
'save_mail_settings_impl',
|
||||||
'rule_event_choices': NotificationRule.EVENT_CHOICES,
|
'save_email_routing_settings_impl',
|
||||||
'rule_operator_choices': NotificationRule.OPERATOR_CHOICES,
|
'save_notification_rules_impl',
|
||||||
'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')
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ from .form_builder import (
|
|||||||
get_section_order,
|
get_section_order,
|
||||||
apply_form_preset,
|
apply_form_preset,
|
||||||
)
|
)
|
||||||
from .form_builder_views import form_builder_page_impl
|
from .form_builder_views import form_builder_page_impl, form_builder_save_order_impl
|
||||||
from .intro_builder_views import intro_builder_page_impl
|
from .intro_builder_views import intro_builder_page_impl
|
||||||
from .observability_views import (
|
from .observability_views import (
|
||||||
audit_log_page_impl,
|
audit_log_page_impl,
|
||||||
@@ -499,7 +499,12 @@ def welcome_emails_page(request):
|
|||||||
@_require_capability('manage_welcome_emails')
|
@_require_capability('manage_welcome_emails')
|
||||||
@require_POST
|
@require_POST
|
||||||
def trigger_welcome_email_now(request, schedule_id: int):
|
def trigger_welcome_email_now(request, schedule_id: int):
|
||||||
return integrations_views.trigger_welcome_email_now_impl(request, schedule_id, audit_fn=_audit)
|
return integrations_views.trigger_welcome_email_now_impl(
|
||||||
|
request,
|
||||||
|
schedule_id,
|
||||||
|
audit_fn=_audit,
|
||||||
|
send_task_fn=send_scheduled_welcome_email,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@_require_capability('manage_welcome_emails')
|
@_require_capability('manage_welcome_emails')
|
||||||
@@ -508,39 +513,14 @@ def save_welcome_email_settings(request):
|
|||||||
return integrations_views.save_welcome_email_settings_impl(request, audit_fn=_audit)
|
return integrations_views.save_welcome_email_settings_impl(request, audit_fn=_audit)
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@_require_capability('manage_welcome_emails')
|
@_require_capability('manage_welcome_emails')
|
||||||
@require_POST
|
@require_POST
|
||||||
def bulk_welcome_email_action(request):
|
def bulk_welcome_email_action(request):
|
||||||
return integrations_views.bulk_welcome_email_action_impl(request, audit_fn=_audit)
|
return integrations_views.bulk_welcome_email_action_impl(
|
||||||
|
request,
|
||||||
|
audit_fn=_audit,
|
||||||
|
send_task_fn=send_scheduled_welcome_email,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@_require_capability('manage_welcome_emails')
|
@_require_capability('manage_welcome_emails')
|
||||||
@@ -552,7 +532,12 @@ def pause_welcome_email(request, schedule_id: int):
|
|||||||
@_require_capability('manage_welcome_emails')
|
@_require_capability('manage_welcome_emails')
|
||||||
@require_POST
|
@require_POST
|
||||||
def resume_welcome_email(request, schedule_id: int):
|
def resume_welcome_email(request, schedule_id: int):
|
||||||
return integrations_views.resume_welcome_email_impl(request, schedule_id, audit_fn=_audit)
|
return integrations_views.resume_welcome_email_impl(
|
||||||
|
request,
|
||||||
|
schedule_id,
|
||||||
|
audit_fn=_audit,
|
||||||
|
send_task_fn=send_scheduled_welcome_email,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@_require_capability('manage_welcome_emails')
|
@_require_capability('manage_welcome_emails')
|
||||||
@@ -564,7 +549,7 @@ def cancel_welcome_email(request, schedule_id: int):
|
|||||||
@_require_capability('manage_builders')
|
@_require_capability('manage_builders')
|
||||||
@require_POST
|
@require_POST
|
||||||
def form_builder_save_order(request):
|
def form_builder_save_order(request):
|
||||||
return integrations_views.form_builder_save_order_impl(request, audit_fn=_audit)
|
return form_builder_save_order_impl(request, audit_fn=_audit)
|
||||||
|
|
||||||
|
|
||||||
@_require_capability('manage_integrations')
|
@_require_capability('manage_integrations')
|
||||||
|
|||||||
279
backend/workflows/welcome_email_views.py
Normal file
279
backend/workflows/welcome_email_views.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
from celery import current_app
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .branding import get_default_notification_templates
|
||||||
|
from .models import NotificationTemplate, ScheduledWelcomeEmail, WorkflowConfig
|
||||||
|
from .tasks import send_scheduled_welcome_email
|
||||||
|
|
||||||
|
|
||||||
|
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, send_task_fn=send_scheduled_welcome_email):
|
||||||
|
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_task_fn.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, send_task_fn=send_scheduled_welcome_email):
|
||||||
|
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_task_fn.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, send_task_fn=send_scheduled_welcome_email):
|
||||||
|
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_task_fn.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')
|
||||||
Reference in New Issue
Block a user