snapshot: preserve dynamic form builder parity and presets

This commit is contained in:
Md Bayazid Bostame
2026-03-27 12:30:10 +01:00
parent aa54f41731
commit e929e7509b
12 changed files with 2097 additions and 505 deletions

View File

@@ -37,13 +37,22 @@ from .branding import get_branding_email_copy, get_company_email_domain, get_def
from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
from .form_builder import (
DEFAULT_FIELD_ORDER,
FORM_PRESETS,
LOCKED_FIELD_RULES,
LOCKED_SECTION_RULES,
OFFBOARDING_PAGE_LABELS,
OFFBOARDING_PAGE_ORDER,
ONBOARDING_DEFAULT_PAGE,
ONBOARDING_PAGE_LABELS,
ONBOARDING_PAGE_ORDER,
ensure_form_field_configs,
ensure_form_section_configs,
get_default_page_map,
get_section_labels,
get_section_order,
apply_form_preset,
)
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, 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
@@ -531,13 +540,14 @@ def _section_for_block(block: dict, field_pages: dict[str, str]) -> str:
return field_pages.get(fields[0].name, 'abschluss')
def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str]) -> list[dict]:
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}
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)
return [
{
'key': key,
@@ -546,6 +556,35 @@ def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str])
'blocks': grouped[key],
}
for key in ONBOARDING_SECTION_ORDER
if key in visible_keys
]
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]
]
@@ -1780,7 +1819,12 @@ def onboarding_create(request):
onboarding_blocks = _build_onboarding_layout(form)
field_pages = getattr(form, '_field_page_keys', {})
onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages)
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
}
onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages, visible_section_keys=visible_section_keys)
return render(
request,
@@ -1969,6 +2013,14 @@ def offboarding_create(request):
else:
form = OffboardingRequestForm(prefill_profile=selected_profile, initial={'search_query': search_query})
field_pages = getattr(form, '_field_page_keys', {})
section_configs = ensure_form_section_configs('offboarding')
visible_section_keys = {
key for key in OFFBOARDING_PAGE_ORDER
if key in LOCKED_SECTION_RULES.get('offboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible
}
offboarding_sections = _build_offboarding_sections(form, visible_section_keys=visible_section_keys)
return render(
request,
'workflows/offboarding_form.html',
@@ -1980,6 +2032,7 @@ def offboarding_create(request):
'saved': request.GET.get('saved') == '1',
'saved_request_id': request.GET.get('id', ''),
'portal_email_domain': get_company_email_domain(),
'offboarding_sections': offboarding_sections,
},
)
@@ -1997,6 +2050,9 @@ def offboarding_success(request, request_id: int):
def form_builder_page(request):
language_code = get_language()
form_type = request.GET.get('form_type', 'onboarding')
anchor = (request.GET.get('anchor') or '').strip()
active_panel = (request.GET.get('panel') or '').strip()
active_subpanel = (request.GET.get('subpanel') or '').strip()
if form_type not in DEFAULT_FIELD_ORDER:
form_type = 'onboarding'
option_category = request.GET.get('option_category', 'department')
@@ -2017,7 +2073,7 @@ def form_builder_page(request):
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}")
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=options#builder-content")
action = request.POST.get('builder_action', '')
if action == 'add_option':
@@ -2068,7 +2124,7 @@ def form_builder_page(request):
option.save(update_fields=['label', 'label_en', 'value', 'is_active', 'sort_order'])
except IntegrityError:
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}")
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}&panel=builder-content&subpanel=options#builder-content")
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.')
@@ -2087,7 +2143,67 @@ 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.')
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}")
elif action == 'save_field_rules':
field_ids = request.POST.getlist('field_rule_ids')
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
updated = 0
for raw_id in field_ids:
cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
if cfg.field_name in locked_fields:
cfg.is_visible = True
cfg.is_required = None
else:
cfg.is_visible = request.POST.get(f'is_visible_{cfg.id}') == 'on'
required_mode = (request.POST.get(f'is_required_{cfg.id}') or '').strip()
cfg.is_required = True if required_mode == 'required' else False if required_mode == 'optional' else None
cfg.save(update_fields=['is_visible', 'is_required'])
updated += 1
_audit(request, 'form_field_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
messages.success(request, 'Feldregeln wurden gespeichert.')
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())
updated = 0
for section_key, cfg in section_configs.items():
if section_key in locked_sections:
if not cfg.is_visible:
cfg.is_visible = True
cfg.save(update_fields=['is_visible'])
continue
cfg.is_visible = request.POST.get(f'section_visible_{section_key}') == 'on'
cfg.save(update_fields=['is_visible'])
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.')
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'
_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'}:
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 {'save_field_rules', 'save_section_rules'}:
active_panel = 'builder-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 anchor == 'builder-content' or active_panel == 'builder-content':
redirect_target += "#builder-content"
return redirect(redirect_target)
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
existing_names = list(
@@ -2100,12 +2216,17 @@ def form_builder_page(request):
default_names.append(name)
ensure_form_field_configs(form_type, default_names)
section_configs = ensure_form_section_configs(form_type)
section_order = get_section_order(form_type)
section_labels = get_section_labels(form_type)
default_page_map = get_default_page_map(form_type)
configs = list(
FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name')
)
labels = _form_field_labels(form_type)
locked = LOCKED_FIELD_RULES.get(form_type, set())
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
if form_type == 'onboarding':
columns = [
@@ -2131,27 +2252,127 @@ def form_builder_page(request):
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
'page_key': page_key,
}
)
else:
columns = [
{
'key': 'all',
'title': 'Offboarding Felder',
'items': [
{
'field_name': cfg.field_name,
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
'label_en': cfg.label_override_en,
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
}
for cfg in configs
],
'key': key,
'title': section_labels.get(key, key),
'items': [],
}
for key in section_order
]
column_by_key = {c['key']: c for c in columns}
fallback = section_order[-1] if section_order else 'all'
for cfg in configs:
page_key = cfg.page_key or default_page_map.get(cfg.field_name, fallback)
if page_key not in column_by_key:
page_key = fallback
column_by_key[page_key]['items'].append(
{
'field_name': cfg.field_name,
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
'label_en': cfg.label_override_en,
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
'page_key': page_key,
}
)
section_rule_items = []
if section_order:
fallback_section = section_order[-1] if section_order else ''
for key in section_order:
cfg = section_configs.get(key)
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]),
}
)
field_rule_items = []
for cfg in configs:
page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '')
field_rule_items.append(
{
'id': cfg.id,
'field_name': cfg.field_name,
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'page_key': page_key,
'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,
}
)
field_rule_groups = []
if section_order:
grouped_rules = {key: [] for key in section_order}
for item in field_rule_items:
grouped_rules.setdefault(item['page_key'], []).append(item)
for key in section_order:
field_rule_groups.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': grouped_rules.get(key, []),
}
)
field_text_groups = []
if section_order:
grouped_texts = {key: [] for key in section_order}
for cfg in configs:
page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '')
grouped_texts.setdefault(page_key, []).append(cfg)
for key in section_order:
field_text_groups.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': grouped_texts.get(key, []),
}
)
preview_sections = []
if section_order:
field_rule_group_map = {group['key']: group['items'] for group in field_rule_groups}
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
visible_items = [
item for item in field_rule_group_map.get(key, [])
if item['locked'] or item['is_visible']
]
if section_visible:
preview_sections.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': visible_items,
}
)
locked_field_count = len([item for item in field_rule_items if item['locked']])
hidden_field_count = len([item for item in field_rule_items if not item['is_visible']])
configurable_field_count = len(field_rule_items) - locked_field_count
hidden_section_count = len([item for item in section_rule_items if not item['is_visible']]) if section_rule_items else 0
builder_summary = {
'locked_field_count': locked_field_count,
'configurable_field_count': configurable_field_count,
'hidden_field_count': hidden_field_count,
'hidden_section_count': hidden_section_count,
}
return render(
request,
@@ -2164,6 +2385,15 @@ def form_builder_page(request):
'selected_option_category': option_category,
'option_items': FormOption.objects.filter(category=option_category).order_by('sort_order', 'label'),
'field_text_items': configs,
'field_rule_items': field_rule_items,
'field_rule_groups': field_rule_groups,
'field_text_groups': field_text_groups,
'preview_sections': preview_sections,
'section_rule_items': section_rule_items,
'builder_summary': builder_summary,
'active_panel': active_panel,
'active_subpanel': active_subpanel,
'available_presets': FORM_PRESETS.get(form_type, {}),
},
)