Ribbon & Action Menus

Developer guide for the Enezaframework. Part of the Developer documentation. For a step-by-step recipe, see Adding Ribbon Tabs, Groups & Menus.

The record ribbon is driven by a ribbon profile. A profile is defined by the list view, passed to the detail page as a short profile key, and resolved on the server into tabs, groups, buttons, dropdowns, related-record workspaces and inline panels.

This guide covers:

  1. What Changed
  2. Quick Start
  3. Tabs, Groups and Menu Items
  4. Menu Kinds
  5. Related Record Menus
  6. Business Actions
  7. List-Context Menus
  8. Permissions and Entity Overrides
  9. Database Overlay
  10. Backward Compatibility
  11. Troubleshooting

What Changed

Every generic record page now supports:

Tab Purpose
Home Primary record actions contributed by a profile.
Options Default tab for developer-defined actions and menus.
View Display/refresh controls.
Personalization Reserved key for future user preferences; hidden until it has items.

Additional tabs (for example Manage or Reports) are added by the profile. The existing detail action bar remains in place, including the workflow dropdown. Report screens in enezareporting are not changed.

The important rule is:

Define the profile once on the list view. The list sends ?ribbon_profile=<key> to detail URLs; code and optional database overlays are resolved by the framework.


Quick Start

python
from enezaframework.ribbon import (
    RibbonMenuItemSpec,
    RibbonProfileSpec,
    RibbonTabSpec,
)
from enezaframework.views import BaseDetailView, BaseListView
from enezaframework.utils import auto_url_register

VENDOR_PROFILE = RibbonProfileSpec(
    key="accountpayable.vendor",
    name="Vendor",
    detail_tabs=[
        RibbonTabSpec(key="manage", label="Manage", order=20),
    ],
    menus=[
        RibbonMenuItemSpec(
            code="bank-details",
            label="Bank Details",
            icon="building",
            tab="manage",
            group="Banking",
            kind="related",
            display_mode="inline",
            target_kind="list",
            target_model="vendorbankaccount",
            parent_lookup="vendor",
            relation_name="bank_accounts",
        ),
    ],
)


@auto_url_register
class VendorListView(BaseListView):
    model = VendorTable

    def get_ribbon_profile(self):
        return VENDOR_PROFILE

    def set_list_fields(self):
        return ["code", "name"]

    def set_search_fields(self):
        return ["code", "name"]


@auto_url_register
class VendorDetailView(BaseDetailView):
    model = VendorTable
    default_ribbon_profile = "accountpayable.vendor"

When a row opens the detail page, the framework appends the profile key automatically. Direct detail links use default_ribbon_profile.

The profile may also be declared as a class attribute:

python
class VendorListView(BaseListView):
    ribbon_profile = VENDOR_PROFILE

get_ribbon_profile() is the recommended hook when the profile depends on the request.


Tabs, Groups and Menu Items

RibbonProfileSpec

Field Meaning
key Stable key used in ?ribbon_profile=.
name Diagnostic name.
owner_app_label, owner_model Optional; derived from the list view model.
tabs Shared tab definitions. Use context="both" for tabs available in list and detail.
list_tabs, detail_tabs Optional context-specific tab definitions. Omit to use the base tabs.
menus Menu/action definitions.
default_tab Tab selected when no ribbon_tab is supplied.
replace_tabs When True, profile tabs replace the base tabs instead of merging.

RibbonTabSpec

key, label, context (list, detail or both) and order. icon is optional.

RibbonMenuItemSpec

Field Default Meaning
code required Stable identifier for overrides and list row data attributes.
label required Button/menu text.
icon empty Icon name resolved by the framework.
tab options Tab key.
context detail list, detail or both.
group, group_order, order General, 0, 0 Ribbon grouping/sorting.
kind action action, link, related, dropdown, separator.
url_name empty Django URL name.
http_method POST for actions GET or POST.
target #detail-container HTMX target.
swap innerHTML HTMX swap style.
confirm_message empty Confirmation text.
hx_vals {} Extra HTMX values.
display_mode workspace inline, workspace, full_page.
target_kind custom list, setup, detail, custom.
target_app_label, target_model empty Related model.
target_url_name empty Explicit target URL.
parent_lookup empty FK/O2O field on the target model.
relation_name, relation_type empty, one_to_many Inline metadata.
requires_object True Disable in create mode or with no selected row.
show_in_edit, show_in_create True Visibility flags.
permissions, groups empty Visibility rules.
children empty One dropdown level.
is_active True Enable/disable.

Menu code may contain letters, numbers, underscores and hyphens.


Action

Runs a registered BaseAction URL. The framework appends return_url, the profile key and the active tab so the user returns to the same context.

python
RibbonMenuItemSpec(
    code="put-on-hold",
    label="Put on Hold",
    tab="options",
    kind="action",
    url_name="vendor_put_on_hold",  # resolves vendor_put_on_hold_action
    http_method="POST",
    confirm_message="Put this vendor on hold?",
)
python
RibbonMenuItemSpec(
    code="open-website",
    label="Website",
    kind="link",
    url_name="accountpayable:vendor_website",  # or a full URL name
    display_mode="full_page",
)
python
RibbonMenuItemSpec(
    code="more",
    label="More",
    kind="dropdown",
    children=(
        RibbonMenuItemSpec(code="audit", label="Audit", kind="link", url_name="..."),
        RibbonMenuItemSpec(code="archive", label="Archive", kind="action", url_name="..."),
    ),
)

Only one dropdown level is supported.

Separator

python
RibbonMenuItemSpec(code="sep-1", label="", kind="separator")

A related menu targets another record page.

python
RibbonMenuItemSpec(
    code="bank-details",
    label="Bank Details",
    tab="manage",
    group="Banking",
    kind="related",
    target_kind="list",
    target_app_label="accountpayable",
    target_model="vendorbankaccount",
    parent_lookup="vendor",
    display_mode="inline",
)

display_mode

Mode Behavior
inline Opens the existing inline list/detail workspace for the related model. Best for child records such as bank accounts.
workspace Opens the related list, detail, create and edit inside the parent content area. The parent ribbon stays visible.
full_page Navigates the whole browser to the target, for example a setup/master-detail page.

target_kind

Kind Behavior
list Resolve {app}:{model}_list.
detail Resolve the related object and open {app}:{model}_detail.
setup Requires target_url_name; opens the setup page as a full page with return_url.
custom Requires target_url_name; the developer owns the target template.

For list/workspace menus, the framework adds the parent record identity to the target URL and filters the target list when parent_lookup is configured. For inline menus, the existing inline views are reused and the parent context is passed through main_object_pk.

Inline mode and full templates

A vendor bank-account menu can use display_mode="inline" to reuse the inline views. The inline list, detail, create and delete behavior stays the same as a normal inline section.

If the related records need the full list workspace (filters, paging, local New/Edit/Delete), use display_mode="workspace". The related record list opens inside #detail-container; its detail, edit and create views stay in the same workspace, and Back returns to the parent record with the Manage tab active.


Business Actions

BaseAction remains the execution contract. A menu only points at the action URL; the action view keeps its own business logic and permission checks.

python
from enezaframework.views import BaseAction

@auto_url_register
class VendorPutOnHoldAction(BaseAction):
    model = VendorTable

    def process_action(self, pk):
        vendor = VendorTable.objects.get(pk=pk)
        vendor.status = "on_hold"
        vendor.save()

The framework passes return_url, ribbon_profile and ribbon_tab to the action. If no safe return URL is supplied, BaseAction redirects to the record detail as before.


List-Context Menus

Set context="list" to add a menu to the list ribbon. Such menus act on exactly one selected row. The framework attaches the selected row’s resolved action URL to the row and enables the ribbon item only when one row is selected.

python
RibbonMenuItemSpec(
    code="put-on-hold",
    label="Put on Hold",
    context="list",
    tab="options",
    kind="action",
    url_name="vendor_put_on_hold",
)

Menus with requires_object=False can be static links and do not depend on selection.


Permissions and Entity Overrides

Menus may declare Django permissions and/or group names:

python
RibbonMenuItemSpec(
    code="bank-details",
    label="Bank Details",
    permissions=("accountpayable.view_vendorbankaccount",),
    groups=("Credit Control",),
)

All listed permissions must be held; if groups are supplied, the user must belong to at least one. Hidden items are not rendered. The action view must still enforce access because a user can call the URL directly.

Developer code profiles apply to every entity. The optional database overlay can add an entity-specific override (see below).


Database Overlay

The database overlay is optional. It is useful when a deployment needs to add or override menus without a code release. The models live in enezaframework/models/ribbon.py:

Model Purpose
RibbonProfile Optional profile record; entity=NULL is global.
RibbonTab Tabs for a profile.
RibbonMenu Actions, links, related menus and dropdown children.

Resolution order:

  1. Base get_ribbon_tabs().
  2. Code profile.
  3. Global DB overlay.
  4. Entity-specific DB overlay.

An item with the same code in a later layer replaces the earlier item. If no profile resolves, the base ribbon is rendered unchanged.

There is intentionally no in-app setup screen for these models in this iteration. Create records through the ORM, shell or Django admin if the models are registered in the project.


Backward Compatibility

The following continue to work unchanged:

  • get_ribbon_tabs() overrides.
  • action_list and print_actions in the detail action bar.
  • main_actions and the workflow dropdown in navactions.html.
  • WorkflowMixin.
  • Setup, journal, parameter, attachment and reporting screens.

If a view does not define a profile, no profile query parameter is added and the base tabs are used. A missing or invalid profile key falls back to the detail view’s default profile, then the base tabs.


Troubleshooting

Symptom Likely cause
Menu does not render is_active=False, wrong context, failed permission/group check, or invalid target configuration.
Menu is disabled in create mode requires_object=True and the record is not saved yet. This is expected.
List menu stays disabled Zero or multiple rows are selected, or the row has no resolved URL for that menu code.
URL error in DEBUG The url_name/target_url_name could not be reversed. Use the full app:name form.
Inline menu is empty The target inline views are not registered under {model}_list, {model}_detail, {model}_create and {model}_delete.
Workspace returns a nested shell The workspace request is missing partial=1. The framework adds it for internal workspace navigation; custom links should include it when targeting #ez-workspace-body.
Refresh loses a custom profile Set default_ribbon_profile on the detail view or register the profile at import time.