Field & Inline UI Configuration

Developer guide for the Enezaframework. Part of the Developer documentation.

Table of Contents

  1. Overview
  2. The detail_ui_view Method
  3. Tab Schema
  4. Disabling and Hiding Fields
  5. Header Position for Documents
  6. ManyToMany and Autocomplete Fields
  7. Inline Related Objects
  8. Resolving One-to-One Inline Objects
  9. Change History
  10. 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 a Fieldtabs = [...] class attribute are not what the base views call — implement detail_ui_view and return the list from it.


The detail_ui_view Method

python
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

python
"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

python
"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

python
"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

python
"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:

python
{
    "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, then name, then the first CharField, then id.
python
"Fieldcolumns": [
    {"fields": ["name", "assignee_users"]},
]

Extended form (dict)

Use a dictionary for explicit control:

python
"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.


Override get_inline_related_objects(self) on a detail view to display related records as inline tabs. Each entry describes one inline section.

python
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:

python
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. Use position as 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), while BaseInlineOneToOneDetailView.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's pk (or None).

python
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.