Field & Inline UI Configuration
Developer guide for the Enezaframework. Part of the architecture series.
Table of Contents
- Overview
- The
detail_ui_viewMethod - Tab Schema
- Disabling Fields
- Header Position for Documents
- ManyToMany and Autocomplete Fields
- Inline Related Objects
- Tab Groups (
get_tab_groups) - Resolving One-to-One Inline Objects
- Related Guides
Overview
Detail and create views describe their form layout by implementing a detail_ui_view(self) method. The framework calls this method and turns its tab/column description into the rendered form. The same schema drives tabbed inlines and document headers/lines.
flowchart TD
VIEW[Detail or Create view] --> METHOD[detail_ui_view]
METHOD --> TABS[Fieldtabs list]
TABS --> TAB[Tab: name, icon, flags]
TAB --> COLS[Fieldcolumns]
COLS --> FIELDS[Simple field names]
COLS --> EXT[Extended field dicts<br/>autocomplete config]
VIEW --> INLINES[get_inline_related_objects]
INLINES --> RESOLVE[get_related_object_id<br/>for one-to-one]The framework reads the method
self.detail_ui_view()(enezaframework/views/mixins.py). Older snippets that assign aFieldtabs = [...]class attribute are not what the base views call — implementdetail_ui_viewand return the list from it.
The detail_ui_view Method
def detail_ui_view(self):
Fieldtabs = [
{
"name": "Basic Information",
"icon": "Home",
"disable_fields": ["code", "name"],
"disable_if": {
"field": "postedstatus",
"value": "POSTED",
},
"headerpostion": "header",
"Fieldcolumns": [
{"fields": ["name", "code"]},
{"fields": ["currency_code"]},
],
},
]
return Fieldtabs
Return a list of tab dictionaries. Use it in both detail and create views; the only difference is usually which fields are disabled.
Tab Schema
| Key | Type | Required | Description |
|---|---|---|---|
name |
str | Yes | Display name of the tab. |
icon |
str | Yes | Icon name resolved from the static icons/ folder. |
Fieldcolumns |
list | Yes | Ordered list of columns; each column has a fields list. |
disable_fields |
list | No | Field names that are always read-only in this tab. |
disable_if |
dict | No | Conditionally disable all fields in the tab. |
headerpostion |
str | No | For document views: "header" or "lines". |
Columns and fields
"Fieldcolumns": [
{"fields": ["name", "code"]},
{"fields": ["currency_code"]},
]
Each column renders its fields top-to-bottom. Simple string field names are the common case; see the ManyToMany section for the dictionary form.
Disabling Fields
Always read-only
"disable_fields": ["code", "name"]
Use this for keys that are auto-generated or must not change after creation. During create, disabling a server-generated field keeps it out of user input.
Conditionally read-only
"disable_if": {
"field": "postedstatus",
"value": "POSTED",
},
When the tab's referenced model field equals the given value, every field in that tab becomes read-only. This is commonly used to lock a document after posting.
Header Position for Documents
Document-oriented views (see Inline & Specialized Views) can place a tab's content in the header region instead of alongside the lines:
{
"name": "General",
"icon": "Home",
"headerpostion": "header",
"Fieldcolumns": [
{"fields": ["document_no", "customer"]},
],
}
"header"renders the tab in the document header area."lines"renders it in the lines area.
ManyToMany and Autocomplete Fields
For ManyToMany fields, the framework can render a TomSelect autocomplete widget.
Simple form (string)
When a field is a plain string, the framework:
- Detects that the field is ManyToMany.
- Generates the autocomplete URL name as
{app_label}:{model_name}_autocomplete. - Auto-detects the label field, preferring
first_name, thenname, then the firstCharField, thenid.
"Fieldcolumns": [
{"fields": ["name", "assignee_users"]},
]
Extended form (dict)
Use a dictionary for explicit control:
"Fieldcolumns": [
{
"fields": [
"name",
{
"name": "assignee_users",
"autocomplete_url": "myapp:custom_user_autocomplete",
"value_field": "id",
"label_field": "text",
"search_fields": ["first_name", "last_name"],
},
]
},
]
| Option | Type | Default | Description |
|---|---|---|---|
name |
string | (required) | The model field name. |
autocomplete_url |
string | Auto-generated | Django URL name for the autocomplete endpoint. |
value_field |
string | "id" |
Field used as the option value. |
label_field |
string | Auto-detected | Field displayed as the option label. |
search_fields |
list | [] |
Fields searched when filtering autocomplete results. |
The autocomplete URL follows {app_label}:{model_name}_autocomplete. For the User model in enezacommon, that is enezacommon:user_autocomplete — register your autocomplete view under that name so the default lookup resolves.
def detail_ui_view(self):
Fieldtabs = [
{
"name": "Step Information",
"icon": "Home",
"Fieldcolumns": [
{"fields": ["order", "name", "step_type"]},
{
"fields": [
"condition_expression",
{
"name": "assignee_users",
"label_field": "text",
},
]
},
],
},
]
return Fieldtabs
Inline Related Objects
Override get_inline_related_objects(self) on a detail view to display related records as inline tabs. Each entry describes one inline section.
def get_inline_related_objects(self):
inline_related_object = [
{
"name": "State",
"caption": "States",
"model": "state",
"relation_name": "state_country",
"position": 1,
"active": 1,
"icon": "DocumentPlus",
"relation_type": "one_to_many",
},
]
return inline_related_object
| Key | Description |
|---|---|
name |
Internal key for the inline section. |
caption |
Display caption for the tab/section. |
model |
Lowercase model name of the related object. |
relation_name |
The related_name on the FK/O2O from the inline model back to the parent. |
position |
Order of the inline tab (lower first). |
active |
1 to show, 0 to hide. |
icon |
Icon for the inline tab. |
relation_type |
"one_to_many" or "one_to_one". |
condition |
Optional value that must match on the parent for the inline to appear. |
url_name |
Optional URL-prefix override; must match the inline view's url_name. |
tab_group |
Optional tab-group key used with get_tab_groups(); inlines without it render ungrouped at the page bottom. |
Return entries sorted by position when you mix conditional and static inlines:
all_inlines = conditional_inlines + static_inlines
return sorted(all_inlines, key=lambda item: item.get("position", 99))
Tab Groups (get_tab_groups)
A parent detail view can group its field tabs and inlines into named top-level tabs by overriding
get_tab_groups(). The method returns [] by default, which renders the page in accordion-only mode
with no tab bar.
def get_tab_groups(self):
return [
{"key": "ChildTab", "name": "Child Records"},
]
Field tabs are assigned to a group by their headerpostion value, and inline objects by their
tab_group value:
def get_inline_related_objects(self):
return [
{
"name": "ChildTab",
"caption": "Child Records",
"model": "childmodel",
"url_name": "childmodel_inline",
"relation_name": "children",
"position": 1,
"active": 1,
"icon": "ChartBar",
"relation_type": "one_to_many",
"tab_group": "ChildTab",
},
]
Behavior:
- Field tabs whose
headerpostionmatches a groupkeyrender in that group. - Inlines whose
tab_groupmatches a groupkeyrender inside that group's pane. - Field tabs that match no group fall into the first group.
- Inlines without a
tab_groupare excluded from the groups and render ungrouped at the page bottom (ungrouped_inlines).
Resolving One-to-One Inline Objects
Conditional or generic one-to-one inlines need the parent view to resolve which related object to load. Override get_related_object_id.
Signature note: the base inline implementation uses
get_related_object_id(self), while some views useget_related_object_id(self, related_model). Match the signature of the base class on your branch before overriding, and return the related object'spk(orNone).
from django.contrib.contenttypes.models import ContentType
from enezacommon.models import Business, Person
from enezacommon.models.choices import VendorType
from accountpayable.models import VendorTableDetails
def get_related_object_id(self, related_model_name_str):
related_object = None
try:
if related_model_name_str == "VendorTableDetails":
related_object = VendorTableDetails.objects.get(vendortable=self.object)
elif (
self.object.vendortype == VendorType.ORGANIZATION
and related_model_name_str == "Business"
):
content_type = ContentType.objects.get_for_model(self.model)
related_object = Business.objects.get(
content_type=content_type,
object_id=self.object.id,
)
elif (
self.object.vendortype == VendorType.PERSON
and related_model_name_str == "Person"
):
content_type = ContentType.objects.get_for_model(self.model)
related_object = Person.objects.get(
content_type=content_type,
object_id=self.object.id,
)
except (
Business.DoesNotExist,
Person.DoesNotExist,
VendorTableDetails.DoesNotExist,
):
return None
return related_object.id if related_object else None
Always guard for the create-view case where self.object may not exist yet, and always return None rather than raising when the related object is absent.