snapshot: preserve dynamic form builder parity and presets
This commit is contained in:
@@ -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, {}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user