snapshot: preserve audit log and filtering phase

This commit is contained in:
Md Bayazid Bostame
2026-03-25 20:28:28 +01:00
parent 965531d155
commit a8f7eadbc6
9 changed files with 316 additions and 3 deletions

View File

@@ -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')

View File

@@ -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'],
},
),
]

View File

@@ -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(

View File

@@ -0,0 +1,88 @@
{% extends 'workflows/base_shell.html' %}
{% load static i18n %}
{% block title %}{% trans "Audit Log" %}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'workflows/css/admin_tools.css' %}" />
{% endblock %}
{% block shell_body %}
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %}
<div class="toolbar">
<div>
<h1>{% trans "Audit Log" %}</h1>
<p class="sub">{% trans "Nachvollziehbarkeit aller wichtigen Admin-Aktionen im Portal." %}</p>
</div>
</div>
<div class="card">
<form method="get" class="grid" style="margin-bottom:12px;">
<div class="field">
<label for="action">{% trans "Aktion" %}</label>
<select id="action" name="action">
<option value="">{% trans "Alle" %}</option>
{% for value in action_choices %}
<option value="{{ value }}" {% if selected_action == value %}selected{% endif %}>{{ value }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label for="user">{% trans "Nutzer" %}</label>
<input id="user" type="text" name="user" value="{{ user_query }}" placeholder="{% trans 'Name, Benutzername oder E-Mail' %}" />
</div>
<div class="field">
<label for="date_from">{% trans "Von Datum" %}</label>
<input id="date_from" type="date" name="date_from" value="{{ date_from }}" />
</div>
<div class="field">
<label for="date_to">{% trans "Bis Datum" %}</label>
<input id="date_to" type="date" name="date_to" value="{{ date_to }}" />
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">{% trans "Filtern" %}</button>
<a class="btn btn-secondary" href="/admin-tools/audit-log/">{% trans "Zurücksetzen" %}</a>
</div>
</form>
<div class="table-wrap">
<table class="table-controls">
<thead>
<tr>
<th>{% trans "Zeit" %}</th>
<th>{% trans "Nutzer" %}</th>
<th>{% trans "Aktion" %}</th>
<th>{% trans "Typ" %}</th>
<th>{% trans "Ziel" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row.created_at|date:"Y-m-d H:i:s" }}</td>
<td>{{ row.actor_display|default:"-" }}</td>
<td><code>{{ row.action }}</code></td>
<td>{{ row.target_type|default:"-" }}</td>
<td>
{% if row.target_label %}
{{ row.target_label }}
{% if row.target_id %}<div class="hint">#{{ row.target_id }}</div>{% endif %}
{% elif row.target_id %}
#{{ row.target_id }}
{% else %}
-
{% endif %}
</td>
<td><code>{{ row.details|default:"{}" }}</code></td>
</tr>
{% empty %}
<tr>
<td colspan="6">{% trans "Noch keine Audit-Einträge vorhanden." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -169,6 +169,14 @@ docker compose exec -T web django-admin compilemessages</code></pre>
<div class="note">
Dynamic content should use explicit DE/EN fields with German fallback, not machine translation at runtime.
</div>
<h3>Audit Trail</h3>
<ul>
<li>Model: <code>AdminAuditLog</code></li>
<li>Purpose: record staff-side mutations that affect operations or configuration.</li>
<li>Current hooks include builder edits, PDF generation, welcome-email actions, integration changes, mode toggles, tests, and request deletions.</li>
<li>Staff UI page: <code>/admin-tools/audit-log/</code></li>
<li>The current UI supports filtering by action, user, and date range. Keep filters server-side to avoid loading unbounded audit rows into the browser.</li>
</ul>
<h2 id="testing">11) Testing and Validation</h2>
<pre><code>docker compose exec -T web python manage.py check

View File

@@ -131,6 +131,11 @@
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>{% trans "Audit Log" %}</h3>
<p>{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}</p>
<a class="btn btn-secondary" href="/admin-tools/audit-log/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>{% trans "Integrationen" %}</h3>
<p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
@@ -192,4 +197,3 @@
</div>
</main>
{% endblock %}

View File

@@ -176,6 +176,7 @@
<li><strong>Einweisungs-Builder:</strong> manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.</li>
<li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules.</li>
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>
<li><strong>Audit Log:</strong> 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.</li>
<li><strong>Requests Dashboard:</strong> search records, open PDFs, delete records (single/bulk for staff).</li>
<li><strong>Einweisungs- und Übergabeprotokoll:</strong> staff-only <code>PDF erzeugen</code>, <code>Neu erzeugen</code>, and <code>PDF öffnen</code> actions directly on onboarding rows in the Requests Dashboard.</li>
<li><strong>Einweisung durchführen:</strong> staff-only live checklist page opened from onboarding rows, with draft/completed status, notes, progress tracking, and a separate live-status PDF export.</li>

View File

@@ -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'),

View File

@@ -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')