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

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