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 "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." %} | +|||||
AdminAuditLog/admin-tools/audit-log/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')