snapshot: preserve audit log and filtering phase
This commit is contained in:
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user