diff --git a/backend/workflows/admin.py b/backend/workflows/admin.py index ce04659..5895ee7 100644 --- a/backend/workflows/admin.py +++ b/backend/workflows/admin.py @@ -3,7 +3,7 @@ from django.conf import settings from django import forms from .emailing import send_system_email -from .models import EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig +from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig @admin.register(EmployeeProfile) @@ -12,6 +12,14 @@ class EmployeeProfileAdmin(admin.ModelAdmin): search_fields = ('full_name', 'work_email', 'department') +@admin.register(AdminAuditLog) +class AdminAuditLogAdmin(admin.ModelAdmin): + list_display = ('created_at', 'actor_display', 'action', 'target_type', 'target_id', 'target_label') + search_fields = ('actor_display', 'action', 'target_type', 'target_label') + list_filter = ('action', 'target_type', 'created_at') + ordering = ('-created_at', '-id') + + @admin.register(OnboardingRequest) class OnboardingRequestAdmin(admin.ModelAdmin): list_display = ('id', 'full_name', 'work_email', 'department', 'contract_start', 'created_at') diff --git a/backend/workflows/migrations/0032_adminauditlog.py b/backend/workflows/migrations/0032_adminauditlog.py new file mode 100644 index 0000000..7646ff4 --- /dev/null +++ b/backend/workflows/migrations/0032_adminauditlog.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.5 on 2026-03-25 19:24 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0031_alter_offboardingrequest_preferred_language_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AdminAuditLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('actor_display', models.CharField(blank=True, max_length=255)), + ('action', models.CharField(max_length=120)), + ('target_type', models.CharField(blank=True, max_length=80)), + ('target_id', models.PositiveIntegerField(blank=True, null=True)), + ('target_label', models.CharField(blank=True, max_length=255)), + ('details', models.JSONField(blank=True, default=dict)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='admin_audit_logs', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Admin Audit Log', + 'verbose_name_plural': 'Admin Audit Logs', + 'ordering': ['-created_at', '-id'], + }, + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 477849c..66fc850 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.db import models from django.utils.translation import get_language @@ -22,6 +23,32 @@ class EmployeeProfile(models.Model): return f"{self.full_name} <{self.work_email}>" +class AdminAuditLog(models.Model): + actor = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='admin_audit_logs', + ) + actor_display = models.CharField(max_length=255, blank=True) + action = models.CharField(max_length=120) + target_type = models.CharField(max_length=80, blank=True) + target_id = models.PositiveIntegerField(null=True, blank=True) + target_label = models.CharField(max_length=255, blank=True) + details = models.JSONField(default=dict, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at', '-id'] + verbose_name = 'Admin Audit Log' + verbose_name_plural = 'Admin Audit Logs' + + def __str__(self) -> str: + actor = self.actor_display or 'Unbekannt' + return f'{self.created_at:%Y-%m-%d %H:%M} | {actor} | {self.action}' + + class OnboardingRequest(models.Model): full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname') gender = models.CharField( diff --git a/backend/workflows/templates/workflows/audit_log.html b/backend/workflows/templates/workflows/audit_log.html new file mode 100644 index 0000000..b47d3c7 --- /dev/null +++ b/backend/workflows/templates/workflows/audit_log.html @@ -0,0 +1,88 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Audit Log" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block shell_body %} +{% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %} + +
+
+

{% trans "Audit Log" %}

+

{% trans "Nachvollziehbarkeit aller wichtigen Admin-Aktionen im Portal." %}

+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + {% trans "Zurücksetzen" %} +
+
+
+ + + + + + + + + + + + + {% for row in rows %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Zeit" %}{% trans "Nutzer" %}{% trans "Aktion" %}{% trans "Typ" %}{% trans "Ziel" %}{% trans "Details" %}
{{ row.created_at|date:"Y-m-d H:i:s" }}{{ row.actor_display|default:"-" }}{{ row.action }}{{ row.target_type|default:"-" }} + {% if row.target_label %} + {{ row.target_label }} + {% if row.target_id %}
#{{ row.target_id }}
{% endif %} + {% elif row.target_id %} + #{{ row.target_id }} + {% else %} + - + {% endif %} +
{{ row.details|default:"{}" }}
{% trans "Noch keine Audit-Einträge vorhanden." %}
+
+
+{% endblock %} diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index 6e9bc0f..ed13668 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -169,6 +169,14 @@ docker compose exec -T web django-admin compilemessages
Dynamic content should use explicit DE/EN fields with German fallback, not machine translation at runtime.
+

Audit Trail

+

11) Testing and Validation

docker compose exec -T web python manage.py check
diff --git a/backend/workflows/templates/workflows/home.html b/backend/workflows/templates/workflows/home.html
index fd20aab..d0131d1 100644
--- a/backend/workflows/templates/workflows/home.html
+++ b/backend/workflows/templates/workflows/home.html
@@ -131,6 +131,11 @@
 {% trans "Öffnen" %}
         
         
+

{% trans "Audit Log" %}

+

{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}

+{% trans "Öffnen" %} +
+

{% trans "Integrationen" %}

{% trans "Nextcloud- und E-Mail-Setup." %}

{% trans "Öffnen" %} @@ -192,4 +197,3 @@ {% endblock %} - diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html index 5b8f6cf..8337897 100644 --- a/backend/workflows/templates/workflows/project_wiki.html +++ b/backend/workflows/templates/workflows/project_wiki.html @@ -176,6 +176,7 @@
  • Einweisungs-Builder: manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.
  • Integrations: Nextcloud, SMTP, default routing addresses, notification rules.
  • Welcome Emails: scheduled jobs, pause/resume/cancel/trigger now.
  • +
  • Audit Log: staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.
  • Requests Dashboard: search records, open PDFs, delete records (single/bulk for staff).
  • Einweisungs- und Übergabeprotokoll: staff-only PDF erzeugen, Neu erzeugen, and PDF öffnen actions directly on onboarding rows in the Requests Dashboard.
  • Einweisung durchführen: staff-only live checklist page opened from onboarding rows, with draft/completed status, notes, progress tracking, and a separate live-status PDF export.
  • diff --git a/backend/workflows/urls.py b/backend/workflows/urls.py index 415d093..21436ab 100644 --- a/backend/workflows/urls.py +++ b/backend/workflows/urls.py @@ -31,6 +31,7 @@ urlpatterns = [ path('admin-tools/wiki/', views.project_wiki_page, name='project_wiki_page'), path('admin-tools/developer-handbook/', views.developer_handbook_page, name='developer_handbook_page'), path('admin-tools/release-checklist/', views.release_checklist_page, name='release_checklist_page'), + path('admin-tools/audit-log/', views.audit_log_page, name='audit_log_page'), path('admin-tools/form-builder/', views.form_builder_page, name='form_builder_page'), path('admin-tools/form-builder/save-order/', views.form_builder_save_order, name='form_builder_save_order'), path('admin-tools/intro-builder/', views.intro_builder_page, name='intro_builder_page'), diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 4f25449..a9a001e 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -27,7 +27,7 @@ from .form_builder import ( ONBOARDING_PAGE_ORDER, ensure_form_field_configs, ) -from .models import EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig +from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig from .emailing import send_system_email from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud from .tasks import ( @@ -117,6 +117,28 @@ def _display_user_name(user) -> str: return (getattr(user, 'email', '') or '').strip() +def _audit( + request, + action: str, + *, + target_type: str = '', + target_id: int | None = None, + target_label: str = '', + details: dict | None = None, +) -> None: + if not getattr(request, 'user', None) or not request.user.is_authenticated: + return + AdminAuditLog.objects.create( + actor=request.user, + actor_display=_display_user_name(request.user), + action=action, + target_type=target_type, + target_id=target_id, + target_label=target_label, + details=details or {}, + ) + + def _form_field_labels(form_type: str) -> dict[str, str]: if form_type == 'onboarding': return {name: str(field.label or name) for name, field in OnboardingRequestForm.base_fields.items()} @@ -238,6 +260,47 @@ def release_checklist_page(request): return render(request, 'workflows/release_checklist.html') +@login_required +@user_passes_test(_is_staff) +def audit_log_page(request): + action = (request.GET.get('action') or '').strip() + user_query = (request.GET.get('user') or '').strip() + date_from = (request.GET.get('date_from') or '').strip() + date_to = (request.GET.get('date_to') or '').strip() + + rows_qs = AdminAuditLog.objects.select_related('actor').all() + + if action: + rows_qs = rows_qs.filter(action=action) + if user_query: + rows_qs = rows_qs.filter( + Q(actor_display__icontains=user_query) + | Q(actor__username__icontains=user_query) + | Q(actor__email__icontains=user_query) + ) + if date_from: + rows_qs = rows_qs.filter(created_at__date__gte=date_from) + if date_to: + rows_qs = rows_qs.filter(created_at__date__lte=date_to) + + rows = list(rows_qs[:300]) + action_choices = ( + AdminAuditLog.objects.order_by('action').values_list('action', flat=True).distinct() + ) + return render( + request, + 'workflows/audit_log.html', + { + 'rows': rows, + 'action_choices': action_choices, + 'selected_action': action, + 'user_query': user_query, + 'date_from': date_from, + 'date_to': date_to, + }, + ) + + @login_required def requests_dashboard(request): if request.method == 'POST': @@ -280,6 +343,13 @@ def requests_dashboard(request): deleted_count += 1 if deleted_count: + _audit( + request, + 'requests_deleted', + target_type='request', + target_label='Dashboard bulk/single delete', + details={'deleted_count': deleted_count, 'invalid_count': invalid_count, 'selected': selected}, + ) messages.success(request, _('%(count)s Eintrag/Einträge gelöscht.') % {'count': deleted_count}) if invalid_count: messages.warning(request, _('%(count)s Auswahl(en) konnten nicht verarbeitet werden.') % {'count': invalid_count}) @@ -443,6 +513,7 @@ def generate_onboarding_intro_pdf(request, request_id: int): pdf_path = _generate_onboarding_intro_pdf(obj, language_code=get_language()) obj.intro_pdf_path = str(pdf_path) obj.save(update_fields=['intro_pdf_path']) + _audit(request, 'intro_pdf_generated', target_type='onboarding', target_id=obj.id, target_label=obj.full_name) messages.success(request, _('Einweisungs- und Übergabeprotokoll wurde erzeugt.')) return redirect('requests_dashboard') @@ -460,6 +531,7 @@ def generate_onboarding_intro_session_pdf(request, request_id: int): ) session.exported_pdf_path = str(pdf_path) session.save(update_fields=['exported_pdf_path']) + _audit(request, 'intro_live_pdf_generated', target_type='onboarding', target_id=onboarding.id, target_label=onboarding.full_name) messages.success(request, _('Einweisungsprotokoll aus Live-Status wurde erzeugt.')) return redirect('onboarding_intro_session_page', request_id=request_id) @@ -489,17 +561,34 @@ def onboarding_intro_session_page(request, request_id: int): session.completed_by_name = '' session.exported_pdf_path = '' session.save(update_fields=['checklist_state', 'notes', 'status', 'completed_at', 'completed_by_name', 'exported_pdf_path']) + _audit(request, 'intro_session_reset', target_type='onboarding', target_id=onboarding.id, target_label=onboarding.full_name) messages.success(request, _('Einweisung wurde zurückgesetzt.')) return redirect('onboarding_intro_session_page', request_id=request_id) if action == 'complete': session.status = 'completed' session.completed_at = timezone.now() session.completed_by_name = _display_user_name(request.user) + _audit( + request, + 'intro_session_completed', + target_type='onboarding', + target_id=onboarding.id, + target_label=onboarding.full_name, + details={'checked_count': len([value for value in checklist_state.values() if value])}, + ) messages.success(request, _('Einweisung wurde als abgeschlossen gespeichert.')) else: session.status = 'draft' session.completed_at = None session.completed_by_name = '' + _audit( + request, + 'intro_session_saved', + target_type='onboarding', + target_id=onboarding.id, + target_label=onboarding.full_name, + details={'checked_count': len([value for value in checklist_state.values() if value])}, + ) messages.success(request, _('Einweisung wurde als Entwurf gespeichert.')) session.save() return redirect('onboarding_intro_session_page', request_id=request_id) @@ -622,7 +711,10 @@ def form_builder_page(request): messages.error(request, 'Option nicht gefunden.') else: option_category = option.category + deleted_label = option.label + deleted_id = option.id option.delete() + _audit(request, 'form_option_deleted', target_type='form_option', target_id=deleted_id, target_label=deleted_label) messages.success(request, 'Option wurde gelöscht.') return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}") @@ -641,6 +733,7 @@ def form_builder_page(request): FormOption.objects.filter(category=category).order_by('-sort_order').values_list('sort_order', flat=True).first() ) FormOption.objects.create( + # Global form option catalog entry category=category, label=label, label_en=label_en, @@ -648,6 +741,13 @@ def form_builder_page(request): sort_order=(next_sort + 1) if next_sort is not None else 0, is_active=True, ) + _audit( + request, + 'form_option_added', + target_type='form_option', + target_label=label, + details={'category': category, 'label_en': label_en, 'value': value or label}, + ) messages.success(request, 'Option wurde hinzugefügt.') option_category = category @@ -669,6 +769,7 @@ def form_builder_page(request): messages.error(request, f'Doppelte Bezeichnung in Kategorie: {next_label}') return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}") option_category = option.category + _audit(request, 'form_options_saved', target_type='form_option', target_label=option_category, details={'count': len(option_ids)}) messages.success(request, 'Optionen wurden gespeichert.') elif action == 'save_field_texts': @@ -682,6 +783,7 @@ def form_builder_page(request): cfg.help_text_override = (request.POST.get(f'help_text_override_{cfg.id}') or '').strip() cfg.help_text_override_en = (request.POST.get(f'help_text_override_en_{cfg.id}') or '').strip() cfg.save(update_fields=['label_override', 'label_override_en', 'help_text_override', 'help_text_override_en']) + _audit(request, 'form_field_texts_saved', target_type='form_config', target_label=form_type, details={'count': len(field_ids)}) messages.success(request, 'Feldtexte wurden gespeichert.') return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}") @@ -773,7 +875,10 @@ def intro_builder_page(request): if delete_id: item = IntroChecklistItem.objects.filter(id=delete_id).first() if item: + deleted_label = item.label + deleted_id_int = item.id item.delete() + _audit(request, 'intro_checklist_item_deleted', target_type='intro_checklist_item', target_id=deleted_id_int, target_label=deleted_label) messages.success(request, 'Checklistenpunkt wurde gelöscht.') else: messages.error(request, 'Checklistenpunkt nicht gefunden.') @@ -801,6 +906,7 @@ def intro_builder_page(request): is_active=True, condition_operator='always', ) + _audit(request, 'intro_checklist_item_added', target_type='intro_checklist_item', target_label=label, details={'section': section, 'label_en': label_en}) messages.success(request, 'Checklistenpunkt wurde hinzugefügt.') return redirect('intro_builder_page') @@ -838,6 +944,7 @@ def intro_builder_page(request): 'sort_order', ] ) + _audit(request, 'intro_checklist_saved', target_type='intro_checklist_item', details={'count': len(item_ids)}) messages.success(request, 'Einweisungs-Checkliste wurde gespeichert.') return redirect('intro_builder_page') @@ -942,6 +1049,7 @@ def trigger_welcome_email_now(request, schedule_id: int): scheduled.status = 'scheduled' scheduled.last_error = '' scheduled.save(update_fields=['celery_task_id', 'status', 'last_error', 'updated_at']) + _audit(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') @@ -1005,6 +1113,17 @@ def save_welcome_email_settings(request): if changes: template.save(update_fields=changes) + _audit( + 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') @@ -1096,6 +1215,13 @@ def bulk_welcome_email_action(request): 'delete': 'gelöscht', }[action] if success_count: + _audit( + 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).') @@ -1117,6 +1243,7 @@ def pause_welcome_email(request, schedule_id: int): _revoke_celery_task(scheduled.celery_task_id) scheduled.status = 'paused' scheduled.save(update_fields=['status', 'updated_at']) + _audit(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') @@ -1139,6 +1266,7 @@ def resume_welcome_email(request, schedule_id: int): scheduled.status = 'scheduled' scheduled.last_error = '' scheduled.save(update_fields=['celery_task_id', 'status', 'last_error', 'updated_at']) + _audit(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') @@ -1159,6 +1287,7 @@ def cancel_welcome_email(request, schedule_id: int): scheduled.status = 'cancelled' scheduled.last_error = '' scheduled.save(update_fields=['status', 'last_error', 'updated_at']) + _audit(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') @@ -1224,6 +1353,7 @@ def form_builder_save_order(request): cfg.page_key = '' FormFieldConfig.objects.bulk_update(configs, ['sort_order', 'page_key']) + _audit(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': len(configs)}) return JsonResponse({'ok': True, 'saved_count': len(configs)}) @@ -1242,6 +1372,7 @@ def send_test_email(request): ), to=[settings.TEST_NOTIFICATION_EMAIL], ) + _audit(request, 'smtp_test_sent', target_type='system_email', target_label=settings.TEST_NOTIFICATION_EMAIL, details={'email_test_mode': is_email_test_mode()}) messages.success(request, f'SMTP-Testmail wurde gesendet ({mode}).') return redirect('home') @@ -1265,8 +1396,10 @@ def nextcloud_test_upload(request): ok = upload_to_nextcloud(temp_path, filename) if ok: + _audit(request, 'nextcloud_test_upload', target_type='nextcloud', target_label=filename, details={'result': 'success'}) messages.success(request, f'Nextcloud-Testupload erfolgreich: {filename}') else: + _audit(request, 'nextcloud_test_upload', target_type='nextcloud', target_label=filename, details={'result': 'error'}) messages.error(request, 'Nextcloud-Testupload fehlgeschlagen. Bitte Konfiguration prüfen.') except Exception as exc: messages.error(request, f'Nextcloud-Testupload fehlgeschlagen: {exc}') @@ -1285,6 +1418,7 @@ def toggle_nextcloud_enabled(request): currently_enabled = is_nextcloud_enabled() config.nextcloud_enabled_override = not currently_enabled config.save(update_fields=['nextcloud_enabled_override']) + _audit(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}.') @@ -1299,6 +1433,7 @@ def toggle_email_mode(request): 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(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.') @@ -1339,6 +1474,7 @@ def save_integrations_settings(request): config.email_password = email_password config.save() + _audit(request, 'integrations_saved', target_type='workflow_config', target_label='all_integrations') messages.success(request, 'Integrations-Einstellungen wurden gespeichert.') return redirect('home') @@ -1364,6 +1500,7 @@ def save_nextcloud_settings(request): config.nextcloud_password_override = nextcloud_password config.save() + _audit(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') @@ -1392,6 +1529,7 @@ def save_mail_settings(request): config.email_password = email_password config.save() + _audit(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') @@ -1459,6 +1597,7 @@ def save_email_routing_settings(request): if changed: obj.save(update_fields=changed) + _audit(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') @@ -1522,6 +1661,7 @@ def save_notification_rules(request): sort_order=NotificationRule.objects.filter(event_type=new_event).count() + 1, ) + _audit(request, 'notification_rules_saved', target_type='notification_rule') messages.success(request, 'Benachrichtigungsregeln wurden gespeichert.') return redirect('/admin-tools/integrations/?kind=emails') @@ -1539,5 +1679,6 @@ def delete_request_from_dashboard(request, kind: str, request_id: int): return redirect('requests_dashboard') obj.delete() + _audit(request, 'request_deleted', target_type=kind, target_id=request_id, target_label=str(obj)) messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde gelöscht.') return redirect('requests_dashboard')