Field & Inline UI Configuration
Developer guide for the Enezaframework. Part of the Developer documentation.
Table of Contents
- Overview
- The
detail_ui_viewMethod - Tab Schema
- Disabling and Hiding Fields
- Header Position for Documents
- ManyToMany and Autocomplete Fields
- Inline Related Objects
- Resolving One-to-One Inline Objects
- Change History
- 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):
return [
{
"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 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 or list | No | Conditionally disable all fields in the tab (single condition or a list of conditions). |
hide_if |
dict or list | No | Conditionally hide specific fields from the form (uses hide_fields). |
headerpostion |
str | No | For document views: "header" or "lines". |
default_open |
bool | No | Whether the tab starts expanded; defaults to True. |
Columns and fields
"Fieldcolumns": [
{"fields": ["name", "code"]},
{"fields": ["currency_code"]},
]
Each column renders its fields top-to-bottom. A column can set "width": "full" to span the row;
otherwise it renders as a half width column. Simple string field names are the common case; see the
ManyToMany section for the dictionary form.
Disabling and Hiding 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 referenced field equals the given value, every field in that tab becomes read-only. value can
be a list to match any of several values, and a source key (default "self") can point the check at a
related object. A list of conditions is also accepted and disables the tab if any condition matches.
Conditionally hidden
"hide_if": {
"field": "step_type",
"value": "APPROVAL",
"hide_fields": ["condition_expression"],
},
hide_if removes the named fields from the generated form (not just read-only) when the condition
matches. It accepts a single dict or a list of dicts.
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"],
"full_width": True,
"rich_text": False,
},
]
},
]
| 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. |
full_width |
bool | False |
Render the field across the full row width. |
rich_text |
bool | False |
Render as a rich-text field. |
widget |
str | None |
Explicit widget override. |
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.
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):
return [
{
"name": "State",
"caption": "States",
"model": "state",
"relation_name": "state_country",
"position": 1,
"active": 1,
"icon": "DocumentPlus",
"relation_type": "one_to_many",
},
]
| 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 for the inline view. |
tab_group |
Optional tab-group key used with get_tab_groups(). |
is_party_inline |
Set by the framework for auto-injected party inlines. |
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))
The inline renderer reads
tab.get("postion", 1)in one configuration path (enezaframework/views/mixins.py), which is a framework typo. Usepositionas shown; if your inline ordering is ignored, check the key the installed framework reads.
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 inline configuration calls
get_related_object_id(self, related_model)(mixins.py), whileBaseInlineOneToOneDetailView.get_related_object_id(self)is parameterless (baseinline.py). Match the signature of the base class on the path you are 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.
Change History
Dated changes that affect this guide, taken from
docs/Release note/CHANGELOG.md and the release notes. For the
complete history, see the changelog.
| Date | Version | What changed |
|---|---|---|
| 2026-06-27 | — | disable_if and hide_if extended to parent/grandparent model fields via source resolution. |
| 2026-06-30 | 0.2.8 | Added field-level full_width, column width: "full", display: "label" (with dotted paths), rich_text, and widget: "button_group". |
| 2026-07-02 | 0.2.11 | disable_if now accepts multiple conditions and multi-value conditions (OR logic). |
| 2026-07-02 | 0.2.12 | default_open added; inline column width and field full_width fixed in inline templates. |
| 2026-07-05 | 0.2.13 | Inline tab_group added; get_tab_groups() introduced; ungrouped inlines render at the page bottom. |
| 2026-08-23 | 0.2.19 | Boolean fields render as a Yes/No select; TomSelect auto-initializes after HTMX swaps. |