Field & Inline UI Configuration

Developer guide for the Enezaframework. Part of the architecture series.

Table of Contents

  1. Overview
  2. The detail_ui_view Method
  3. Tab Schema
  4. Disabling Fields
  5. Header Position for Documents
  6. ManyToMany and Autocomplete Fields
  7. Inline Related Objects
  8. Tab Groups (get_tab_groups)
  9. Resolving One-to-One Inline Objects
  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):
    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

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

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

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"],
            },
        ]
    },
]
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.

python
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

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

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

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

python
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 headerpostion matches a group key render in that group.
  • Inlines whose tab_group matches a group key render inside that group's pane.
  • Field tabs that match no group fall into the first group.
  • Inlines without a tab_group are 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 use get_related_object_id(self, related_model). Match the signature of the base class on your branch before 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.