snapshot: modularize workflow helper and task orchestration layers
This commit is contained in:
@@ -4,8 +4,6 @@ from datetime import timedelta
|
||||
from tempfile import NamedTemporaryFile
|
||||
import json
|
||||
from io import BytesIO
|
||||
from functools import wraps
|
||||
|
||||
from celery import current_app
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
@@ -23,8 +21,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.utils.translation import gettext as _, gettext_lazy
|
||||
from django.utils.translation import get_language, override
|
||||
from django.utils.translation import gettext as _
|
||||
from django.urls import reverse
|
||||
|
||||
from .app_registry import build_portal_app_sections, get_portal_app_registry_rows, normalize_portal_app_sort_orders
|
||||
@@ -69,6 +66,28 @@ from .observability_views import (
|
||||
job_monitor_page_impl,
|
||||
verify_backup_from_admin_impl,
|
||||
)
|
||||
from .view_audit import audit as _audit, audit_action_label as _audit_action_label, display_user_name as _display_user_name
|
||||
from .view_context import (
|
||||
form_field_labels as _form_field_labels,
|
||||
request_custom_field_details as _request_custom_field_details,
|
||||
request_status_label as _request_status_label,
|
||||
request_target_label as _request_target_label,
|
||||
)
|
||||
from .view_form_runtime import (
|
||||
CONDITIONAL_RULE_OPERATOR_CHOICES,
|
||||
ONBOARDING_CHECKBOX_LISTS,
|
||||
ONBOARDING_GROUPS,
|
||||
ONBOARDING_INLINE_CHECKS,
|
||||
active_conditional_target_keys as _active_conditional_target_keys,
|
||||
build_offboarding_sections as _build_offboarding_sections,
|
||||
build_onboarding_layout as _build_onboarding_layout,
|
||||
build_onboarding_sections as _build_onboarding_sections,
|
||||
conditional_rule_summary as _conditional_rule_summary,
|
||||
field_rule_summary as _field_rule_summary,
|
||||
normalized_conditional_rule_payload as _normalized_conditional_rule_payload,
|
||||
translate_choice_list as _translate_choice_list,
|
||||
)
|
||||
from .view_permissions import require_capability as _require_capability
|
||||
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormOption, FormSectionConfig, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
|
||||
from .emailing import send_system_email
|
||||
from .notifications import notify_user
|
||||
@@ -109,96 +128,6 @@ def mark_all_notifications_read(request):
|
||||
UserNotification.objects.filter(user=request.user, read_at__isnull=True).update(read_at=timezone.now())
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
ONBOARDING_GROUPS = {
|
||||
'business-card-box': ['business_card_name', 'business_card_title', 'business_card_email', 'business_card_phone'],
|
||||
'employment-end-box': ['employment_end_date'],
|
||||
'group-mailboxes-box': ['group_mailboxes'],
|
||||
'extra-hardware-box': ['additional_hardware_multi', 'additional_hardware_other'],
|
||||
'extra-software-box': ['additional_software_multi', 'additional_software'],
|
||||
'extra-access-box': ['additional_access_text'],
|
||||
'successor-box': ['successor_name', 'inherit_phone_number_choice'],
|
||||
}
|
||||
|
||||
ONBOARDING_INLINE_CHECKS = {'order_business_cards', 'agreement_confirm'}
|
||||
ONBOARDING_CHECKBOX_LISTS = {
|
||||
'needed_devices_multi',
|
||||
'additional_hardware_multi',
|
||||
'needed_software_multi',
|
||||
'additional_software_multi',
|
||||
'needed_accesses_multi',
|
||||
'needed_workspace_groups_multi',
|
||||
'needed_resources_multi',
|
||||
}
|
||||
ONBOARDING_SECTION_META = {
|
||||
'stammdaten': {'title': gettext_lazy('Stammdaten'), 'subtitle': gettext_lazy('Person, Rolle, Abteilung')},
|
||||
'vertrag': {'title': gettext_lazy('Vertrag'), 'subtitle': gettext_lazy('Beschäftigung und Termine')},
|
||||
'itsetup': {'title': gettext_lazy('IT-Setup'), 'subtitle': gettext_lazy('Geräte, Software und Zugänge')},
|
||||
'abschluss': {'title': gettext_lazy('Abschluss'), 'subtitle': gettext_lazy('Notizen und Freigabe')},
|
||||
}
|
||||
|
||||
CONDITIONAL_RULE_OPERATOR_CHOICES = [
|
||||
('checked', _('ist aktiviert')),
|
||||
('equals', _('ist gleich')),
|
||||
('not_equals', _('ist nicht gleich')),
|
||||
]
|
||||
|
||||
|
||||
def _field_rule_summary(*, is_visible: bool, is_required, locked: bool) -> str:
|
||||
if locked:
|
||||
return str(_('Fixes Kernfeld, immer sichtbar.'))
|
||||
if not is_visible:
|
||||
return str(_('Ausgeblendet, erscheint nicht im Formular.'))
|
||||
if is_required is True:
|
||||
return str(_('Sichtbar und als Pflichtfeld markiert.'))
|
||||
if is_required is False:
|
||||
return str(_('Sichtbar und optional.'))
|
||||
return str(_('Sichtbar mit Standardverhalten.'))
|
||||
|
||||
|
||||
def _conditional_clause_sentence(clause: dict, field_label_map: dict[str, str]) -> str:
|
||||
field_name = (clause.get('field') or '').strip()
|
||||
operator = (clause.get('operator') or '').strip()
|
||||
value = clause.get('value')
|
||||
if not field_name or not operator:
|
||||
return ''
|
||||
field_label = field_label_map.get(field_name, field_name)
|
||||
if operator == 'checked':
|
||||
return _('%(field)s ist aktiviert') % {'field': field_label}
|
||||
if operator == 'equals':
|
||||
if value not in (None, ''):
|
||||
return _('%(field)s ist gleich %(value)s') % {'field': field_label, 'value': value}
|
||||
return _('%(field)s ist gleich') % {'field': field_label}
|
||||
if operator == 'not_equals':
|
||||
if value not in (None, ''):
|
||||
return _('%(field)s ist nicht gleich %(value)s') % {'field': field_label, 'value': value}
|
||||
return _('%(field)s ist nicht gleich') % {'field': field_label}
|
||||
return _('%(field)s erfüllt die Bedingung') % {'field': field_label}
|
||||
|
||||
|
||||
def _conditional_rule_summary(clauses: list[dict], field_label_map: dict[str, str]) -> str:
|
||||
active_clauses = [clause for clause in clauses if clause.get('field') and clause.get('operator')]
|
||||
if not active_clauses:
|
||||
return str(_('Immer sichtbar.'))
|
||||
parts = [str(_conditional_clause_sentence(clause, field_label_map)) for clause in active_clauses]
|
||||
return str(_('Sichtbar, wenn %(conditions)s.') % {'conditions': ' und '.join(parts)})
|
||||
|
||||
|
||||
def _normalized_conditional_rule_payload(form_type: str) -> dict[str, dict]:
|
||||
configs = ensure_form_conditional_rule_configs(form_type)
|
||||
payload = {}
|
||||
for target_key, cfg in configs.items():
|
||||
if not cfg.is_active:
|
||||
continue
|
||||
clauses = [clause for clause in (cfg.clauses or []) if clause.get('field') and clause.get('operator')]
|
||||
if clauses:
|
||||
payload[target_key] = {'all': clauses}
|
||||
return payload
|
||||
|
||||
|
||||
def _active_conditional_target_keys(form_type: str) -> set[str]:
|
||||
return set(_normalized_conditional_rule_payload(form_type).keys())
|
||||
|
||||
|
||||
def healthz(request):
|
||||
db_ok = True
|
||||
try:
|
||||
@@ -233,335 +162,6 @@ def account_profile_page(request):
|
||||
return account_views.account_profile_page_impl(request)
|
||||
|
||||
|
||||
def _require_capability(capability: str):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
@login_required
|
||||
def wrapped(request, *args, **kwargs):
|
||||
if not user_has_capability(request.user, capability):
|
||||
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
|
||||
return redirect('home')
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _display_user_name(user) -> str:
|
||||
first_name = (getattr(user, 'first_name', '') or '').strip()
|
||||
last_name = (getattr(user, 'last_name', '') or '').strip()
|
||||
full_name = f'{first_name} {last_name}'.strip()
|
||||
if full_name:
|
||||
return full_name
|
||||
username = (getattr(user, 'username', '') or '').strip()
|
||||
if username:
|
||||
return username
|
||||
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()}
|
||||
if form_type == 'offboarding':
|
||||
return {name: str(field.label or name) for name, field in OffboardingRequestForm.base_fields.items()}
|
||||
return {}
|
||||
|
||||
|
||||
def _request_target_label(obj, kind: str | None = None) -> str:
|
||||
request_kind = (kind or '').strip()
|
||||
if not request_kind:
|
||||
request_kind = 'onboarding' if isinstance(obj, OnboardingRequest) else 'offboarding'
|
||||
name = (getattr(obj, 'full_name', '') or '').strip() or f'#{getattr(obj, "id", "?")}'
|
||||
email = (getattr(obj, 'work_email', '') or '').strip()
|
||||
created_at = getattr(obj, 'created_at', None)
|
||||
date_label = created_at.strftime('%Y-%m-%d') if created_at else ''
|
||||
parts = [request_kind.capitalize(), name]
|
||||
if email:
|
||||
parts.append(f'<{email}>')
|
||||
if date_label:
|
||||
parts.append(date_label)
|
||||
return ' | '.join(parts)
|
||||
|
||||
|
||||
def _request_status_label(status_key: str, language_code: str | None = None) -> str:
|
||||
lang = ((language_code or 'de').split('-')[0] or 'de').lower()
|
||||
with override(lang):
|
||||
labels = {
|
||||
'submitted': _('Eingereicht'),
|
||||
'processing': _('In Bearbeitung'),
|
||||
'completed': _('Abgeschlossen'),
|
||||
'failed': _('Fehlgeschlagen'),
|
||||
}
|
||||
return labels.get(status_key, status_key)
|
||||
|
||||
|
||||
def _request_custom_field_details(obj, kind: str, language_code: str | None = None) -> list[dict[str, str]]:
|
||||
form_type = 'onboarding' if kind == 'onboarding' else 'offboarding'
|
||||
language_code = ((language_code or getattr(obj, 'preferred_language', '') or get_language() or 'de').split('-')[0]).lower()
|
||||
values = getattr(obj, 'custom_field_values', {}) or {}
|
||||
rows = []
|
||||
yes_label = 'Ja' if language_code == 'de' else 'Yes'
|
||||
for cfg in get_custom_field_configs(form_type, include_inactive=True):
|
||||
raw_value = values.get(cfg.field_key)
|
||||
if raw_value in (None, '', False, []):
|
||||
continue
|
||||
if isinstance(raw_value, bool):
|
||||
display_value = str(yes_label) if raw_value else ''
|
||||
elif isinstance(raw_value, list):
|
||||
display_value = ', '.join(str(item).strip() for item in raw_value if str(item).strip())
|
||||
else:
|
||||
display_value = str(raw_value).strip()
|
||||
if not display_value:
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
'label': cfg.translated_label(language_code),
|
||||
'value': display_value,
|
||||
'section': cfg.section_key,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda item: (item['section'], item['sort_order'], item['label']))
|
||||
return rows
|
||||
|
||||
|
||||
def _audit_action_label(action: str) -> str:
|
||||
labels = {
|
||||
'requests_deleted': _('Vorgänge gelöscht'),
|
||||
'request_deleted': _('Vorgang gelöscht'),
|
||||
'request_retried': _('Vorgang erneut angestoßen'),
|
||||
'intro_pdf_generated': _('Einweisungs-PDF erzeugt'),
|
||||
'intro_live_pdf_generated': _('Live-Protokoll erzeugt'),
|
||||
'intro_session_reset': _('Einweisung zurückgesetzt'),
|
||||
'intro_session_saved': _('Einweisung als Entwurf gespeichert'),
|
||||
'intro_session_completed': _('Einweisung abgeschlossen'),
|
||||
'form_option_deleted': _('Formularoption gelöscht'),
|
||||
'form_options_saved': _('Formularoptionen gespeichert'),
|
||||
'form_field_texts_saved': _('Feldtexte gespeichert'),
|
||||
'form_layout_saved': _('Formularlayout gespeichert'),
|
||||
'intro_checklist_item_deleted': _('Einweisungs-Checkpunkt gelöscht'),
|
||||
'intro_checklist_item_added': _('Einweisungs-Checkpunkt hinzugefügt'),
|
||||
'intro_checklist_saved': _('Einweisungs-Checkliste gespeichert'),
|
||||
'welcome_email_triggered_now': _('Welcome E-Mail sofort ausgelöst'),
|
||||
'welcome_email_settings_saved': _('Welcome E-Mail Einstellungen gespeichert'),
|
||||
'welcome_email_bulk_action': _('Welcome E-Mail Sammelaktion ausgeführt'),
|
||||
'welcome_email_paused': _('Welcome E-Mail pausiert'),
|
||||
'welcome_email_resumed': _('Welcome E-Mail fortgesetzt'),
|
||||
'welcome_email_cancelled': _('Welcome E-Mail abgebrochen'),
|
||||
'smtp_test_sent': _('SMTP-Test gesendet'),
|
||||
'nextcloud_test_upload': _('Nextcloud-Testupload ausgeführt'),
|
||||
'nextcloud_mode_toggled': _('Nextcloud-Modus umgeschaltet'),
|
||||
'email_mode_toggled': _('E-Mail-Modus umgeschaltet'),
|
||||
'integrations_saved': _('Integrationen gespeichert'),
|
||||
'nextcloud_settings_saved': _('Nextcloud-Einstellungen gespeichert'),
|
||||
'mail_settings_saved': _('Mail-Einstellungen gespeichert'),
|
||||
'email_routing_saved': _('E-Mail-Routing gespeichert'),
|
||||
'notification_rules_saved': _('Benachrichtigungsregeln gespeichert'),
|
||||
'user_created': _('Benutzer erstellt'),
|
||||
'user_updated': _('Benutzer aktualisiert'),
|
||||
'user_password_reset_sent': _('Passwort-Reset-Link versendet'),
|
||||
'user_deleted': _('Benutzer gelöscht'),
|
||||
'backup_created': _('Backup erstellt'),
|
||||
'backup_verified': _('Backup verifiziert'),
|
||||
'backup_deleted': _('Backup gelöscht'),
|
||||
'backup_settings_saved': _('Backup-Einstellungen gespeichert'),
|
||||
'portal_app_registry_saved': _('App-Registry gespeichert'),
|
||||
}
|
||||
return labels.get(action, action.replace('_', ' ').strip().capitalize())
|
||||
|
||||
|
||||
def _translate_choice_list(choices):
|
||||
return [(value, str(label)) for value, label in choices]
|
||||
|
||||
|
||||
def _build_onboarding_layout(form) -> list[dict]:
|
||||
ordered_names = list(form.fields.keys())
|
||||
group_by_field = {}
|
||||
for group_id, group_fields in ONBOARDING_GROUPS.items():
|
||||
for name in group_fields:
|
||||
group_by_field[name] = group_id
|
||||
conditional_target_keys = _active_conditional_target_keys('onboarding')
|
||||
|
||||
rendered_groups = set()
|
||||
consumed = set()
|
||||
blocks = []
|
||||
|
||||
for field_name in ordered_names:
|
||||
if field_name in consumed:
|
||||
continue
|
||||
|
||||
group_id = group_by_field.get(field_name)
|
||||
if group_id:
|
||||
if group_id in rendered_groups:
|
||||
continue
|
||||
group_fields = [
|
||||
form[name]
|
||||
for name in ONBOARDING_GROUPS[group_id]
|
||||
if name in form.fields
|
||||
]
|
||||
if not group_fields:
|
||||
continue
|
||||
blocks.append(
|
||||
{
|
||||
'kind': 'group',
|
||||
'id': group_id,
|
||||
'hidden_default': group_id in conditional_target_keys,
|
||||
'fields': group_fields,
|
||||
}
|
||||
)
|
||||
rendered_groups.add(group_id)
|
||||
consumed.update([f.name for f in group_fields])
|
||||
continue
|
||||
|
||||
if field_name.startswith('custom__') and field_name in conditional_target_keys:
|
||||
blocks.append(
|
||||
{
|
||||
'kind': 'group',
|
||||
'id': field_name,
|
||||
'hidden_default': True,
|
||||
'fields': [form[field_name]],
|
||||
}
|
||||
)
|
||||
consumed.add(field_name)
|
||||
continue
|
||||
|
||||
blocks.append({'kind': 'field', 'field': form[field_name]})
|
||||
consumed.add(field_name)
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def _section_for_block(block: dict, field_pages: dict[str, str]) -> str:
|
||||
if block['kind'] == 'field':
|
||||
return field_pages.get(block['field'].name, 'abschluss')
|
||||
fields = block.get('fields') or []
|
||||
if not fields:
|
||||
return 'abschluss'
|
||||
return field_pages.get(fields[0].name, 'abschluss')
|
||||
|
||||
|
||||
def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str], visible_section_keys: set[str] | None = None) -> list[dict]:
|
||||
section_defs = get_section_definitions('onboarding')
|
||||
section_order = [item['key'] for item in section_defs]
|
||||
section_titles = {item['key']: item['title'] for item in section_defs}
|
||||
grouped = {key: [] for key in section_order}
|
||||
for block in blocks:
|
||||
section_key = _section_for_block(block, field_pages)
|
||||
if section_key not in grouped:
|
||||
section_key = 'abschluss'
|
||||
grouped[section_key].append(block)
|
||||
visible_keys = visible_section_keys or set(section_order)
|
||||
sections = []
|
||||
custom_section_keys = {item['key'] for item in section_defs if item.get('is_custom')}
|
||||
for key in section_order:
|
||||
if key not in visible_keys:
|
||||
continue
|
||||
blocks_for_section = grouped[key]
|
||||
has_custom_checkbox_fields = False
|
||||
for block in blocks_for_section:
|
||||
candidate_fields = [block['field']] if block['kind'] == 'field' else (block.get('fields') or [])
|
||||
for bound_field in candidate_fields:
|
||||
widget_type = getattr(getattr(bound_field.field, 'widget', None), 'input_type', '')
|
||||
if bound_field.name.startswith('custom__') and widget_type == 'checkbox':
|
||||
has_custom_checkbox_fields = True
|
||||
break
|
||||
if has_custom_checkbox_fields:
|
||||
break
|
||||
sections.append(
|
||||
{
|
||||
'key': key,
|
||||
'title': section_titles.get(key, ONBOARDING_SECTION_META.get(key, {}).get('title', key)),
|
||||
'subtitle': ONBOARDING_SECTION_META.get(key, {}).get('subtitle', ''),
|
||||
'blocks': blocks_for_section,
|
||||
'is_custom': key in custom_section_keys,
|
||||
'has_custom_checkbox_fields': has_custom_checkbox_fields,
|
||||
}
|
||||
)
|
||||
return sections
|
||||
|
||||
|
||||
OFFBOARDING_SECTION_META = {
|
||||
'mitarbeitende': {'title': gettext_lazy('Mitarbeitende'), 'subtitle': gettext_lazy('Person, Rolle und Bereich')},
|
||||
'austritt': {'title': gettext_lazy('Austritt'), 'subtitle': gettext_lazy('Letzter Arbeitstag')},
|
||||
'abschluss': {'title': gettext_lazy('Abschluss'), 'subtitle': gettext_lazy('Hinweise und Abschlussnotizen')},
|
||||
}
|
||||
|
||||
|
||||
def _build_offboarding_sections(form, visible_section_keys: set[str] | None = None) -> list[dict]:
|
||||
field_pages = getattr(form, '_field_page_keys', {})
|
||||
grouped = {key: [] for key in OFFBOARDING_PAGE_ORDER}
|
||||
for field_name in form.fields.keys():
|
||||
section_key = field_pages.get(field_name, 'abschluss')
|
||||
if section_key not in grouped:
|
||||
section_key = 'abschluss'
|
||||
grouped[section_key].append(form[field_name])
|
||||
visible_keys = visible_section_keys or set(OFFBOARDING_PAGE_ORDER)
|
||||
return [
|
||||
{
|
||||
'key': key,
|
||||
'title': OFFBOARDING_SECTION_META[key]['title'],
|
||||
'subtitle': OFFBOARDING_SECTION_META[key]['subtitle'],
|
||||
'fields': grouped[key],
|
||||
}
|
||||
for key in OFFBOARDING_PAGE_ORDER
|
||||
if key in visible_keys and grouped[key]
|
||||
]
|
||||
|
||||
|
||||
def _ops_summary_for_user(user) -> dict[str, object]:
|
||||
can_view_jobs = user_has_capability(user, 'view_job_monitor')
|
||||
can_manage_backups = user_has_capability(user, 'manage_backups')
|
||||
summary: dict[str, object] = {
|
||||
'show': can_view_jobs or can_manage_backups,
|
||||
'can_view_jobs': can_view_jobs,
|
||||
'can_manage_backups': can_manage_backups,
|
||||
'failed_count_24h': 0,
|
||||
'started_count_24h': 0,
|
||||
'success_count_24h': 0,
|
||||
'recent_failed_logs': [],
|
||||
'backup_health': latest_backup_health_snapshot() if can_manage_backups else None,
|
||||
}
|
||||
if not can_view_jobs:
|
||||
return summary
|
||||
|
||||
since = timezone.now() - timedelta(hours=24)
|
||||
logs = AsyncTaskLog.objects.filter(started_at__gte=since)
|
||||
counts = {
|
||||
row['status']: row['count']
|
||||
for row in logs.values('status').annotate(count=Count('id'))
|
||||
}
|
||||
summary['failed_count_24h'] = counts.get('failed', 0)
|
||||
summary['started_count_24h'] = counts.get('started', 0)
|
||||
summary['success_count_24h'] = counts.get('succeeded', 0)
|
||||
summary['recent_failed_logs'] = list(
|
||||
AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5]
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||
@@ -957,13 +557,24 @@ def backup_recovery_page(request):
|
||||
@_require_capability('manage_backups')
|
||||
@require_POST
|
||||
def create_backup_from_admin(request):
|
||||
return create_backup_from_admin_impl(request, audit_fn=_audit)
|
||||
return create_backup_from_admin_impl(
|
||||
request,
|
||||
audit_fn=_audit,
|
||||
notify_user_fn=notify_user,
|
||||
create_backup_bundle_fn=create_backup_bundle,
|
||||
)
|
||||
|
||||
|
||||
@_require_capability('manage_backups')
|
||||
@require_POST
|
||||
def verify_backup_from_admin(request, backup_name: str):
|
||||
return verify_backup_from_admin_impl(request, backup_name, audit_fn=_audit)
|
||||
return verify_backup_from_admin_impl(
|
||||
request,
|
||||
backup_name,
|
||||
audit_fn=_audit,
|
||||
notify_user_fn=notify_user,
|
||||
verify_backup_bundle_fn=verify_backup_bundle,
|
||||
)
|
||||
|
||||
|
||||
@_require_capability('manage_backups')
|
||||
|
||||
Reference in New Issue
Block a user