snapshot: preserve custom field parity across forms timeline and pdf

This commit is contained in:
Md Bayazid Bostame
2026-03-27 13:21:25 +01:00
parent 2e5e941d41
commit fdc27f2123
20 changed files with 2294 additions and 545 deletions

View File

@@ -10,6 +10,7 @@ from django.conf import settings
from django.db import connection
from django.db import IntegrityError
from django.db.models import Q
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.contrib import messages
from django.contrib.auth import get_user_model, login as auth_login
@@ -46,15 +47,18 @@ from .form_builder import (
ONBOARDING_DEFAULT_PAGE,
ONBOARDING_PAGE_LABELS,
ONBOARDING_PAGE_ORDER,
build_custom_field_key,
custom_field_target_key,
ensure_form_field_configs,
ensure_form_conditional_rule_configs,
ensure_form_section_configs,
get_custom_field_configs,
get_default_page_map,
get_section_labels,
get_section_order,
apply_form_preset,
)
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, 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
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability
@@ -105,8 +109,6 @@ ONBOARDING_GROUPS = {
'phone-box': ['phone_number_choice'],
}
ONBOARDING_HIDDEN_BY_DEFAULT = set(DEFAULT_CONDITIONAL_RULES.get('onboarding', {}).keys())
ONBOARDING_INLINE_CHECKS = {'order_business_cards', 'agreement_confirm'}
ONBOARDING_CHECKBOX_LISTS = {
'needed_devices_multi',
@@ -144,6 +146,10 @@ def _normalized_conditional_rule_payload(form_type: str) -> dict[str, dict]:
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:
@@ -450,6 +456,36 @@ def _request_status_label(status_key: str, language_code: str | None = None) ->
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'),
@@ -505,6 +541,7 @@ def _build_onboarding_layout(form) -> list[dict]:
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()
@@ -529,7 +566,7 @@ def _build_onboarding_layout(form) -> list[dict]:
{
'kind': 'group',
'id': group_id,
'hidden_default': group_id in ONBOARDING_HIDDEN_BY_DEFAULT,
'hidden_default': group_id in conditional_target_keys,
'fields': group_fields,
}
)
@@ -537,6 +574,18 @@ def _build_onboarding_layout(form) -> list[dict]:
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)
@@ -600,10 +649,42 @@ def _build_offboarding_sections(form, visible_section_keys: set[str] | None = No
]
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')
role_key = get_user_role_key(request.user)
ops_summary = _ops_summary_for_user(request.user)
return render(
request,
'workflows/home.html',
@@ -614,6 +695,7 @@ def home(request):
'role_label': get_user_role_label(request.user),
'role_key': role_key,
'portal_app_sections': build_portal_app_sections(request.user),
'ops_summary': ops_summary,
},
)
@@ -641,6 +723,13 @@ def job_monitor_page(request):
logs = logs.filter(task_name=task_filter)
logs = logs.order_by('-started_at', '-id')[:200]
task_names = list(AsyncTaskLog.objects.order_by('task_name').values_list('task_name', flat=True).distinct())
since = timezone.now() - timedelta(hours=24)
recent_logs = AsyncTaskLog.objects.filter(started_at__gte=since)
counts = {
row['status']: row['count']
for row in recent_logs.values('status').annotate(count=Count('id'))
}
recent_failed = list(AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5])
return render(
request,
'workflows/job_monitor.html',
@@ -650,6 +739,12 @@ def job_monitor_page(request):
'task_filter': task_filter,
'task_names': task_names,
'status_choices': [('started', _('Gestartet')), ('succeeded', _('Erfolgreich')), ('failed', _('Fehlgeschlagen'))],
'job_summary': {
'started_count_24h': counts.get('started', 0),
'success_count_24h': counts.get('succeeded', 0),
'failed_count_24h': counts.get('failed', 0),
'recent_failed': recent_failed,
},
},
)
@@ -1469,6 +1564,7 @@ def request_timeline_page(request, kind: str, request_id: int):
return redirect('requests_dashboard')
request_label = _request_target_label(obj, kind)
custom_field_details = _request_custom_field_details(obj, kind, getattr(request, 'LANGUAGE_CODE', None))
audit_rows = list(
AdminAuditLog.objects.select_related('actor')
.filter(target_type__in=[kind, 'request'])
@@ -1483,6 +1579,7 @@ def request_timeline_page(request, kind: str, request_id: int):
'title': _('Anfrage erstellt'),
'summary': request_label,
'meta': _('Status: %(status)s') % {'status': obj.get_processing_status_display()},
'details': {item['label']: item['value'] for item in custom_field_details},
}
]
@@ -1569,6 +1666,7 @@ def request_timeline_page(request, kind: str, request_id: int):
'request_obj': obj,
'request_label': request_label,
'timeline_rows': timeline_rows,
'custom_field_details': custom_field_details,
'contract_start': getattr(obj, 'contract_start', None),
'handover_date': getattr(obj, 'handover_date', None),
},
@@ -2076,6 +2174,7 @@ def form_builder_page(request):
if request.method == 'POST':
delete_option_id = request.POST.get('delete_option_id', '').strip()
delete_custom_field_id = request.POST.get('delete_custom_field_id', '').strip()
if delete_option_id:
option = FormOption.objects.filter(id=delete_option_id).first()
if not option:
@@ -2088,6 +2187,17 @@ def form_builder_page(request):
_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}&panel=builder-content&subpanel=options#builder-content")
if delete_custom_field_id:
custom_field = FormCustomFieldConfig.objects.filter(id=delete_custom_field_id, form_type=form_type).first()
if not custom_field:
messages.error(request, 'Benutzerdefiniertes Feld nicht gefunden.')
else:
deleted_label = custom_field.label
deleted_id = custom_field.id
custom_field.delete()
_audit(request, 'form_custom_field_deleted', target_type='form_custom_field', target_id=deleted_id, target_label=deleted_label)
messages.success(request, 'Benutzerdefiniertes Feld wurde gelöscht.')
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content")
action = request.POST.get('builder_action', '')
if action == 'add_option':
@@ -2157,6 +2267,91 @@ def form_builder_page(request):
_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.')
elif action == 'add_custom_field':
label = (request.POST.get('custom_label') or '').strip()
label_en = (request.POST.get('custom_label_en') or '').strip()
section_key = (request.POST.get('custom_section_key') or '').strip()
field_type = (request.POST.get('custom_field_type') or '').strip()
sort_order_raw = (request.POST.get('custom_sort_order') or '').strip()
help_text = (request.POST.get('custom_help_text') or '').strip()
help_text_en = (request.POST.get('custom_help_text_en') or '').strip()
select_options = (request.POST.get('custom_select_options') or '').strip()
select_options_en = (request.POST.get('custom_select_options_en') or '').strip()
section_choices = {key for key in get_section_order(form_type)}
field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES}
if not label:
messages.error(request, 'Bitte eine Bezeichnung für das benutzerdefinierte Feld angeben.')
elif section_key not in section_choices:
messages.error(request, 'Ungültiger Abschnitt für das benutzerdefinierte Feld.')
elif field_type not in field_type_choices:
messages.error(request, 'Ungültiger Feldtyp.')
elif field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not select_options:
messages.error(request, 'Auswahlfelder benötigen mindestens eine Option.')
else:
field_key_base = build_custom_field_key(label)
field_key = field_key_base
suffix = 2
while FormCustomFieldConfig.objects.filter(form_type=form_type, field_key=field_key).exists():
field_key = f'{field_key_base}_{suffix}'
suffix += 1
try:
sort_order = int(sort_order_raw or 0)
except ValueError:
sort_order = 0
FormCustomFieldConfig.objects.create(
form_type=form_type,
field_key=field_key,
section_key=section_key,
sort_order=max(0, sort_order),
field_type=field_type,
is_active=True,
is_required=request.POST.get('custom_is_required') == 'on',
label=label,
label_en=label_en,
help_text=help_text,
help_text_en=help_text_en,
select_options=select_options,
select_options_en=select_options_en,
)
_audit(request, 'form_custom_field_added', target_type='form_custom_field', target_label=label, details={'form_type': form_type, 'field_type': field_type, 'section_key': section_key})
messages.success(request, 'Benutzerdefiniertes Feld wurde hinzugefügt.')
elif action == 'save_custom_fields':
custom_ids = request.POST.getlist('custom_field_ids')
updated = 0
section_choices = {key for key in get_section_order(form_type)}
field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES}
for raw_id in custom_ids:
cfg = FormCustomFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
field_type = (request.POST.get(f'custom_field_type_{cfg.id}') or '').strip()
section_key = (request.POST.get(f'custom_section_key_{cfg.id}') or '').strip()
try:
sort_order = int((request.POST.get(f'custom_sort_order_{cfg.id}') or '').strip() or cfg.sort_order)
except ValueError:
sort_order = cfg.sort_order
cfg.label = (request.POST.get(f'custom_label_{cfg.id}') or '').strip() or cfg.label
cfg.label_en = (request.POST.get(f'custom_label_en_{cfg.id}') or '').strip()
cfg.help_text = (request.POST.get(f'custom_help_text_{cfg.id}') or '').strip()
cfg.help_text_en = (request.POST.get(f'custom_help_text_en_{cfg.id}') or '').strip()
cfg.is_required = request.POST.get(f'custom_is_required_{cfg.id}') == 'on'
cfg.is_active = request.POST.get(f'custom_is_active_{cfg.id}') == 'on'
if field_type in field_type_choices:
cfg.field_type = field_type
if section_key in section_choices:
cfg.section_key = section_key
cfg.sort_order = max(0, sort_order)
cfg.select_options = (request.POST.get(f'custom_select_options_{cfg.id}') or '').strip()
cfg.select_options_en = (request.POST.get(f'custom_select_options_en_{cfg.id}') or '').strip()
if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not cfg.select_options:
messages.error(request, f'Auswahlfeld "{cfg.label}" benötigt mindestens eine Option.')
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content")
cfg.save()
updated += 1
_audit(request, 'form_custom_fields_saved', target_type='form_custom_field', target_label=form_type, details={'count': updated})
messages.success(request, 'Benutzerdefinierte Felder wurden gespeichert.')
elif action == 'save_field_rules':
field_ids = request.POST.getlist('field_rule_ids')
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
@@ -2223,12 +2418,14 @@ def form_builder_page(request):
else:
messages.error(request, 'Preset konnte nicht angewendet werden.')
if action in {'add_option', 'save_options', 'save_field_texts'}:
if action in {'add_option', 'save_options', 'save_field_texts', 'add_custom_field', 'save_custom_fields'}:
active_panel = 'builder-content'
if action in {'add_option', 'save_options'}:
active_subpanel = 'options'
elif action == 'save_field_texts':
active_subpanel = 'field-texts'
elif action in {'add_custom_field', 'save_custom_fields'}:
active_subpanel = 'custom-fields'
elif action in {'save_field_rules', 'save_section_rules', 'save_conditional_rules'}:
active_panel = 'builder-rules'
redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}"
@@ -2263,6 +2460,7 @@ def form_builder_page(request):
labels = _form_field_labels(form_type)
locked = LOCKED_FIELD_RULES.get(form_type, set())
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
custom_field_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('section_key', 'sort_order', 'field_key'))
if form_type == 'onboarding':
columns = [
@@ -2289,8 +2487,30 @@ def form_builder_page(request):
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
'page_key': page_key,
'is_custom': False,
'sort_order': cfg.sort_order,
}
)
for cfg in custom_field_configs:
page_key = cfg.section_key or fallback
if page_key not in column_by_key:
page_key = fallback
column_by_key[page_key]['items'].append(
{
'field_name': f'custom__{cfg.field_key}',
'label': cfg.translated_label(language_code),
'label_de': cfg.label,
'label_en': cfg.label_en,
'is_visible': cfg.is_active,
'is_required': cfg.is_required,
'locked': False,
'page_key': page_key,
'is_custom': True,
'sort_order': cfg.sort_order,
}
)
for column in columns:
column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name']))
else:
columns = [
{
@@ -2316,8 +2536,30 @@ def form_builder_page(request):
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
'page_key': page_key,
'is_custom': False,
'sort_order': cfg.sort_order,
}
)
for cfg in custom_field_configs:
page_key = cfg.section_key or fallback
if page_key not in column_by_key:
page_key = fallback
column_by_key[page_key]['items'].append(
{
'field_name': f'custom__{cfg.field_key}',
'label': cfg.translated_label(language_code),
'label_de': cfg.label,
'label_en': cfg.label_en,
'is_visible': cfg.is_active,
'is_required': cfg.is_required,
'locked': False,
'page_key': page_key,
'is_custom': True,
'sort_order': cfg.sort_order,
}
)
for column in columns:
column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name']))
section_rule_items = []
if section_order:
@@ -2350,6 +2592,20 @@ def form_builder_page(request):
}
)
custom_field_groups = []
if section_order:
grouped_custom = {key: [] for key in section_order}
for cfg in custom_field_configs:
grouped_custom.setdefault(cfg.section_key, []).append(cfg)
for key in section_order:
custom_field_groups.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': grouped_custom.get(key, []),
}
)
field_rule_groups = []
if section_order:
grouped_rules = {key: [] for key in section_order}
@@ -2393,6 +2649,8 @@ def form_builder_page(request):
'inherit_phone_number_choice',
]:
conditional_field_choices.append((field_name, labels.get(field_name, field_name)))
for cfg in custom_field_configs:
conditional_field_choices.append((f'custom__{cfg.field_key}', cfg.translated_label(language_code)))
conditional_target_titles = {
'business-card-box': _('Visitenkarten-Details'),
'employment-end-box': _('Vertragsende'),
@@ -2417,16 +2675,26 @@ def form_builder_page(request):
clauses = list(cfg.clauses or [])
while len(clauses) < 2:
clauses.append({'field': '', 'operator': 'equals', 'value': ''})
if target_key.startswith('custom__'):
custom_field_key = target_key.replace('custom__', '', 1)
custom_field = next((item for item in custom_field_configs if item.field_key == custom_field_key), None)
target_title = custom_field.translated_label(language_code) if custom_field else target_key
target_description = _('Steuert die Sichtbarkeit dieses benutzerdefinierten Feldes.')
target_fields = [target_title]
else:
target_title = conditional_target_titles.get(target_key, target_key)
target_description = conditional_target_descriptions.get(target_key, '')
target_fields = [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])]
conditional_rule_items.append(
{
'target_key': target_key,
'title': conditional_target_titles.get(target_key, target_key),
'description': conditional_target_descriptions.get(target_key, ''),
'title': target_title,
'description': target_description,
'is_active': cfg.is_active,
'clauses': clauses[:2],
'field_choices': conditional_field_choices,
'operator_choices': CONDITIONAL_RULE_OPERATOR_CHOICES,
'target_fields': [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])],
'target_fields': target_fields,
}
)
@@ -2441,6 +2709,16 @@ def form_builder_page(request):
item for item in field_rule_group_map.get(key, [])
if item['locked'] or item['is_visible']
]
visible_items.extend(
[
{
'label': cfg.translated_label(language_code),
'locked': False,
}
for cfg in custom_field_configs
if cfg.section_key == key and cfg.is_active
]
)
if section_visible:
preview_sections.append(
{
@@ -2459,6 +2737,7 @@ def form_builder_page(request):
'configurable_field_count': configurable_field_count,
'hidden_field_count': hidden_field_count,
'hidden_section_count': hidden_section_count,
'custom_field_count': len([cfg for cfg in custom_field_configs if cfg.is_active]),
}
return render(
@@ -2479,6 +2758,8 @@ def form_builder_page(request):
'section_rule_items': section_rule_items,
'builder_summary': builder_summary,
'conditional_rule_items': conditional_rule_items,
'custom_field_groups': custom_field_groups,
'custom_field_type_choices': _translate_choice_list(FormCustomFieldConfig.FIELD_TYPE_CHOICES),
'active_panel': active_panel,
'active_subpanel': active_subpanel,
'available_presets': FORM_PRESETS.get(form_type, {}),
@@ -2921,22 +3202,24 @@ def form_builder_save_order(request):
form_type = payload.get('form_type')
if form_type not in DEFAULT_FIELD_ORDER:
return JsonResponse({'ok': False, 'error': 'Ungültiger Formulartyp.'}, status=400)
default_page_map = get_default_page_map(form_type)
columns = payload.get('columns')
if not isinstance(columns, dict):
return JsonResponse({'ok': False, 'error': 'Spalten-Daten fehlen.'}, status=400)
configs = list(FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name'))
allowed_names = {cfg.field_name for cfg in configs}
custom_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_key'))
allowed_names = {cfg.field_name for cfg in configs} | {f'custom__{cfg.field_key}' for cfg in custom_configs}
seen = set()
ordered_names = []
if form_type == 'onboarding':
allowed_columns = ONBOARDING_PAGE_ORDER
else:
allowed_columns = ['all']
allowed_columns = OFFBOARDING_PAGE_ORDER
name_to_cfg = {cfg.field_name: cfg for cfg in configs}
custom_name_to_cfg = {f'custom__{cfg.field_key}': cfg for cfg in custom_configs}
sort_order = 0
for column_key in allowed_columns:
@@ -2950,14 +3233,15 @@ def form_builder_save_order(request):
if name not in allowed_names or name in seen:
continue
seen.add(name)
ordered_names.append(name)
cfg = name_to_cfg[name]
cfg.sort_order = sort_order
sort_order += 1
if form_type == 'onboarding':
if name in name_to_cfg:
cfg = name_to_cfg[name]
cfg.sort_order = sort_order
cfg.page_key = column_key
else:
cfg.page_key = ''
cfg = custom_name_to_cfg[name]
cfg.sort_order = sort_order
cfg.section_key = column_key
sort_order += 1
missing = [cfg.field_name for cfg in configs if cfg.field_name not in seen]
for name in missing:
@@ -2967,11 +3251,24 @@ def form_builder_save_order(request):
if form_type == 'onboarding':
cfg.page_key = cfg.page_key or ONBOARDING_DEFAULT_PAGE.get(name, 'abschluss')
else:
cfg.page_key = ''
cfg.page_key = cfg.page_key or default_page_map.get(name, OFFBOARDING_PAGE_ORDER[-1])
missing_custom = [name for name in custom_name_to_cfg.keys() if name not in seen]
for name in missing_custom:
cfg = custom_name_to_cfg[name]
cfg.sort_order = sort_order
sort_order += 1
if form_type == 'onboarding':
cfg.section_key = cfg.section_key or 'abschluss'
else:
cfg.section_key = cfg.section_key or OFFBOARDING_PAGE_ORDER[-1]
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)})
if custom_configs:
FormCustomFieldConfig.objects.bulk_update(custom_configs, ['sort_order', 'section_key'])
saved_count = len(configs) + len(custom_configs)
_audit(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': saved_count})
return JsonResponse({'ok': True, 'saved_count': saved_count})
@_require_capability('manage_integrations')