snapshot: preserve dynamic builder and section ordering work

This commit is contained in:
Md Bayazid Bostame
2026-03-27 16:54:11 +01:00
parent fdc27f2123
commit 30877ed8ee
13 changed files with 1391 additions and 365 deletions

View File

@@ -45,20 +45,20 @@ from .form_builder import (
OFFBOARDING_PAGE_LABELS,
OFFBOARDING_PAGE_ORDER,
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_custom_section_configs,
get_default_page_map,
get_section_definitions,
get_section_labels,
get_section_order,
apply_form_preset,
)
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 .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, 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
@@ -106,7 +106,6 @@ ONBOARDING_GROUPS = {
'extra-software-box': ['additional_software_multi', 'additional_software'],
'extra-access-box': ['additional_access_text'],
'successor-box': ['successor_name', 'inherit_phone_number_choice'],
'phone-box': ['phone_number_choice'],
}
ONBOARDING_INLINE_CHECKS = {'order_business_cards', 'agreement_confirm'}
@@ -119,7 +118,6 @@ ONBOARDING_CHECKBOX_LISTS = {
'needed_workspace_groups_multi',
'needed_resources_multi',
}
ONBOARDING_SECTION_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
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')},
@@ -602,21 +600,24 @@ def _section_for_block(block: dict, field_pages: dict[str, str]) -> str:
def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str], visible_section_keys: set[str] | None = None) -> list[dict]:
grouped = {key: [] for key in ONBOARDING_SECTION_ORDER}
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(ONBOARDING_SECTION_ORDER)
visible_keys = visible_section_keys or set(section_order)
return [
{
'key': key,
'title': ONBOARDING_SECTION_META[key]['title'],
'subtitle': ONBOARDING_SECTION_META[key]['subtitle'],
'title': section_titles.get(key, ONBOARDING_SECTION_META.get(key, {}).get('title', key)),
'subtitle': ONBOARDING_SECTION_META.get(key, {}).get('subtitle', ''),
'blocks': grouped[key],
}
for key in ONBOARDING_SECTION_ORDER
for key in section_order
if key in visible_keys
]
@@ -1930,10 +1931,14 @@ def onboarding_create(request):
onboarding_blocks = _build_onboarding_layout(form)
field_pages = getattr(form, '_field_page_keys', {})
section_configs = ensure_form_section_configs('onboarding')
visible_section_keys = {
key for key in ONBOARDING_SECTION_ORDER
if key in LOCKED_SECTION_RULES.get('onboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible
}
visible_section_keys = set()
for section in get_section_definitions('onboarding'):
key = section['key']
if section.get('is_custom'):
if section.get('is_active', True):
visible_section_keys.add(key)
elif key in LOCKED_SECTION_RULES.get('onboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible:
visible_section_keys.add(key)
onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages, visible_section_keys=visible_section_keys)
onboarding_conditional_rules = _normalized_conditional_rule_payload('onboarding')
@@ -2162,9 +2167,11 @@ def offboarding_success(request, request_id: int):
def form_builder_page(request):
language_code = get_language()
form_type = request.GET.get('form_type', 'onboarding')
can_override_locked_builder_rules = get_user_role_key(request.user) == ROLE_PLATFORM_OWNER
anchor = (request.GET.get('anchor') or '').strip()
active_panel = (request.GET.get('panel') or '').strip()
active_subpanel = (request.GET.get('subpanel') or '').strip()
active_rules_panel = (request.GET.get('rules_panel') or '').strip()
if form_type not in DEFAULT_FIELD_ORDER:
form_type = 'onboarding'
option_category = request.GET.get('option_category', 'department')
@@ -2267,6 +2274,54 @@ 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_section' and form_type == 'onboarding':
title = (request.POST.get('custom_section_title') or '').strip()
title_en = (request.POST.get('custom_section_title_en') or '').strip()
sort_order_raw = (request.POST.get('custom_section_sort_order') or '').strip()
if not title:
messages.error(request, 'Bitte einen Titel für den benutzerdefinierten Abschnitt angeben.')
else:
section_key_base = build_custom_field_key(title)
section_key = section_key_base
suffix = 2
while FormCustomSectionConfig.objects.filter(form_type=form_type, section_key=section_key).exists():
section_key = f'{section_key_base}_{suffix}'
suffix += 1
try:
sort_order = int(sort_order_raw or 0)
except ValueError:
sort_order = 0
FormCustomSectionConfig.objects.create(
form_type=form_type,
section_key=section_key,
sort_order=max(0, sort_order),
title=title,
title_en=title_en,
is_active=True,
)
_audit(request, 'form_custom_section_added', target_type='form_custom_section', target_label=title, details={'form_type': form_type, 'section_key': section_key})
messages.success(request, 'Benutzerdefinierter Abschnitt wurde hinzugefügt.')
elif action == 'save_custom_sections' and form_type == 'onboarding':
section_ids = request.POST.getlist('custom_section_ids')
updated = 0
for raw_id in section_ids:
cfg = FormCustomSectionConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
try:
sort_order = int((request.POST.get(f'custom_section_sort_order_{cfg.id}') or '').strip() or cfg.sort_order)
except ValueError:
sort_order = cfg.sort_order
cfg.title = (request.POST.get(f'custom_section_title_{cfg.id}') or '').strip() or cfg.title
cfg.title_en = (request.POST.get(f'custom_section_title_en_{cfg.id}') or '').strip()
cfg.is_active = request.POST.get(f'custom_section_is_active_{cfg.id}') == 'on'
cfg.sort_order = max(0, sort_order)
cfg.save(update_fields=['title', 'title_en', 'is_active', 'sort_order'])
updated += 1
_audit(request, 'form_custom_sections_saved', target_type='form_custom_section', target_label=form_type, details={'count': updated})
messages.success(request, 'Benutzerdefinierte Abschnitte 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()
@@ -2360,7 +2415,7 @@ def form_builder_page(request):
cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
if cfg.field_name in locked_fields:
if cfg.field_name in locked_fields and not can_override_locked_builder_rules:
cfg.is_visible = True
cfg.is_required = None
else:
@@ -2375,9 +2430,27 @@ def form_builder_page(request):
elif action == 'save_section_rules' and form_type in {'onboarding', 'offboarding'}:
section_configs = ensure_form_section_configs(form_type)
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
posted_order = request.POST.getlist('section_order')
next_sort_order = 0
updated = 0
for section_key in posted_order:
cfg = section_configs.get(section_key)
if cfg is not None:
if cfg.sort_order != next_sort_order:
cfg.sort_order = next_sort_order
cfg.save(update_fields=['sort_order'])
updated += 1
next_sort_order += 1
continue
if form_type == 'onboarding':
custom_cfg = FormCustomSectionConfig.objects.filter(form_type=form_type, section_key=section_key).first()
if custom_cfg and custom_cfg.sort_order != next_sort_order:
custom_cfg.sort_order = next_sort_order
custom_cfg.save(update_fields=['sort_order'])
updated += 1
next_sort_order += 1
for section_key, cfg in section_configs.items():
if section_key in locked_sections:
if section_key in locked_sections and not can_override_locked_builder_rules:
if not cfg.is_visible:
cfg.is_visible = True
cfg.save(update_fields=['is_visible'])
@@ -2385,6 +2458,11 @@ def form_builder_page(request):
cfg.is_visible = request.POST.get(f'section_visible_{section_key}') == 'on'
cfg.save(update_fields=['is_visible'])
updated += 1
if form_type == 'onboarding':
for cfg in FormCustomSectionConfig.objects.filter(form_type=form_type):
cfg.is_active = request.POST.get(f'section_visible_{cfg.section_key}') == 'on'
cfg.save(update_fields=['is_active'])
updated += 1
_audit(request, 'form_section_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
messages.success(request, 'Abschnittsregeln wurden gespeichert.')
@@ -2412,13 +2490,14 @@ def form_builder_page(request):
elif action == 'apply_preset':
preset_key = (request.POST.get('preset_key') or '').strip()
if apply_form_preset(form_type, preset_key):
active_panel = 'builder-preview'
active_panel = 'builder-content'
active_subpanel = 'preview'
_audit(request, 'form_preset_applied', target_type='form_config', target_label=form_type, details={'preset': preset_key})
messages.success(request, 'Preset wurde angewendet.')
else:
messages.error(request, 'Preset konnte nicht angewendet werden.')
if action in {'add_option', 'save_options', 'save_field_texts', 'add_custom_field', 'save_custom_fields'}:
if action in {'add_option', 'save_options', 'save_field_texts', 'add_custom_field', 'save_custom_fields', 'add_custom_section', 'save_custom_sections'}:
active_panel = 'builder-content'
if action in {'add_option', 'save_options'}:
active_subpanel = 'options'
@@ -2426,13 +2505,23 @@ def form_builder_page(request):
active_subpanel = 'field-texts'
elif action in {'add_custom_field', 'save_custom_fields'}:
active_subpanel = 'custom-fields'
elif action in {'add_custom_section', 'save_custom_sections'}:
active_subpanel = 'custom-sections'
elif action in {'save_field_rules', 'save_section_rules', 'save_conditional_rules'}:
active_panel = 'builder-rules'
if action == 'save_section_rules':
active_rules_panel = 'section-rules'
elif action == 'save_field_rules':
active_rules_panel = 'field-rules'
elif action == 'save_conditional_rules':
active_rules_panel = 'conditional-rules'
redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}"
if active_panel:
redirect_target += f"&panel={active_panel}"
if active_subpanel:
redirect_target += f"&subpanel={active_subpanel}"
if active_rules_panel:
redirect_target += f"&rules_panel={active_rules_panel}"
if anchor == 'builder-content' or active_panel == 'builder-content':
redirect_target += "#builder-content"
return redirect(redirect_target)
@@ -2450,8 +2539,9 @@ def form_builder_page(request):
ensure_form_field_configs(form_type, default_names)
section_configs = ensure_form_section_configs(form_type)
conditional_rule_configs = ensure_form_conditional_rule_configs(form_type) if form_type == 'onboarding' else {}
section_order = get_section_order(form_type)
section_labels = get_section_labels(form_type)
section_definitions = get_section_definitions(form_type, include_inactive_custom=True)
section_order = [item['key'] for item in section_definitions]
section_labels = {item['key']: item['title'] for item in section_definitions}
default_page_map = get_default_page_map(form_type)
configs = list(
@@ -2461,15 +2551,16 @@ def form_builder_page(request):
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'))
custom_section_configs = get_custom_section_configs(form_type, include_inactive=True)
if form_type == 'onboarding':
columns = [
{
'key': key,
'title': ONBOARDING_PAGE_LABELS.get(key, key),
'title': section_labels.get(key, key),
'items': [],
}
for key in ONBOARDING_PAGE_ORDER
for key in section_order
]
column_by_key = {c['key']: c for c in columns}
fallback = 'abschluss'
@@ -2564,15 +2655,20 @@ def form_builder_page(request):
section_rule_items = []
if section_order:
fallback_section = section_order[-1] if section_order else ''
custom_section_map = {cfg.section_key: cfg for cfg in custom_section_configs}
for key in section_order:
cfg = section_configs.get(key)
custom_cfg = custom_section_map.get(key)
is_custom = custom_cfg is not None
section_rule_items.append(
{
'key': key,
'title': section_labels.get(key, key),
'is_visible': True if not cfg else cfg.is_visible,
'locked': key in locked_sections,
'field_count': len([c for c in configs if (c.page_key or default_page_map.get(c.field_name, fallback_section)) == key]),
'is_visible': bool(custom_cfg.is_active) if is_custom else (True if not cfg else cfg.is_visible),
'locked': False if is_custom else (key in locked_sections and not can_override_locked_builder_rules),
'is_custom': is_custom,
'sort_order': custom_cfg.sort_order if is_custom else (cfg.sort_order if cfg else 0),
'field_count': len([c for c in configs if (c.page_key or default_page_map.get(c.field_name, fallback_section)) == key]) + len([c for c in custom_field_configs if c.section_key == key]),
}
)
@@ -2588,7 +2684,7 @@ def form_builder_page(request):
'page_label': section_labels.get(page_key, page_key) if section_order else '',
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
'locked': cfg.field_name in locked and not can_override_locked_builder_rules,
}
)
@@ -2659,7 +2755,6 @@ def form_builder_page(request):
'extra-software-box': _('Zusätzliche Software'),
'extra-access-box': _('Zusätzliche Zugänge'),
'successor-box': _('Nachfolge'),
'phone-box': _('Direktwahl'),
}
conditional_target_descriptions = {
'business-card-box': _('Steuert die Detailfelder für Visitenkarten.'),
@@ -2669,7 +2764,6 @@ def form_builder_page(request):
'extra-software-box': _('Steuert zusätzliche Software-Felder.'),
'extra-access-box': _('Steuert zusätzliche Zugangsangaben.'),
'successor-box': _('Steuert Nachfolge- und Übernahmefelder.'),
'phone-box': _('Steuert die manuelle Direktwahl.'),
}
for target_key, cfg in conditional_rule_configs.items():
clauses = list(cfg.clauses or [])
@@ -2704,7 +2798,8 @@ def form_builder_page(request):
for key in section_order:
section_cfg = section_configs.get(key)
section_locked = key in locked_sections
section_visible = True if section_locked or not section_cfg else section_cfg.is_visible
custom_section = next((cfg for cfg in custom_section_configs if cfg.section_key == key), None)
section_visible = bool(custom_section.is_active) if custom_section else (True if section_locked or not section_cfg else section_cfg.is_visible)
visible_items = [
item for item in field_rule_group_map.get(key, [])
if item['locked'] or item['is_visible']
@@ -2738,6 +2833,7 @@ def form_builder_page(request):
'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]),
'custom_section_count': len([cfg for cfg in custom_section_configs if cfg.is_active]),
}
return render(
@@ -2760,9 +2856,12 @@ def form_builder_page(request):
'conditional_rule_items': conditional_rule_items,
'custom_field_groups': custom_field_groups,
'custom_field_type_choices': _translate_choice_list(FormCustomFieldConfig.FIELD_TYPE_CHOICES),
'custom_section_items': custom_section_configs,
'active_panel': active_panel,
'active_subpanel': active_subpanel,
'active_rules_panel': active_rules_panel,
'available_presets': FORM_PRESETS.get(form_type, {}),
'can_override_locked_builder_rules': can_override_locked_builder_rules,
},
)
@@ -3213,10 +3312,8 @@ def form_builder_save_order(request):
allowed_names = {cfg.field_name for cfg in configs} | {f'custom__{cfg.field_key}' for cfg in custom_configs}
seen = set()
if form_type == 'onboarding':
allowed_columns = ONBOARDING_PAGE_ORDER
else:
allowed_columns = OFFBOARDING_PAGE_ORDER
allowed_columns = get_section_order(form_type)
fallback_section = allowed_columns[-1] if allowed_columns else ''
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}
@@ -3248,20 +3345,14 @@ def form_builder_save_order(request):
cfg = name_to_cfg[name]
cfg.sort_order = sort_order
sort_order += 1
if form_type == 'onboarding':
cfg.page_key = cfg.page_key or ONBOARDING_DEFAULT_PAGE.get(name, 'abschluss')
else:
cfg.page_key = cfg.page_key or default_page_map.get(name, OFFBOARDING_PAGE_ORDER[-1])
cfg.page_key = cfg.page_key or default_page_map.get(name, fallback_section)
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]
cfg.section_key = cfg.section_key or fallback_section
FormFieldConfig.objects.bulk_update(configs, ['sort_order', 'page_key'])
if custom_configs: