snapshot: preserve custom field parity across forms timeline and pdf
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
from collections import OrderedDict
|
||||
from django import forms
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import get_language
|
||||
|
||||
from .models import FormConditionalRuleConfig, FormFieldConfig, FormSectionConfig
|
||||
from .models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormSectionConfig
|
||||
|
||||
|
||||
DEFAULT_FIELD_ORDER = {
|
||||
@@ -164,6 +166,8 @@ DEFAULT_CONDITIONAL_RULES = {
|
||||
},
|
||||
}
|
||||
|
||||
CUSTOM_FIELD_PREFIX = 'custom__'
|
||||
|
||||
|
||||
def get_section_order(form_type: str) -> list[str]:
|
||||
if form_type == 'onboarding':
|
||||
@@ -193,6 +197,94 @@ 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:
|
||||
@@ -253,21 +345,28 @@ def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]:
|
||||
|
||||
def ensure_form_conditional_rule_configs(form_type: str) -> dict[str, FormConditionalRuleConfig]:
|
||||
defaults = get_default_conditional_rules(form_type)
|
||||
if not defaults:
|
||||
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 defaults.keys() if key not in existing]
|
||||
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=defaults[key].get('clauses', []),
|
||||
is_active=True,
|
||||
clauses=target_defaults[key].get('clauses', []),
|
||||
is_active=bool(target_defaults[key].get('clauses')),
|
||||
)
|
||||
for key in missing
|
||||
],
|
||||
@@ -280,6 +379,39 @@ def ensure_form_conditional_rule_configs(form_type: str) -> dict[str, FormCondit
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user