snapshot: split form builder config and runtime responsibilities
This commit is contained in:
@@ -1,626 +1,2 @@
|
|||||||
from collections import OrderedDict
|
from .form_builder_config import *
|
||||||
from django import forms
|
from .form_builder_runtime import *
|
||||||
from django.utils.text import slugify
|
|
||||||
from django.utils.translation import get_language, gettext_lazy as _
|
|
||||||
|
|
||||||
from .models import FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormSectionConfig
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_FIELD_ORDER = {
|
|
||||||
'onboarding': [
|
|
||||||
'first_name',
|
|
||||||
'last_name',
|
|
||||||
'full_name',
|
|
||||||
'gender',
|
|
||||||
'job_title',
|
|
||||||
'department',
|
|
||||||
'work_email',
|
|
||||||
'order_business_cards',
|
|
||||||
'business_card_name',
|
|
||||||
'business_card_title',
|
|
||||||
'business_card_email',
|
|
||||||
'business_card_phone',
|
|
||||||
'contract_start',
|
|
||||||
'employment_type',
|
|
||||||
'employment_end_date',
|
|
||||||
'handover_date',
|
|
||||||
'group_mailboxes_required_choice',
|
|
||||||
'group_mailboxes',
|
|
||||||
'needed_devices_multi',
|
|
||||||
'additional_hardware_needed_choice',
|
|
||||||
'additional_hardware_multi',
|
|
||||||
'additional_hardware_other',
|
|
||||||
'needed_software_multi',
|
|
||||||
'additional_software_needed_choice',
|
|
||||||
'additional_software_multi',
|
|
||||||
'additional_software',
|
|
||||||
'needed_accesses_multi',
|
|
||||||
'additional_access_needed_choice',
|
|
||||||
'additional_access_text',
|
|
||||||
'needed_workspace_groups_multi',
|
|
||||||
'needed_resources_multi',
|
|
||||||
'successor_required_choice',
|
|
||||||
'successor_name',
|
|
||||||
'inherit_phone_number_choice',
|
|
||||||
'phone_number_choice',
|
|
||||||
'additional_notes',
|
|
||||||
'signature_url',
|
|
||||||
'signature_image',
|
|
||||||
'onboarded_by_email',
|
|
||||||
'agreement_confirm',
|
|
||||||
],
|
|
||||||
'offboarding': [
|
|
||||||
'full_name',
|
|
||||||
'work_email',
|
|
||||||
'department',
|
|
||||||
'job_title',
|
|
||||||
'last_working_day',
|
|
||||||
'notes',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
ONBOARDING_PAGE_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
|
|
||||||
OFFBOARDING_PAGE_ORDER = ['mitarbeitende', 'austritt', 'abschluss']
|
|
||||||
ONBOARDING_PAGE_LABELS = {
|
|
||||||
'stammdaten': _('1. Stammdaten'),
|
|
||||||
'vertrag': _('2. Vertrag'),
|
|
||||||
'itsetup': _('3. IT-Setup'),
|
|
||||||
'abschluss': _('4. Abschluss'),
|
|
||||||
}
|
|
||||||
OFFBOARDING_PAGE_LABELS = {
|
|
||||||
'mitarbeitende': _('1. Mitarbeitende'),
|
|
||||||
'austritt': _('2. Austritt'),
|
|
||||||
'abschluss': _('3. Abschluss'),
|
|
||||||
}
|
|
||||||
CORE_SECTION_LABELS = {
|
|
||||||
'onboarding': ONBOARDING_PAGE_LABELS,
|
|
||||||
'offboarding': OFFBOARDING_PAGE_LABELS,
|
|
||||||
}
|
|
||||||
|
|
||||||
LOCKED_FIELD_RULES = {
|
|
||||||
'onboarding': {'full_name', 'work_email', 'contract_start', 'agreement_confirm'},
|
|
||||||
'offboarding': {'full_name', 'work_email', 'last_working_day'},
|
|
||||||
}
|
|
||||||
|
|
||||||
LOCKED_SECTION_RULES = {
|
|
||||||
'onboarding': {'stammdaten', 'vertrag', 'abschluss'},
|
|
||||||
'offboarding': {'mitarbeitende', 'austritt'},
|
|
||||||
}
|
|
||||||
|
|
||||||
ONBOARDING_DEFAULT_PAGE = {
|
|
||||||
'first_name': 'stammdaten',
|
|
||||||
'last_name': 'stammdaten',
|
|
||||||
'full_name': 'stammdaten',
|
|
||||||
'gender': 'stammdaten',
|
|
||||||
'job_title': 'stammdaten',
|
|
||||||
'department': 'stammdaten',
|
|
||||||
'work_email': 'stammdaten',
|
|
||||||
'order_business_cards': 'stammdaten',
|
|
||||||
'business_card_name': 'stammdaten',
|
|
||||||
'business_card_title': 'stammdaten',
|
|
||||||
'business_card_email': 'stammdaten',
|
|
||||||
'business_card_phone': 'stammdaten',
|
|
||||||
'contract_start': 'vertrag',
|
|
||||||
'employment_type': 'vertrag',
|
|
||||||
'employment_end_date': 'vertrag',
|
|
||||||
'handover_date': 'vertrag',
|
|
||||||
'group_mailboxes_required_choice': 'vertrag',
|
|
||||||
'group_mailboxes': 'vertrag',
|
|
||||||
'needed_devices_multi': 'itsetup',
|
|
||||||
'additional_hardware_needed_choice': 'itsetup',
|
|
||||||
'additional_hardware_multi': 'itsetup',
|
|
||||||
'additional_hardware_other': 'itsetup',
|
|
||||||
'needed_software_multi': 'itsetup',
|
|
||||||
'additional_software_needed_choice': 'itsetup',
|
|
||||||
'additional_software_multi': 'itsetup',
|
|
||||||
'additional_software': 'itsetup',
|
|
||||||
'needed_accesses_multi': 'itsetup',
|
|
||||||
'additional_access_needed_choice': 'itsetup',
|
|
||||||
'additional_access_text': 'itsetup',
|
|
||||||
'needed_workspace_groups_multi': 'itsetup',
|
|
||||||
'needed_resources_multi': 'itsetup',
|
|
||||||
'successor_required_choice': 'itsetup',
|
|
||||||
'successor_name': 'itsetup',
|
|
||||||
'inherit_phone_number_choice': 'itsetup',
|
|
||||||
'phone_number_choice': 'itsetup',
|
|
||||||
'additional_notes': 'abschluss',
|
|
||||||
'signature_url': 'abschluss',
|
|
||||||
'signature_image': 'abschluss',
|
|
||||||
'onboarded_by_email': 'abschluss',
|
|
||||||
'agreement_confirm': 'abschluss',
|
|
||||||
}
|
|
||||||
OFFBOARDING_DEFAULT_PAGE = {
|
|
||||||
'full_name': 'mitarbeitende',
|
|
||||||
'work_email': 'mitarbeitende',
|
|
||||||
'department': 'mitarbeitende',
|
|
||||||
'job_title': 'mitarbeitende',
|
|
||||||
'last_working_day': 'austritt',
|
|
||||||
'notes': 'abschluss',
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_CONDITIONAL_RULES = {
|
|
||||||
'onboarding': {
|
|
||||||
'business-card-box': {
|
|
||||||
'clauses': [{'field': 'order_business_cards', 'operator': 'checked', 'value': True}],
|
|
||||||
},
|
|
||||||
'employment-end-box': {
|
|
||||||
'clauses': [{'field': 'employment_type', 'operator': 'equals', 'value': 'befristet'}],
|
|
||||||
},
|
|
||||||
'group-mailboxes-box': {
|
|
||||||
'clauses': [{'field': 'group_mailboxes_required_choice', 'operator': 'equals', 'value': 'ja'}],
|
|
||||||
},
|
|
||||||
'extra-hardware-box': {
|
|
||||||
'clauses': [{'field': 'additional_hardware_needed_choice', 'operator': 'equals', 'value': 'ja'}],
|
|
||||||
},
|
|
||||||
'extra-software-box': {
|
|
||||||
'clauses': [{'field': 'additional_software_needed_choice', 'operator': 'equals', 'value': 'ja'}],
|
|
||||||
},
|
|
||||||
'extra-access-box': {
|
|
||||||
'clauses': [{'field': 'additional_access_needed_choice', 'operator': 'equals', 'value': 'ja'}],
|
|
||||||
},
|
|
||||||
'successor-box': {
|
|
||||||
'clauses': [{'field': 'successor_required_choice', 'operator': 'equals', 'value': 'ja'}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
CUSTOM_FIELD_PREFIX = 'custom__'
|
|
||||||
|
|
||||||
|
|
||||||
def get_section_order(form_type: str) -> list[str]:
|
|
||||||
return [item['key'] for item in get_section_definitions(form_type)]
|
|
||||||
|
|
||||||
|
|
||||||
def get_section_labels(form_type: str) -> dict[str, str]:
|
|
||||||
return {item['key']: item['title'] for item in get_section_definitions(form_type)}
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_page_map(form_type: str) -> dict[str, str]:
|
|
||||||
if form_type == 'onboarding':
|
|
||||||
return ONBOARDING_DEFAULT_PAGE
|
|
||||||
if form_type == 'offboarding':
|
|
||||||
return OFFBOARDING_DEFAULT_PAGE
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_custom_section_configs(form_type: str, include_inactive: bool = False) -> list[FormCustomSectionConfig]:
|
|
||||||
qs = FormCustomSectionConfig.objects.filter(form_type=form_type)
|
|
||||||
if not include_inactive:
|
|
||||||
qs = qs.filter(is_active=True)
|
|
||||||
return list(qs.order_by('sort_order', 'section_key'))
|
|
||||||
|
|
||||||
|
|
||||||
def get_section_definitions(form_type: str, include_inactive_custom: bool = False) -> list[dict[str, object]]:
|
|
||||||
definitions: list[dict[str, object]] = []
|
|
||||||
section_configs = ensure_form_section_configs(form_type)
|
|
||||||
for cfg in sorted(section_configs.values(), key=lambda item: (item.sort_order, item.section_key)):
|
|
||||||
label_map = CORE_SECTION_LABELS.get(form_type, {})
|
|
||||||
definitions.append(
|
|
||||||
{
|
|
||||||
'key': cfg.section_key,
|
|
||||||
'title': label_map.get(cfg.section_key, cfg.section_key),
|
|
||||||
'locked': cfg.section_key in LOCKED_SECTION_RULES.get(form_type, set()),
|
|
||||||
'is_custom': False,
|
|
||||||
'sort_order': cfg.sort_order,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for cfg in get_custom_section_configs(form_type, include_inactive=include_inactive_custom):
|
|
||||||
definitions.append(
|
|
||||||
{
|
|
||||||
'key': cfg.section_key,
|
|
||||||
'title': cfg.translated_title(get_language()),
|
|
||||||
'locked': False,
|
|
||||||
'is_custom': True,
|
|
||||||
'is_active': cfg.is_active,
|
|
||||||
'sort_order': cfg.sort_order,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
definitions.sort(key=lambda item: (item.get('sort_order', 9999), item['key']))
|
|
||||||
return definitions
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_conditional_rules(form_type: str) -> dict[str, dict]:
|
|
||||||
return DEFAULT_CONDITIONAL_RULES.get(form_type, {})
|
|
||||||
|
|
||||||
|
|
||||||
def custom_field_target_key(field_key: str) -> str:
|
|
||||||
return f'custom__{field_key}'
|
|
||||||
|
|
||||||
|
|
||||||
def is_custom_field_target_key(target_key: str) -> bool:
|
|
||||||
return target_key.startswith(CUSTOM_FIELD_PREFIX)
|
|
||||||
|
|
||||||
|
|
||||||
def custom_field_form_name(field_key: str) -> str:
|
|
||||||
return f'{CUSTOM_FIELD_PREFIX}{field_key}'
|
|
||||||
|
|
||||||
|
|
||||||
def is_custom_field_name(field_name: str) -> bool:
|
|
||||||
return field_name.startswith(CUSTOM_FIELD_PREFIX)
|
|
||||||
|
|
||||||
|
|
||||||
def custom_field_key_from_name(field_name: str) -> str:
|
|
||||||
return field_name[len(CUSTOM_FIELD_PREFIX):] if is_custom_field_name(field_name) else field_name
|
|
||||||
|
|
||||||
|
|
||||||
def build_custom_field_key(label: str) -> str:
|
|
||||||
return slugify(label).replace('-', '_')[:60] or 'custom_field'
|
|
||||||
|
|
||||||
|
|
||||||
def get_custom_field_configs(form_type: str, include_inactive: bool = False):
|
|
||||||
qs = FormCustomFieldConfig.objects.filter(form_type=form_type)
|
|
||||||
if not include_inactive:
|
|
||||||
qs = qs.filter(is_active=True)
|
|
||||||
return list(qs.order_by('sort_order', 'field_key'))
|
|
||||||
|
|
||||||
|
|
||||||
def add_custom_form_fields(form_type: str, form, initial_values: dict | None = None) -> None:
|
|
||||||
language_code = get_language()
|
|
||||||
initial_values = initial_values or {}
|
|
||||||
field_page_keys = getattr(form, '_field_page_keys', {})
|
|
||||||
sort_map = {}
|
|
||||||
for cfg in get_custom_field_configs(form_type):
|
|
||||||
field_name = custom_field_form_name(cfg.field_key)
|
|
||||||
initial = initial_values.get(cfg.field_key)
|
|
||||||
if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_TEXTAREA:
|
|
||||||
field = forms.CharField(
|
|
||||||
label=cfg.translated_label(language_code),
|
|
||||||
help_text=cfg.translated_help_text(language_code),
|
|
||||||
required=cfg.is_required,
|
|
||||||
initial=initial,
|
|
||||||
widget=forms.Textarea(attrs={'rows': 3}),
|
|
||||||
)
|
|
||||||
elif cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT:
|
|
||||||
field = forms.ChoiceField(
|
|
||||||
label=cfg.translated_label(language_code),
|
|
||||||
help_text=cfg.translated_help_text(language_code),
|
|
||||||
required=cfg.is_required,
|
|
||||||
initial=initial or '',
|
|
||||||
choices=[('', '--')] + cfg.translated_select_options(language_code),
|
|
||||||
)
|
|
||||||
elif cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_CHECKBOX:
|
|
||||||
field = forms.BooleanField(
|
|
||||||
label=cfg.translated_label(language_code),
|
|
||||||
help_text=cfg.translated_help_text(language_code),
|
|
||||||
required=False,
|
|
||||||
initial=bool(initial),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
field = forms.CharField(
|
|
||||||
label=cfg.translated_label(language_code),
|
|
||||||
help_text=cfg.translated_help_text(language_code),
|
|
||||||
required=cfg.is_required,
|
|
||||||
initial=initial,
|
|
||||||
)
|
|
||||||
form.fields[field_name] = field
|
|
||||||
field_page_keys[field_name] = cfg.section_key
|
|
||||||
sort_map[field_name] = cfg.sort_order
|
|
||||||
|
|
||||||
if form.fields:
|
|
||||||
core_configs = ensure_form_field_configs(form_type, [name for name in form.fields.keys() if not is_custom_field_name(name)])
|
|
||||||
for name in form.fields.keys():
|
|
||||||
if is_custom_field_name(name):
|
|
||||||
continue
|
|
||||||
cfg = core_configs.get(name)
|
|
||||||
if cfg:
|
|
||||||
sort_map[name] = cfg.sort_order
|
|
||||||
form.fields = OrderedDict(
|
|
||||||
(name, form.fields[name])
|
|
||||||
for name in sorted(form.fields.keys(), key=lambda name: (sort_map.get(name, 9999), name))
|
|
||||||
)
|
|
||||||
form._field_page_keys = field_page_keys
|
|
||||||
|
|
||||||
|
|
||||||
def _default_sort(form_type: str, field_name: str) -> int:
|
|
||||||
ordered = DEFAULT_FIELD_ORDER.get(form_type, [])
|
|
||||||
if field_name in ordered:
|
|
||||||
return ordered.index(field_name)
|
|
||||||
return len(ordered) + 500
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_configs(form_type: str, field_names: list[str]) -> dict[str, FormFieldConfig]:
|
|
||||||
existing = {
|
|
||||||
cfg.field_name: cfg
|
|
||||||
for cfg in FormFieldConfig.objects.filter(form_type=form_type, field_name__in=field_names)
|
|
||||||
}
|
|
||||||
missing_names = [name for name in field_names if name not in existing]
|
|
||||||
if missing_names:
|
|
||||||
FormFieldConfig.objects.bulk_create(
|
|
||||||
[
|
|
||||||
FormFieldConfig(
|
|
||||||
form_type=form_type,
|
|
||||||
field_name=name,
|
|
||||||
sort_order=_default_sort(form_type, name),
|
|
||||||
page_key=get_default_page_map(form_type).get(name, ''),
|
|
||||||
)
|
|
||||||
for name in missing_names
|
|
||||||
],
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
existing = {
|
|
||||||
cfg.field_name: cfg
|
|
||||||
for cfg in FormFieldConfig.objects.filter(form_type=form_type, field_name__in=field_names)
|
|
||||||
}
|
|
||||||
return existing
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_form_field_configs(form_type: str, field_names: list[str]) -> dict[str, FormFieldConfig]:
|
|
||||||
return _ensure_configs(form_type, field_names)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]:
|
|
||||||
section_order = list(CORE_SECTION_LABELS.get(form_type, {}).keys())
|
|
||||||
if not section_order:
|
|
||||||
return {}
|
|
||||||
existing = {
|
|
||||||
cfg.section_key: cfg
|
|
||||||
for cfg in FormSectionConfig.objects.filter(form_type=form_type)
|
|
||||||
}
|
|
||||||
missing = [key for key in section_order if key not in existing]
|
|
||||||
if missing:
|
|
||||||
FormSectionConfig.objects.bulk_create(
|
|
||||||
[
|
|
||||||
FormSectionConfig(
|
|
||||||
form_type=form_type,
|
|
||||||
section_key=key,
|
|
||||||
sort_order=section_order.index(key),
|
|
||||||
is_visible=True,
|
|
||||||
)
|
|
||||||
for key in missing
|
|
||||||
],
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
existing = {
|
|
||||||
cfg.section_key: cfg
|
|
||||||
for cfg in FormSectionConfig.objects.filter(form_type=form_type)
|
|
||||||
}
|
|
||||||
return existing
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_form_conditional_rule_configs(form_type: str) -> dict[str, FormConditionalRuleConfig]:
|
|
||||||
defaults = get_default_conditional_rules(form_type)
|
|
||||||
if form_type != 'onboarding' and not defaults:
|
|
||||||
return {}
|
|
||||||
custom_targets = {
|
|
||||||
custom_field_target_key(cfg.field_key): {'clauses': []}
|
|
||||||
for cfg in get_custom_field_configs(form_type)
|
|
||||||
if form_type == 'onboarding'
|
|
||||||
}
|
|
||||||
target_defaults = dict(defaults)
|
|
||||||
target_defaults.update(custom_targets)
|
|
||||||
existing = {
|
|
||||||
cfg.target_key: cfg
|
|
||||||
for cfg in FormConditionalRuleConfig.objects.filter(form_type=form_type)
|
|
||||||
}
|
|
||||||
missing = [key for key in target_defaults.keys() if key not in existing]
|
|
||||||
if missing:
|
|
||||||
FormConditionalRuleConfig.objects.bulk_create(
|
|
||||||
[
|
|
||||||
FormConditionalRuleConfig(
|
|
||||||
form_type=form_type,
|
|
||||||
target_key=key,
|
|
||||||
clauses=target_defaults[key].get('clauses', []),
|
|
||||||
is_active=bool(target_defaults[key].get('clauses')),
|
|
||||||
)
|
|
||||||
for key in missing
|
|
||||||
],
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
existing = {
|
|
||||||
cfg.target_key: cfg
|
|
||||||
for cfg in FormConditionalRuleConfig.objects.filter(form_type=form_type)
|
|
||||||
}
|
|
||||||
return existing
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_conditional_clauses(cleaned_data: dict, clauses: list[dict]) -> bool:
|
|
||||||
def clause_result(clause: dict) -> bool:
|
|
||||||
field_name = (clause.get('field') or '').strip()
|
|
||||||
operator = (clause.get('operator') or '').strip()
|
|
||||||
if not field_name or not operator:
|
|
||||||
return False
|
|
||||||
value = cleaned_data.get(field_name)
|
|
||||||
if operator == 'checked':
|
|
||||||
return bool(value) is bool(clause.get('value'))
|
|
||||||
normalized = '' if value is None else str(value).strip()
|
|
||||||
expected = '' if clause.get('value') is None else str(clause.get('value')).strip()
|
|
||||||
if operator == 'equals':
|
|
||||||
return normalized == expected
|
|
||||||
if operator == 'not_equals':
|
|
||||||
return normalized != expected
|
|
||||||
return False
|
|
||||||
|
|
||||||
active_clauses = [clause for clause in (clauses or []) if clause.get('field') and clause.get('operator')]
|
|
||||||
return bool(active_clauses) and all(clause_result(clause) for clause in active_clauses)
|
|
||||||
|
|
||||||
|
|
||||||
def hidden_custom_field_names(form_type: str, cleaned_data: dict) -> set[str]:
|
|
||||||
if form_type != 'onboarding':
|
|
||||||
return set()
|
|
||||||
hidden = set()
|
|
||||||
for target_key, cfg in ensure_form_conditional_rule_configs(form_type).items():
|
|
||||||
if not cfg.is_active or not is_custom_field_target_key(target_key):
|
|
||||||
continue
|
|
||||||
if not evaluate_conditional_clauses(cleaned_data, list(cfg.clauses or [])):
|
|
||||||
hidden.add(target_key)
|
|
||||||
return hidden
|
|
||||||
|
|
||||||
|
|
||||||
def apply_form_field_config(form_type: str, form) -> None:
|
|
||||||
field_names = list(form.fields.keys())
|
|
||||||
configs = _ensure_configs(form_type, field_names)
|
|
||||||
section_configs = ensure_form_section_configs(form_type)
|
|
||||||
locked = LOCKED_FIELD_RULES.get(form_type, set())
|
|
||||||
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
|
||||||
default_page_map = get_default_page_map(form_type)
|
|
||||||
language_code = get_language()
|
|
||||||
|
|
||||||
for field_name, field in list(form.fields.items()):
|
|
||||||
cfg = configs.get(field_name)
|
|
||||||
if not cfg:
|
|
||||||
continue
|
|
||||||
|
|
||||||
translated_label = cfg.translated_label_override(language_code)
|
|
||||||
if translated_label:
|
|
||||||
field.label = translated_label
|
|
||||||
|
|
||||||
translated_help_text = cfg.translated_help_text_override(language_code)
|
|
||||||
if translated_help_text:
|
|
||||||
field.help_text = translated_help_text
|
|
||||||
|
|
||||||
if field_name not in locked and cfg.is_required is not None:
|
|
||||||
field.required = cfg.is_required
|
|
||||||
|
|
||||||
section_key = cfg.page_key or default_page_map.get(field_name, '')
|
|
||||||
section_hidden = (
|
|
||||||
form_type in {'onboarding', 'offboarding'}
|
|
||||||
and section_key not in locked_sections
|
|
||||||
and section_key in section_configs
|
|
||||||
and not section_configs[section_key].is_visible
|
|
||||||
)
|
|
||||||
if field_name not in locked and (not cfg.is_visible or section_hidden):
|
|
||||||
form.fields.pop(field_name, None)
|
|
||||||
|
|
||||||
ordered_items = sorted(
|
|
||||||
form.fields.items(),
|
|
||||||
key=lambda item: (
|
|
||||||
configs[item[0]].sort_order if item[0] in configs else _default_sort(form_type, item[0]),
|
|
||||||
item[0],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
form.fields = OrderedDict(ordered_items)
|
|
||||||
if form_type in {'onboarding', 'offboarding'}:
|
|
||||||
form._field_page_keys = {
|
|
||||||
name: (configs[name].page_key or default_page_map.get(name, ''))
|
|
||||||
for name in form.fields.keys()
|
|
||||||
if name in configs
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
FORM_PRESETS = {
|
|
||||||
'onboarding': {
|
|
||||||
'standard': {
|
|
||||||
'label': _('Standard'),
|
|
||||||
'sections': {
|
|
||||||
'stammdaten': True,
|
|
||||||
'vertrag': True,
|
|
||||||
'itsetup': True,
|
|
||||||
'abschluss': True,
|
|
||||||
},
|
|
||||||
'fields': {},
|
|
||||||
},
|
|
||||||
'lean': {
|
|
||||||
'label': _('Lean'),
|
|
||||||
'sections': {
|
|
||||||
'stammdaten': True,
|
|
||||||
'vertrag': True,
|
|
||||||
'itsetup': False,
|
|
||||||
'abschluss': True,
|
|
||||||
},
|
|
||||||
'fields': {
|
|
||||||
'gender': {'is_visible': False},
|
|
||||||
'order_business_cards': {'is_visible': False},
|
|
||||||
'business_card_name': {'is_visible': False},
|
|
||||||
'business_card_title': {'is_visible': False},
|
|
||||||
'business_card_email': {'is_visible': False},
|
|
||||||
'business_card_phone': {'is_visible': False},
|
|
||||||
'employment_end_date': {'is_visible': False},
|
|
||||||
'group_mailboxes_required_choice': {'is_visible': False},
|
|
||||||
'group_mailboxes': {'is_visible': False},
|
|
||||||
'additional_notes': {'is_required': False},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'it_heavy': {
|
|
||||||
'label': _('IT-heavy'),
|
|
||||||
'sections': {
|
|
||||||
'stammdaten': True,
|
|
||||||
'vertrag': True,
|
|
||||||
'itsetup': True,
|
|
||||||
'abschluss': True,
|
|
||||||
},
|
|
||||||
'fields': {
|
|
||||||
'needed_devices_multi': {'is_required': True},
|
|
||||||
'needed_software_multi': {'is_required': True},
|
|
||||||
'needed_accesses_multi': {'is_required': True},
|
|
||||||
'needed_workspace_groups_multi': {'is_required': True},
|
|
||||||
'needed_resources_multi': {'is_required': True},
|
|
||||||
'additional_hardware_needed_choice': {'is_visible': True},
|
|
||||||
'additional_software_needed_choice': {'is_visible': True},
|
|
||||||
'additional_access_needed_choice': {'is_visible': True},
|
|
||||||
'successor_required_choice': {'is_visible': True},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'offboarding': {
|
|
||||||
'standard': {
|
|
||||||
'label': _('Standard'),
|
|
||||||
'sections': {
|
|
||||||
'mitarbeitende': True,
|
|
||||||
'austritt': True,
|
|
||||||
'abschluss': True,
|
|
||||||
},
|
|
||||||
'fields': {},
|
|
||||||
},
|
|
||||||
'lean': {
|
|
||||||
'label': _('Lean'),
|
|
||||||
'sections': {
|
|
||||||
'mitarbeitende': True,
|
|
||||||
'austritt': True,
|
|
||||||
'abschluss': False,
|
|
||||||
},
|
|
||||||
'fields': {
|
|
||||||
'department': {'is_visible': False},
|
|
||||||
'job_title': {'is_visible': False},
|
|
||||||
'notes': {'is_visible': False},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'hr_heavy': {
|
|
||||||
'label': _('HR-heavy'),
|
|
||||||
'sections': {
|
|
||||||
'mitarbeitende': True,
|
|
||||||
'austritt': True,
|
|
||||||
'abschluss': True,
|
|
||||||
},
|
|
||||||
'fields': {
|
|
||||||
'department': {'is_visible': True, 'is_required': True},
|
|
||||||
'job_title': {'is_visible': True, 'is_required': True},
|
|
||||||
'notes': {'is_visible': True, 'is_required': True},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def apply_form_preset(form_type: str, preset_key: str) -> bool:
|
|
||||||
preset = FORM_PRESETS.get(form_type, {}).get(preset_key)
|
|
||||||
if not preset:
|
|
||||||
return False
|
|
||||||
|
|
||||||
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
|
|
||||||
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
|
||||||
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
|
|
||||||
ensure_form_field_configs(form_type, default_names)
|
|
||||||
section_configs = ensure_form_section_configs(form_type)
|
|
||||||
|
|
||||||
for section_key, is_visible in preset.get('sections', {}).items():
|
|
||||||
cfg = section_configs.get(section_key)
|
|
||||||
if not cfg or section_key in locked_sections:
|
|
||||||
continue
|
|
||||||
cfg.is_visible = bool(is_visible)
|
|
||||||
cfg.save(update_fields=['is_visible'])
|
|
||||||
|
|
||||||
for cfg in FormFieldConfig.objects.filter(form_type=form_type):
|
|
||||||
if cfg.field_name in locked_fields:
|
|
||||||
cfg.is_visible = True
|
|
||||||
cfg.is_required = None
|
|
||||||
else:
|
|
||||||
cfg.is_visible = True
|
|
||||||
cfg.is_required = None
|
|
||||||
override = preset.get('fields', {}).get(cfg.field_name, {})
|
|
||||||
if 'is_visible' in override:
|
|
||||||
cfg.is_visible = bool(override['is_visible'])
|
|
||||||
if 'is_required' in override:
|
|
||||||
cfg.is_required = override['is_required']
|
|
||||||
cfg.save(update_fields=['is_visible', 'is_required'])
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
117
backend/workflows/form_builder_config.py
Normal file
117
backend/workflows/form_builder_config.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
DEFAULT_FIELD_ORDER = {
|
||||||
|
'onboarding': [
|
||||||
|
'first_name', 'last_name', 'full_name', 'gender', 'job_title', 'department', 'work_email',
|
||||||
|
'order_business_cards', 'business_card_name', 'business_card_title', 'business_card_email', 'business_card_phone',
|
||||||
|
'contract_start', 'employment_type', 'employment_end_date', 'handover_date',
|
||||||
|
'group_mailboxes_required_choice', 'group_mailboxes', 'needed_devices_multi',
|
||||||
|
'additional_hardware_needed_choice', 'additional_hardware_multi', 'additional_hardware_other',
|
||||||
|
'needed_software_multi', 'additional_software_needed_choice', 'additional_software_multi', 'additional_software',
|
||||||
|
'needed_accesses_multi', 'additional_access_needed_choice', 'additional_access_text',
|
||||||
|
'needed_workspace_groups_multi', 'needed_resources_multi', 'successor_required_choice', 'successor_name',
|
||||||
|
'inherit_phone_number_choice', 'phone_number_choice', 'additional_notes', 'signature_url', 'signature_image',
|
||||||
|
'onboarded_by_email', 'agreement_confirm',
|
||||||
|
],
|
||||||
|
'offboarding': ['full_name', 'work_email', 'department', 'job_title', 'last_working_day', 'notes'],
|
||||||
|
}
|
||||||
|
|
||||||
|
ONBOARDING_PAGE_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
|
||||||
|
OFFBOARDING_PAGE_ORDER = ['mitarbeitende', 'austritt', 'abschluss']
|
||||||
|
ONBOARDING_PAGE_LABELS = {
|
||||||
|
'stammdaten': _('1. Stammdaten'),
|
||||||
|
'vertrag': _('2. Vertrag'),
|
||||||
|
'itsetup': _('3. IT-Setup'),
|
||||||
|
'abschluss': _('4. Abschluss'),
|
||||||
|
}
|
||||||
|
OFFBOARDING_PAGE_LABELS = {
|
||||||
|
'mitarbeitende': _('1. Mitarbeitende'),
|
||||||
|
'austritt': _('2. Austritt'),
|
||||||
|
'abschluss': _('3. Abschluss'),
|
||||||
|
}
|
||||||
|
CORE_SECTION_LABELS = {'onboarding': ONBOARDING_PAGE_LABELS, 'offboarding': OFFBOARDING_PAGE_LABELS}
|
||||||
|
|
||||||
|
LOCKED_FIELD_RULES = {
|
||||||
|
'onboarding': {'full_name', 'work_email', 'contract_start', 'agreement_confirm'},
|
||||||
|
'offboarding': {'full_name', 'work_email', 'last_working_day'},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCKED_SECTION_RULES = {
|
||||||
|
'onboarding': {'stammdaten', 'vertrag', 'abschluss'},
|
||||||
|
'offboarding': {'mitarbeitende', 'austritt'},
|
||||||
|
}
|
||||||
|
|
||||||
|
ONBOARDING_DEFAULT_PAGE = {
|
||||||
|
'first_name': 'stammdaten', 'last_name': 'stammdaten', 'full_name': 'stammdaten', 'gender': 'stammdaten',
|
||||||
|
'job_title': 'stammdaten', 'department': 'stammdaten', 'work_email': 'stammdaten', 'order_business_cards': 'stammdaten',
|
||||||
|
'business_card_name': 'stammdaten', 'business_card_title': 'stammdaten', 'business_card_email': 'stammdaten', 'business_card_phone': 'stammdaten',
|
||||||
|
'contract_start': 'vertrag', 'employment_type': 'vertrag', 'employment_end_date': 'vertrag', 'handover_date': 'vertrag',
|
||||||
|
'group_mailboxes_required_choice': 'vertrag', 'group_mailboxes': 'vertrag', 'needed_devices_multi': 'itsetup',
|
||||||
|
'additional_hardware_needed_choice': 'itsetup', 'additional_hardware_multi': 'itsetup', 'additional_hardware_other': 'itsetup',
|
||||||
|
'needed_software_multi': 'itsetup', 'additional_software_needed_choice': 'itsetup', 'additional_software_multi': 'itsetup', 'additional_software': 'itsetup',
|
||||||
|
'needed_accesses_multi': 'itsetup', 'additional_access_needed_choice': 'itsetup', 'additional_access_text': 'itsetup',
|
||||||
|
'needed_workspace_groups_multi': 'itsetup', 'needed_resources_multi': 'itsetup', 'successor_required_choice': 'itsetup',
|
||||||
|
'successor_name': 'itsetup', 'inherit_phone_number_choice': 'itsetup', 'phone_number_choice': 'itsetup',
|
||||||
|
'additional_notes': 'abschluss', 'signature_url': 'abschluss', 'signature_image': 'abschluss', 'onboarded_by_email': 'abschluss', 'agreement_confirm': 'abschluss',
|
||||||
|
}
|
||||||
|
OFFBOARDING_DEFAULT_PAGE = {
|
||||||
|
'full_name': 'mitarbeitende', 'work_email': 'mitarbeitende', 'department': 'mitarbeitende', 'job_title': 'mitarbeitende',
|
||||||
|
'last_working_day': 'austritt', 'notes': 'abschluss',
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_CONDITIONAL_RULES = {
|
||||||
|
'onboarding': {
|
||||||
|
'business-card-box': {'clauses': [{'field': 'order_business_cards', 'operator': 'checked', 'value': True}]},
|
||||||
|
'employment-end-box': {'clauses': [{'field': 'employment_type', 'operator': 'equals', 'value': 'befristet'}]},
|
||||||
|
'group-mailboxes-box': {'clauses': [{'field': 'group_mailboxes_required_choice', 'operator': 'equals', 'value': 'ja'}]},
|
||||||
|
'extra-hardware-box': {'clauses': [{'field': 'additional_hardware_needed_choice', 'operator': 'equals', 'value': 'ja'}]},
|
||||||
|
'extra-software-box': {'clauses': [{'field': 'additional_software_needed_choice', 'operator': 'equals', 'value': 'ja'}]},
|
||||||
|
'extra-access-box': {'clauses': [{'field': 'additional_access_needed_choice', 'operator': 'equals', 'value': 'ja'}]},
|
||||||
|
'successor-box': {'clauses': [{'field': 'successor_required_choice', 'operator': 'equals', 'value': 'ja'}]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
FORM_PRESETS = {
|
||||||
|
'onboarding': {
|
||||||
|
'standard': {'label': _('Standard'), 'sections': {'stammdaten': True, 'vertrag': True, 'itsetup': True, 'abschluss': True}, 'fields': {}},
|
||||||
|
'lean': {
|
||||||
|
'label': _('Lean'),
|
||||||
|
'sections': {'stammdaten': True, 'vertrag': True, 'itsetup': False, 'abschluss': True},
|
||||||
|
'fields': {
|
||||||
|
'gender': {'is_visible': False}, 'order_business_cards': {'is_visible': False}, 'business_card_name': {'is_visible': False},
|
||||||
|
'business_card_title': {'is_visible': False}, 'business_card_email': {'is_visible': False}, 'business_card_phone': {'is_visible': False},
|
||||||
|
'employment_end_date': {'is_visible': False}, 'group_mailboxes_required_choice': {'is_visible': False}, 'group_mailboxes': {'is_visible': False},
|
||||||
|
'additional_notes': {'is_required': False},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'it_heavy': {
|
||||||
|
'label': _('IT-heavy'),
|
||||||
|
'sections': {'stammdaten': True, 'vertrag': True, 'itsetup': True, 'abschluss': True},
|
||||||
|
'fields': {
|
||||||
|
'needed_devices_multi': {'is_required': True}, 'needed_software_multi': {'is_required': True}, 'needed_accesses_multi': {'is_required': True},
|
||||||
|
'needed_workspace_groups_multi': {'is_required': True}, 'needed_resources_multi': {'is_required': True},
|
||||||
|
'additional_hardware_needed_choice': {'is_visible': True}, 'additional_software_needed_choice': {'is_visible': True},
|
||||||
|
'additional_access_needed_choice': {'is_visible': True}, 'successor_required_choice': {'is_visible': True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'offboarding': {
|
||||||
|
'standard': {'label': _('Standard'), 'sections': {'mitarbeitende': True, 'austritt': True, 'abschluss': True}, 'fields': {}},
|
||||||
|
'lean': {
|
||||||
|
'label': _('Lean'),
|
||||||
|
'sections': {'mitarbeitende': True, 'austritt': True, 'abschluss': False},
|
||||||
|
'fields': {'department': {'is_visible': False}, 'job_title': {'is_visible': False}, 'notes': {'is_visible': False}},
|
||||||
|
},
|
||||||
|
'hr_heavy': {
|
||||||
|
'label': _('HR-heavy'),
|
||||||
|
'sections': {'mitarbeitende': True, 'austritt': True, 'abschluss': True},
|
||||||
|
'fields': {
|
||||||
|
'department': {'is_visible': True, 'is_required': True},
|
||||||
|
'job_title': {'is_visible': True, 'is_required': True},
|
||||||
|
'notes': {'is_visible': True, 'is_required': True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CUSTOM_FIELD_PREFIX = 'custom__'
|
||||||
382
backend/workflows/form_builder_runtime.py
Normal file
382
backend/workflows/form_builder_runtime.py
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
from django import forms
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
|
from .model_forms import FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormSectionConfig
|
||||||
|
from .form_builder_config import (
|
||||||
|
CORE_SECTION_LABELS,
|
||||||
|
CUSTOM_FIELD_PREFIX,
|
||||||
|
DEFAULT_CONDITIONAL_RULES,
|
||||||
|
DEFAULT_FIELD_ORDER,
|
||||||
|
FORM_PRESETS,
|
||||||
|
LOCKED_FIELD_RULES,
|
||||||
|
LOCKED_SECTION_RULES,
|
||||||
|
OFFBOARDING_DEFAULT_PAGE,
|
||||||
|
ONBOARDING_DEFAULT_PAGE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_section_order(form_type: str) -> list[str]:
|
||||||
|
return [item['key'] for item in get_section_definitions(form_type)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_section_labels(form_type: str) -> dict[str, str]:
|
||||||
|
return {item['key']: item['title'] for item in get_section_definitions(form_type)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_page_map(form_type: str) -> dict[str, str]:
|
||||||
|
if form_type == 'onboarding':
|
||||||
|
return ONBOARDING_DEFAULT_PAGE
|
||||||
|
if form_type == 'offboarding':
|
||||||
|
return OFFBOARDING_DEFAULT_PAGE
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_custom_section_configs(form_type: str, include_inactive: bool = False) -> list[FormCustomSectionConfig]:
|
||||||
|
qs = FormCustomSectionConfig.objects.filter(form_type=form_type)
|
||||||
|
if not include_inactive:
|
||||||
|
qs = qs.filter(is_active=True)
|
||||||
|
return list(qs.order_by('sort_order', 'section_key'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_section_definitions(form_type: str, include_inactive_custom: bool = False) -> list[dict[str, object]]:
|
||||||
|
definitions: list[dict[str, object]] = []
|
||||||
|
section_configs = ensure_form_section_configs(form_type)
|
||||||
|
for cfg in sorted(section_configs.values(), key=lambda item: (item.sort_order, item.section_key)):
|
||||||
|
label_map = CORE_SECTION_LABELS.get(form_type, {})
|
||||||
|
definitions.append(
|
||||||
|
{
|
||||||
|
'key': cfg.section_key,
|
||||||
|
'title': label_map.get(cfg.section_key, cfg.section_key),
|
||||||
|
'locked': cfg.section_key in LOCKED_SECTION_RULES.get(form_type, set()),
|
||||||
|
'is_custom': False,
|
||||||
|
'sort_order': cfg.sort_order,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for cfg in get_custom_section_configs(form_type, include_inactive=include_inactive_custom):
|
||||||
|
definitions.append(
|
||||||
|
{
|
||||||
|
'key': cfg.section_key,
|
||||||
|
'title': cfg.translated_title(get_language()),
|
||||||
|
'locked': False,
|
||||||
|
'is_custom': True,
|
||||||
|
'is_active': cfg.is_active,
|
||||||
|
'sort_order': cfg.sort_order,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
definitions.sort(key=lambda item: (item.get('sort_order', 9999), item['key']))
|
||||||
|
return definitions
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_conditional_rules(form_type: str) -> dict[str, dict]:
|
||||||
|
return DEFAULT_CONDITIONAL_RULES.get(form_type, {})
|
||||||
|
|
||||||
|
|
||||||
|
def custom_field_target_key(field_key: str) -> str:
|
||||||
|
return f'custom__{field_key}'
|
||||||
|
|
||||||
|
|
||||||
|
def is_custom_field_target_key(target_key: str) -> bool:
|
||||||
|
return target_key.startswith(CUSTOM_FIELD_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def custom_field_form_name(field_key: str) -> str:
|
||||||
|
return f'{CUSTOM_FIELD_PREFIX}{field_key}'
|
||||||
|
|
||||||
|
|
||||||
|
def is_custom_field_name(field_name: str) -> bool:
|
||||||
|
return field_name.startswith(CUSTOM_FIELD_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def custom_field_key_from_name(field_name: str) -> str:
|
||||||
|
return field_name[len(CUSTOM_FIELD_PREFIX):] if is_custom_field_name(field_name) else field_name
|
||||||
|
|
||||||
|
|
||||||
|
def build_custom_field_key(label: str) -> str:
|
||||||
|
return slugify(label).replace('-', '_')[:60] or 'custom_field'
|
||||||
|
|
||||||
|
|
||||||
|
def get_custom_field_configs(form_type: str, include_inactive: bool = False):
|
||||||
|
qs = FormCustomFieldConfig.objects.filter(form_type=form_type)
|
||||||
|
if not include_inactive:
|
||||||
|
qs = qs.filter(is_active=True)
|
||||||
|
return list(qs.order_by('sort_order', 'field_key'))
|
||||||
|
|
||||||
|
|
||||||
|
def add_custom_form_fields(form_type: str, form, initial_values: dict | None = None) -> None:
|
||||||
|
language_code = get_language()
|
||||||
|
initial_values = initial_values or {}
|
||||||
|
field_page_keys = getattr(form, '_field_page_keys', {})
|
||||||
|
sort_map = {}
|
||||||
|
for cfg in get_custom_field_configs(form_type):
|
||||||
|
field_name = custom_field_form_name(cfg.field_key)
|
||||||
|
initial = initial_values.get(cfg.field_key)
|
||||||
|
if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_TEXTAREA:
|
||||||
|
field = forms.CharField(
|
||||||
|
label=cfg.translated_label(language_code),
|
||||||
|
help_text=cfg.translated_help_text(language_code),
|
||||||
|
required=cfg.is_required,
|
||||||
|
initial=initial,
|
||||||
|
widget=forms.Textarea(attrs={'rows': 3}),
|
||||||
|
)
|
||||||
|
elif cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT:
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label=cfg.translated_label(language_code),
|
||||||
|
help_text=cfg.translated_help_text(language_code),
|
||||||
|
required=cfg.is_required,
|
||||||
|
initial=initial or '',
|
||||||
|
choices=[('', '--')] + cfg.translated_select_options(language_code),
|
||||||
|
)
|
||||||
|
elif cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_CHECKBOX:
|
||||||
|
field = forms.BooleanField(
|
||||||
|
label=cfg.translated_label(language_code),
|
||||||
|
help_text=cfg.translated_help_text(language_code),
|
||||||
|
required=False,
|
||||||
|
initial=bool(initial),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
field = forms.CharField(
|
||||||
|
label=cfg.translated_label(language_code),
|
||||||
|
help_text=cfg.translated_help_text(language_code),
|
||||||
|
required=cfg.is_required,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
form.fields[field_name] = field
|
||||||
|
field_page_keys[field_name] = cfg.section_key
|
||||||
|
sort_map[field_name] = cfg.sort_order
|
||||||
|
|
||||||
|
if form.fields:
|
||||||
|
core_configs = ensure_form_field_configs(form_type, [name for name in form.fields.keys() if not is_custom_field_name(name)])
|
||||||
|
for name in form.fields.keys():
|
||||||
|
if is_custom_field_name(name):
|
||||||
|
continue
|
||||||
|
cfg = core_configs.get(name)
|
||||||
|
if cfg:
|
||||||
|
sort_map[name] = cfg.sort_order
|
||||||
|
form.fields = OrderedDict(
|
||||||
|
(name, form.fields[name])
|
||||||
|
for name in sorted(form.fields.keys(), key=lambda name: (sort_map.get(name, 9999), name))
|
||||||
|
)
|
||||||
|
form._field_page_keys = field_page_keys
|
||||||
|
|
||||||
|
|
||||||
|
def _default_sort(form_type: str, field_name: str) -> int:
|
||||||
|
ordered = DEFAULT_FIELD_ORDER.get(form_type, [])
|
||||||
|
if field_name in ordered:
|
||||||
|
return ordered.index(field_name)
|
||||||
|
return len(ordered) + 500
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_configs(form_type: str, field_names: list[str]) -> dict[str, FormFieldConfig]:
|
||||||
|
existing = {
|
||||||
|
cfg.field_name: cfg
|
||||||
|
for cfg in FormFieldConfig.objects.filter(form_type=form_type, field_name__in=field_names)
|
||||||
|
}
|
||||||
|
missing_names = [name for name in field_names if name not in existing]
|
||||||
|
if missing_names:
|
||||||
|
FormFieldConfig.objects.bulk_create(
|
||||||
|
[
|
||||||
|
FormFieldConfig(
|
||||||
|
form_type=form_type,
|
||||||
|
field_name=name,
|
||||||
|
sort_order=_default_sort(form_type, name),
|
||||||
|
page_key=get_default_page_map(form_type).get(name, ''),
|
||||||
|
)
|
||||||
|
for name in missing_names
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
existing = {
|
||||||
|
cfg.field_name: cfg
|
||||||
|
for cfg in FormFieldConfig.objects.filter(form_type=form_type, field_name__in=field_names)
|
||||||
|
}
|
||||||
|
return existing
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_form_field_configs(form_type: str, field_names: list[str]) -> dict[str, FormFieldConfig]:
|
||||||
|
return _ensure_configs(form_type, field_names)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]:
|
||||||
|
section_order = list(CORE_SECTION_LABELS.get(form_type, {}).keys())
|
||||||
|
if not section_order:
|
||||||
|
return {}
|
||||||
|
existing = {
|
||||||
|
cfg.section_key: cfg
|
||||||
|
for cfg in FormSectionConfig.objects.filter(form_type=form_type)
|
||||||
|
}
|
||||||
|
missing = [key for key in section_order if key not in existing]
|
||||||
|
if missing:
|
||||||
|
FormSectionConfig.objects.bulk_create(
|
||||||
|
[
|
||||||
|
FormSectionConfig(
|
||||||
|
form_type=form_type,
|
||||||
|
section_key=key,
|
||||||
|
sort_order=section_order.index(key),
|
||||||
|
is_visible=True,
|
||||||
|
)
|
||||||
|
for key in missing
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
existing = {
|
||||||
|
cfg.section_key: cfg
|
||||||
|
for cfg in FormSectionConfig.objects.filter(form_type=form_type)
|
||||||
|
}
|
||||||
|
return existing
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_form_conditional_rule_configs(form_type: str) -> dict[str, FormConditionalRuleConfig]:
|
||||||
|
defaults = get_default_conditional_rules(form_type)
|
||||||
|
if form_type != 'onboarding' and not defaults:
|
||||||
|
return {}
|
||||||
|
custom_targets = {
|
||||||
|
custom_field_target_key(cfg.field_key): {'clauses': []}
|
||||||
|
for cfg in get_custom_field_configs(form_type)
|
||||||
|
if form_type == 'onboarding'
|
||||||
|
}
|
||||||
|
target_defaults = dict(defaults)
|
||||||
|
target_defaults.update(custom_targets)
|
||||||
|
existing = {
|
||||||
|
cfg.target_key: cfg
|
||||||
|
for cfg in FormConditionalRuleConfig.objects.filter(form_type=form_type)
|
||||||
|
}
|
||||||
|
missing = [key for key in target_defaults.keys() if key not in existing]
|
||||||
|
if missing:
|
||||||
|
FormConditionalRuleConfig.objects.bulk_create(
|
||||||
|
[
|
||||||
|
FormConditionalRuleConfig(
|
||||||
|
form_type=form_type,
|
||||||
|
target_key=key,
|
||||||
|
clauses=target_defaults[key].get('clauses', []),
|
||||||
|
is_active=bool(target_defaults[key].get('clauses')),
|
||||||
|
)
|
||||||
|
for key in missing
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
existing = {
|
||||||
|
cfg.target_key: cfg
|
||||||
|
for cfg in FormConditionalRuleConfig.objects.filter(form_type=form_type)
|
||||||
|
}
|
||||||
|
return existing
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_conditional_clauses(cleaned_data: dict, clauses: list[dict]) -> bool:
|
||||||
|
def clause_result(clause: dict) -> bool:
|
||||||
|
field_name = (clause.get('field') or '').strip()
|
||||||
|
operator = (clause.get('operator') or '').strip()
|
||||||
|
if not field_name or not operator:
|
||||||
|
return False
|
||||||
|
value = cleaned_data.get(field_name)
|
||||||
|
if operator == 'checked':
|
||||||
|
return bool(value) is bool(clause.get('value'))
|
||||||
|
normalized = '' if value is None else str(value).strip()
|
||||||
|
expected = '' if clause.get('value') is None else str(clause.get('value')).strip()
|
||||||
|
if operator == 'equals':
|
||||||
|
return normalized == expected
|
||||||
|
if operator == 'not_equals':
|
||||||
|
return normalized != expected
|
||||||
|
return False
|
||||||
|
|
||||||
|
active_clauses = [clause for clause in (clauses or []) if clause.get('field') and clause.get('operator')]
|
||||||
|
return bool(active_clauses) and all(clause_result(clause) for clause in active_clauses)
|
||||||
|
|
||||||
|
|
||||||
|
def hidden_custom_field_names(form_type: str, cleaned_data: dict) -> set[str]:
|
||||||
|
if form_type != 'onboarding':
|
||||||
|
return set()
|
||||||
|
hidden = set()
|
||||||
|
for target_key, cfg in ensure_form_conditional_rule_configs(form_type).items():
|
||||||
|
if not cfg.is_active or not is_custom_field_target_key(target_key):
|
||||||
|
continue
|
||||||
|
if not evaluate_conditional_clauses(cleaned_data, list(cfg.clauses or [])):
|
||||||
|
hidden.add(target_key)
|
||||||
|
return hidden
|
||||||
|
|
||||||
|
|
||||||
|
def apply_form_field_config(form_type: str, form) -> None:
|
||||||
|
field_names = list(form.fields.keys())
|
||||||
|
configs = _ensure_configs(form_type, field_names)
|
||||||
|
section_configs = ensure_form_section_configs(form_type)
|
||||||
|
locked = LOCKED_FIELD_RULES.get(form_type, set())
|
||||||
|
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
||||||
|
default_page_map = get_default_page_map(form_type)
|
||||||
|
language_code = get_language()
|
||||||
|
|
||||||
|
for field_name, field in list(form.fields.items()):
|
||||||
|
cfg = configs.get(field_name)
|
||||||
|
if not cfg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
translated_label = cfg.translated_label_override(language_code)
|
||||||
|
if translated_label:
|
||||||
|
field.label = translated_label
|
||||||
|
|
||||||
|
translated_help_text = cfg.translated_help_text_override(language_code)
|
||||||
|
if translated_help_text:
|
||||||
|
field.help_text = translated_help_text
|
||||||
|
|
||||||
|
if field_name not in locked and cfg.is_required is not None:
|
||||||
|
field.required = cfg.is_required
|
||||||
|
|
||||||
|
section_key = cfg.page_key or default_page_map.get(field_name, '')
|
||||||
|
section_hidden = (
|
||||||
|
form_type in {'onboarding', 'offboarding'}
|
||||||
|
and section_key not in locked_sections
|
||||||
|
and section_key in section_configs
|
||||||
|
and not section_configs[section_key].is_visible
|
||||||
|
)
|
||||||
|
if field_name not in locked and (not cfg.is_visible or section_hidden):
|
||||||
|
form.fields.pop(field_name, None)
|
||||||
|
|
||||||
|
ordered_items = sorted(
|
||||||
|
form.fields.items(),
|
||||||
|
key=lambda item: (
|
||||||
|
configs[item[0]].sort_order if item[0] in configs else _default_sort(form_type, item[0]),
|
||||||
|
item[0],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
form.fields = OrderedDict(ordered_items)
|
||||||
|
if form_type in {'onboarding', 'offboarding'}:
|
||||||
|
form._field_page_keys = {
|
||||||
|
name: (configs[name].page_key or default_page_map.get(name, ''))
|
||||||
|
for name in form.fields.keys()
|
||||||
|
if name in configs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_form_preset(form_type: str, preset_key: str) -> bool:
|
||||||
|
preset = FORM_PRESETS.get(form_type, {}).get(preset_key)
|
||||||
|
if not preset:
|
||||||
|
return False
|
||||||
|
|
||||||
|
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
|
||||||
|
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
||||||
|
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
|
||||||
|
ensure_form_field_configs(form_type, default_names)
|
||||||
|
section_configs = ensure_form_section_configs(form_type)
|
||||||
|
|
||||||
|
for section_key, is_visible in preset.get('sections', {}).items():
|
||||||
|
cfg = section_configs.get(section_key)
|
||||||
|
if not cfg or section_key in locked_sections:
|
||||||
|
continue
|
||||||
|
cfg.is_visible = bool(is_visible)
|
||||||
|
cfg.save(update_fields=['is_visible'])
|
||||||
|
|
||||||
|
for cfg in FormFieldConfig.objects.filter(form_type=form_type):
|
||||||
|
if cfg.field_name in locked_fields:
|
||||||
|
cfg.is_visible = True
|
||||||
|
cfg.is_required = None
|
||||||
|
else:
|
||||||
|
cfg.is_visible = True
|
||||||
|
cfg.is_required = None
|
||||||
|
override = preset.get('fields', {}).get(cfg.field_name, {})
|
||||||
|
if 'is_visible' in override:
|
||||||
|
cfg.is_visible = bool(override['is_visible'])
|
||||||
|
if 'is_required' in override:
|
||||||
|
cfg.is_required = override['is_required']
|
||||||
|
cfg.save(update_fields=['is_visible', 'is_required'])
|
||||||
|
|
||||||
|
return True
|
||||||
Reference in New Issue
Block a user