Source code for imednet.form_designer.builder
import random
import string
from typing import Any, List, Literal, Optional, cast
from .models import (
CheckboxFieldProps,
Choice,
Col,
DateTimeFieldProps,
DropdownFieldProps,
Entity,
EntityProps,
FileUploadProps,
LabelProps,
Layout,
MemoFieldProps,
NumberFieldProps,
Page,
RadioFieldProps,
Row,
SeparatorProps,
TableProps,
TextFieldProps,
)
[docs]class FormBuilder:
"""
Builder class to construct iMedNet Form Designer payloads programmatically.
Manages ID generation and hierarchical structure.
"""
def __init__(self) -> None:
self.pages: List[Page] = []
self._ensure_page()
# Track generated IDs to avoid collisions (though random large int makes it rare)
self._generated_ids: set[int] = set()
def _ensure_page(self) -> None:
if not self.pages:
self.pages.append(Page(entities=[]))
@property
def current_page(self) -> Page:
self._ensure_page()
return self.pages[-1]
def _generate_new_fld_id(self) -> int:
"""Generate a random unique integer for new fields."""
while True:
new_id = random.randint(10000, 99999)
if new_id not in self._generated_ids:
self._generated_ids.add(new_id)
return new_id
def _generate_dom_id(self) -> str:
"""Generate a random DOM ID (e.g., lfdiv_38492)."""
suffix = "".join(random.choices(string.digits, k=5))
return f"lfdiv_{suffix}"
def _create_entity(self, props: EntityProps, rows: Optional[List[Row]] = None) -> Entity:
return Entity(props=props, id=self._generate_dom_id(), rows=rows)
[docs] def add_page(self) -> None:
"""Add a new page to the form."""
self.pages.append(Page(entities=[]))
[docs] def add_section_header(self, label: str) -> None:
"""Add a separator/section header."""
props = SeparatorProps(type="sep", label=label, septype=1)
entity = self._create_entity(props)
self.current_page.entities.append(entity)
def _get_or_create_table(self) -> Entity:
"""Get the last entity if it's a table, or create a new one."""
if self.current_page.entities:
last = self.current_page.entities[-1]
if last.props.type == "table":
return last
# Create new table
table_props = TableProps(type="table", columns=2) # Standard 2-col
table = self._create_entity(table_props, rows=[])
self.current_page.entities.append(table)
return table
[docs] def add_group_header(self, label: str) -> None:
"""Add a group header (Label-only row)."""
table = self._get_or_create_table()
if table.rows is None:
table.rows = []
# Group header is Row -> Col 1 (Label) -> Col 2 (Empty)
# Col 1
label_props = LabelProps(type="label", label=label)
# Usually group headers don't have IDs linked to controls
col1 = Col(entities=[self._create_entity(label_props)])
# Col 2
col2 = Col(entities=[])
row = Row(cols=[col1, col2])
table.rows.append(row)
[docs] def add_field(
self,
type: Literal[
"text", "number", "radio", "dropdown", "datetime", "upload", "checkbox", "memo"
],
label: str,
question_name: str,
required: bool = False,
# Type specific args
choices: Optional[List[tuple[str, str]]] = None, # (text, code)
max_length: Optional[int] = None,
is_float: bool = False,
) -> None:
"""
Add a standard field (Label + Control).
Args:
type: Field type.
label: Display label (HTML allowed).
question_name: Variable OID.
required: If True, sets bl_req='hard'.
choices: List of (text, code) for radios/dropdowns.
max_length: Max chars (text/memo) or digits (number).
is_float: For numbers, allow decimals.
"""
table = self._get_or_create_table()
if table.rows is None:
table.rows = []
# Generate shared ID
shared_id = self._generate_new_fld_id()
bl_req = "hard" if required else "optional"
# 1. Label Entity (Col 1)
label_props = LabelProps(type="label", label=label, new_fld_id=shared_id)
col1 = Col(entities=[self._create_entity(label_props)])
# 2. Control Entity (Col 2)
ctrl_props: EntityProps
common_kwargs = {"question_name": question_name, "new_fld_id": shared_id, "bl_req": bl_req}
if type == "text":
ctrl_props = TextFieldProps(
type="text",
length=max_length or 100,
columns=30,
**cast(Any, common_kwargs),
)
elif type == "memo":
ctrl_props = MemoFieldProps(
type="memo",
length=max_length or 500,
columns=40,
rows=6,
**cast(Any, common_kwargs),
)
elif type == "number":
ctrl_props = NumberFieldProps(
type="number",
length=max_length or 5,
columns=10,
real=1 if is_float else 0,
**cast(Any, common_kwargs),
)
elif type == "radio":
field_choices = []
if choices:
field_choices = [
Choice(text=t, code=c, choice_id=self._generate_new_fld_id())
for t, c in choices
]
ctrl_props = RadioFieldProps(
type="radio",
choices=field_choices,
radio=1,
**cast(Any, common_kwargs), # Horizontal default
)
elif type == "dropdown":
field_choices = []
if choices:
field_choices = [
Choice(text=t, code=c, choice_id=self._generate_new_fld_id())
for t, c in choices
]
ctrl_props = DropdownFieldProps(
type="dropdown", choices=field_choices, **cast(Any, common_kwargs)
)
elif type == "checkbox":
field_choices = []
if choices:
field_choices = [
Choice(text=t, code=c, choice_id=self._generate_new_fld_id())
for t, c in choices
]
ctrl_props = CheckboxFieldProps(
type="checkbox", choices=field_choices, **cast(Any, common_kwargs)
)
elif type == "datetime":
ctrl_props = DateTimeFieldProps(
type="datetime",
date_ctrl=1,
time_ctrl=0,
allow_no_day=0,
allow_no_month=0,
allow_no_year=0,
**cast(Any, common_kwargs),
)
elif type == "upload":
ctrl_props = FileUploadProps(
type="upload",
mfs=1,
max_files=10, # default to something reasonable if missing
**cast(Any, common_kwargs),
)
else:
# Fallback (should not happen due to type hint)
raise ValueError(f"Unsupported field type: {type}")
col2 = Col(entities=[self._create_entity(ctrl_props)])
row = Row(cols=[col1, col2])
table.rows.append(row)