snapshot: preserve audit log and filtering phase
This commit is contained in:
@@ -3,7 +3,7 @@ from django.conf import settings
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from .emailing import send_system_email
|
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)
|
@admin.register(EmployeeProfile)
|
||||||
@@ -12,6 +12,14 @@ class EmployeeProfileAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('full_name', 'work_email', 'department')
|
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)
|
@admin.register(OnboardingRequest)
|
||||||
class OnboardingRequestAdmin(admin.ModelAdmin):
|
class OnboardingRequestAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'full_name', 'work_email', 'department', 'contract_start', 'created_at')
|
list_display = ('id', 'full_name', 'work_email', 'department', 'contract_start', 'created_at')
|
||||||
|
|||||||
35
backend/workflows/migrations/0032_adminauditlog.py
Normal file
35
backend/workflows/migrations/0032_adminauditlog.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
@@ -22,6 +23,32 @@ class EmployeeProfile(models.Model):
|
|||||||
return f"{self.full_name} <{self.work_email}>"
|
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):
|
class OnboardingRequest(models.Model):
|
||||||
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
||||||
gender = models.CharField(
|
gender = models.CharField(
|
||||||
|
|||||||
88
backend/workflows/templates/workflows/audit_log.html
Normal file
88
backend/workflows/templates/workflows/audit_log.html
Normal 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 %}
|
||||||
@@ -169,6 +169,14 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
|||||||
<div class="note">
|
<div class="note">
|
||||||
Dynamic content should use explicit DE/EN fields with German fallback, not machine translation at runtime.
|
Dynamic content should use explicit DE/EN fields with German fallback, not machine translation at runtime.
|
||||||
</div>
|
</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>
|
<h2 id="testing">11) Testing and Validation</h2>
|
||||||
<pre><code>docker compose exec -T web python manage.py check
|
<pre><code>docker compose exec -T web python manage.py check
|
||||||
|
|||||||
@@ -131,6 +131,11 @@
|
|||||||
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
<section class="admin-card">
|
<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>
|
<h3>{% trans "Integrationen" %}</h3>
|
||||||
<p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
|
<p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
|
||||||
@@ -192,4 +197,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -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>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>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>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>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>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>
|
<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>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ urlpatterns = [
|
|||||||
path('admin-tools/wiki/', views.project_wiki_page, name='project_wiki_page'),
|
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/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/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/', 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/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'),
|
path('admin-tools/intro-builder/', views.intro_builder_page, name='intro_builder_page'),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from .form_builder import (
|
|||||||
ONBOARDING_PAGE_ORDER,
|
ONBOARDING_PAGE_ORDER,
|
||||||
ensure_form_field_configs,
|
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 .emailing import send_system_email
|
||||||
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
@@ -117,6 +117,28 @@ def _display_user_name(user) -> str:
|
|||||||
return (getattr(user, 'email', '') or '').strip()
|
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]:
|
def _form_field_labels(form_type: str) -> dict[str, str]:
|
||||||
if form_type == 'onboarding':
|
if form_type == 'onboarding':
|
||||||
return {name: str(field.label or name) for name, field in OnboardingRequestForm.base_fields.items()}
|
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')
|
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
|
@login_required
|
||||||
def requests_dashboard(request):
|
def requests_dashboard(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@@ -280,6 +343,13 @@ def requests_dashboard(request):
|
|||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
|
|
||||||
if deleted_count:
|
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})
|
messages.success(request, _('%(count)s Eintrag/Einträge gelöscht.') % {'count': deleted_count})
|
||||||
if invalid_count:
|
if invalid_count:
|
||||||
messages.warning(request, _('%(count)s Auswahl(en) konnten nicht verarbeitet werden.') % {'count': 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())
|
pdf_path = _generate_onboarding_intro_pdf(obj, language_code=get_language())
|
||||||
obj.intro_pdf_path = str(pdf_path)
|
obj.intro_pdf_path = str(pdf_path)
|
||||||
obj.save(update_fields=['intro_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.'))
|
messages.success(request, _('Einweisungs- und Übergabeprotokoll wurde erzeugt.'))
|
||||||
return redirect('requests_dashboard')
|
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.exported_pdf_path = str(pdf_path)
|
||||||
session.save(update_fields=['exported_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.'))
|
messages.success(request, _('Einweisungsprotokoll aus Live-Status wurde erzeugt.'))
|
||||||
return redirect('onboarding_intro_session_page', request_id=request_id)
|
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.completed_by_name = ''
|
||||||
session.exported_pdf_path = ''
|
session.exported_pdf_path = ''
|
||||||
session.save(update_fields=['checklist_state', 'notes', 'status', 'completed_at', 'completed_by_name', '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.'))
|
messages.success(request, _('Einweisung wurde zurückgesetzt.'))
|
||||||
return redirect('onboarding_intro_session_page', request_id=request_id)
|
return redirect('onboarding_intro_session_page', request_id=request_id)
|
||||||
if action == 'complete':
|
if action == 'complete':
|
||||||
session.status = 'completed'
|
session.status = 'completed'
|
||||||
session.completed_at = timezone.now()
|
session.completed_at = timezone.now()
|
||||||
session.completed_by_name = _display_user_name(request.user)
|
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.'))
|
messages.success(request, _('Einweisung wurde als abgeschlossen gespeichert.'))
|
||||||
else:
|
else:
|
||||||
session.status = 'draft'
|
session.status = 'draft'
|
||||||
session.completed_at = None
|
session.completed_at = None
|
||||||
session.completed_by_name = ''
|
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.'))
|
messages.success(request, _('Einweisung wurde als Entwurf gespeichert.'))
|
||||||
session.save()
|
session.save()
|
||||||
return redirect('onboarding_intro_session_page', request_id=request_id)
|
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.')
|
messages.error(request, 'Option nicht gefunden.')
|
||||||
else:
|
else:
|
||||||
option_category = option.category
|
option_category = option.category
|
||||||
|
deleted_label = option.label
|
||||||
|
deleted_id = option.id
|
||||||
option.delete()
|
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.')
|
messages.success(request, 'Option wurde gelöscht.')
|
||||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}")
|
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.filter(category=category).order_by('-sort_order').values_list('sort_order', flat=True).first()
|
||||||
)
|
)
|
||||||
FormOption.objects.create(
|
FormOption.objects.create(
|
||||||
|
# Global form option catalog entry
|
||||||
category=category,
|
category=category,
|
||||||
label=label,
|
label=label,
|
||||||
label_en=label_en,
|
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,
|
sort_order=(next_sort + 1) if next_sort is not None else 0,
|
||||||
is_active=True,
|
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.')
|
messages.success(request, 'Option wurde hinzugefügt.')
|
||||||
option_category = category
|
option_category = category
|
||||||
|
|
||||||
@@ -669,6 +769,7 @@ def form_builder_page(request):
|
|||||||
messages.error(request, f'Doppelte Bezeichnung in Kategorie: {next_label}')
|
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}")
|
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}")
|
||||||
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.')
|
messages.success(request, 'Optionen wurden gespeichert.')
|
||||||
|
|
||||||
elif action == 'save_field_texts':
|
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 = (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.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'])
|
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.')
|
messages.success(request, 'Feldtexte wurden gespeichert.')
|
||||||
|
|
||||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}")
|
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:
|
if delete_id:
|
||||||
item = IntroChecklistItem.objects.filter(id=delete_id).first()
|
item = IntroChecklistItem.objects.filter(id=delete_id).first()
|
||||||
if item:
|
if item:
|
||||||
|
deleted_label = item.label
|
||||||
|
deleted_id_int = item.id
|
||||||
item.delete()
|
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.')
|
messages.success(request, 'Checklistenpunkt wurde gelöscht.')
|
||||||
else:
|
else:
|
||||||
messages.error(request, 'Checklistenpunkt nicht gefunden.')
|
messages.error(request, 'Checklistenpunkt nicht gefunden.')
|
||||||
@@ -801,6 +906,7 @@ def intro_builder_page(request):
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
condition_operator='always',
|
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.')
|
messages.success(request, 'Checklistenpunkt wurde hinzugefügt.')
|
||||||
return redirect('intro_builder_page')
|
return redirect('intro_builder_page')
|
||||||
|
|
||||||
@@ -838,6 +944,7 @@ def intro_builder_page(request):
|
|||||||
'sort_order',
|
'sort_order',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
_audit(request, 'intro_checklist_saved', target_type='intro_checklist_item', details={'count': len(item_ids)})
|
||||||
messages.success(request, 'Einweisungs-Checkliste wurde gespeichert.')
|
messages.success(request, 'Einweisungs-Checkliste wurde gespeichert.')
|
||||||
return redirect('intro_builder_page')
|
return redirect('intro_builder_page')
|
||||||
|
|
||||||
@@ -942,6 +1049,7 @@ def trigger_welcome_email_now(request, schedule_id: int):
|
|||||||
scheduled.status = 'scheduled'
|
scheduled.status = 'scheduled'
|
||||||
scheduled.last_error = ''
|
scheduled.last_error = ''
|
||||||
scheduled.save(update_fields=['celery_task_id', 'status', 'last_error', 'updated_at'])
|
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.')
|
messages.success(request, f'Welcome E-Mail #{schedule_id} wurde sofort angestoßen.')
|
||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
@@ -1005,6 +1113,17 @@ def save_welcome_email_settings(request):
|
|||||||
if changes:
|
if changes:
|
||||||
template.save(update_fields=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.')
|
messages.success(request, 'Welcome-E-Mail Einstellungen wurden gespeichert.')
|
||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
@@ -1096,6 +1215,13 @@ def bulk_welcome_email_action(request):
|
|||||||
'delete': 'gelöscht',
|
'delete': 'gelöscht',
|
||||||
}[action]
|
}[action]
|
||||||
if success_count:
|
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}.')
|
messages.success(request, f'{success_count} Welcome-Eintrag/Einträge {action_label}.')
|
||||||
if skipped_count:
|
if skipped_count:
|
||||||
messages.warning(request, f'{skipped_count} Eintrag/Einträge wurden übersprungen (Status nicht geeignet).')
|
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)
|
_revoke_celery_task(scheduled.celery_task_id)
|
||||||
scheduled.status = 'paused'
|
scheduled.status = 'paused'
|
||||||
scheduled.save(update_fields=['status', 'updated_at'])
|
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.')
|
messages.success(request, f'Welcome E-Mail #{schedule_id} wurde pausiert.')
|
||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
@@ -1139,6 +1266,7 @@ def resume_welcome_email(request, schedule_id: int):
|
|||||||
scheduled.status = 'scheduled'
|
scheduled.status = 'scheduled'
|
||||||
scheduled.last_error = ''
|
scheduled.last_error = ''
|
||||||
scheduled.save(update_fields=['celery_task_id', 'status', 'last_error', 'updated_at'])
|
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.')
|
messages.success(request, f'Welcome E-Mail #{schedule_id} wurde fortgesetzt.')
|
||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
@@ -1159,6 +1287,7 @@ def cancel_welcome_email(request, schedule_id: int):
|
|||||||
scheduled.status = 'cancelled'
|
scheduled.status = 'cancelled'
|
||||||
scheduled.last_error = ''
|
scheduled.last_error = ''
|
||||||
scheduled.save(update_fields=['status', 'last_error', 'updated_at'])
|
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.')
|
messages.success(request, f'Welcome E-Mail #{schedule_id} wurde abgebrochen.')
|
||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
@@ -1224,6 +1353,7 @@ def form_builder_save_order(request):
|
|||||||
cfg.page_key = ''
|
cfg.page_key = ''
|
||||||
|
|
||||||
FormFieldConfig.objects.bulk_update(configs, ['sort_order', '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)})
|
return JsonResponse({'ok': True, 'saved_count': len(configs)})
|
||||||
|
|
||||||
|
|
||||||
@@ -1242,6 +1372,7 @@ def send_test_email(request):
|
|||||||
),
|
),
|
||||||
to=[settings.TEST_NOTIFICATION_EMAIL],
|
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}).')
|
messages.success(request, f'SMTP-Testmail wurde gesendet ({mode}).')
|
||||||
return redirect('home')
|
return redirect('home')
|
||||||
|
|
||||||
@@ -1265,8 +1396,10 @@ def nextcloud_test_upload(request):
|
|||||||
|
|
||||||
ok = upload_to_nextcloud(temp_path, filename)
|
ok = upload_to_nextcloud(temp_path, filename)
|
||||||
if ok:
|
if ok:
|
||||||
|
_audit(request, 'nextcloud_test_upload', target_type='nextcloud', target_label=filename, details={'result': 'success'})
|
||||||
messages.success(request, f'Nextcloud-Testupload erfolgreich: {filename}')
|
messages.success(request, f'Nextcloud-Testupload erfolgreich: {filename}')
|
||||||
else:
|
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.')
|
messages.error(request, 'Nextcloud-Testupload fehlgeschlagen. Bitte Konfiguration prüfen.')
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
messages.error(request, f'Nextcloud-Testupload fehlgeschlagen: {exc}')
|
messages.error(request, f'Nextcloud-Testupload fehlgeschlagen: {exc}')
|
||||||
@@ -1285,6 +1418,7 @@ def toggle_nextcloud_enabled(request):
|
|||||||
currently_enabled = is_nextcloud_enabled()
|
currently_enabled = is_nextcloud_enabled()
|
||||||
config.nextcloud_enabled_override = not currently_enabled
|
config.nextcloud_enabled_override = not currently_enabled
|
||||||
config.save(update_fields=['nextcloud_enabled_override'])
|
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'
|
state = 'aktiviert' if config.nextcloud_enabled_override else 'deaktiviert'
|
||||||
messages.success(request, f'Nextcloud Upload wurde {state}.')
|
messages.success(request, f'Nextcloud Upload wurde {state}.')
|
||||||
@@ -1299,6 +1433,7 @@ def toggle_email_mode(request):
|
|||||||
currently_test_mode = is_email_test_mode()
|
currently_test_mode = is_email_test_mode()
|
||||||
config.email_test_mode_override = not currently_test_mode
|
config.email_test_mode_override = not currently_test_mode
|
||||||
config.save(update_fields=['email_test_mode_override'])
|
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'
|
state = 'Testmodus (Umleitung)' if config.email_test_mode_override else 'Produktionsmodus'
|
||||||
messages.success(request, f'E-Mail-Modus wurde auf {state} gesetzt.')
|
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.email_password = email_password
|
||||||
|
|
||||||
config.save()
|
config.save()
|
||||||
|
_audit(request, 'integrations_saved', target_type='workflow_config', target_label='all_integrations')
|
||||||
messages.success(request, 'Integrations-Einstellungen wurden gespeichert.')
|
messages.success(request, 'Integrations-Einstellungen wurden gespeichert.')
|
||||||
return redirect('home')
|
return redirect('home')
|
||||||
|
|
||||||
@@ -1364,6 +1500,7 @@ def save_nextcloud_settings(request):
|
|||||||
config.nextcloud_password_override = nextcloud_password
|
config.nextcloud_password_override = nextcloud_password
|
||||||
|
|
||||||
config.save()
|
config.save()
|
||||||
|
_audit(request, 'nextcloud_settings_saved', target_type='workflow_config', target_label='nextcloud')
|
||||||
messages.success(request, 'Nextcloud-Einstellungen wurden gespeichert.')
|
messages.success(request, 'Nextcloud-Einstellungen wurden gespeichert.')
|
||||||
return redirect('/admin-tools/integrations/?kind=nextcloud')
|
return redirect('/admin-tools/integrations/?kind=nextcloud')
|
||||||
|
|
||||||
@@ -1392,6 +1529,7 @@ def save_mail_settings(request):
|
|||||||
config.email_password = email_password
|
config.email_password = email_password
|
||||||
|
|
||||||
config.save()
|
config.save()
|
||||||
|
_audit(request, 'mail_settings_saved', target_type='workflow_config', target_label='mail')
|
||||||
messages.success(request, 'Mail-Einstellungen wurden gespeichert.')
|
messages.success(request, 'Mail-Einstellungen wurden gespeichert.')
|
||||||
return redirect('/admin-tools/integrations/?kind=mail')
|
return redirect('/admin-tools/integrations/?kind=mail')
|
||||||
|
|
||||||
@@ -1459,6 +1597,7 @@ def save_email_routing_settings(request):
|
|||||||
if changed:
|
if changed:
|
||||||
obj.save(update_fields=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.')
|
messages.success(request, 'E-Mail Routing und Vorlagen wurden gespeichert.')
|
||||||
return redirect('/admin-tools/integrations/?kind=emails')
|
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,
|
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.')
|
messages.success(request, 'Benachrichtigungsregeln wurden gespeichert.')
|
||||||
return redirect('/admin-tools/integrations/?kind=emails')
|
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')
|
return redirect('requests_dashboard')
|
||||||
|
|
||||||
obj.delete()
|
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.')
|
messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde gelöscht.')
|
||||||
return redirect('requests_dashboard')
|
return redirect('requests_dashboard')
|
||||||
|
|||||||
Reference in New Issue
Block a user