Adding Ribbon Tabs, Groups & Menus

Step-by-step developer guide. For the complete reference, see Ribbon & Action Menus.

A ribbon is built from three layers:

text
RibbonProfileSpec
└── Tab                 (RibbonTabSpec)
    └── Group           (created by menu items sharing the same group title)
        └── Menu item   (RibbonMenuItemSpec)

There is no separate group class. A group is created automatically from the group and group_order values on menu items.


1. Define the profile

Define one profile per list/detail family. The profile key is the handoff between the list and detail pages.

python
from enezaframework.ribbon import (
    RibbonMenuItemSpec,
    RibbonProfileSpec,
    RibbonTabSpec,
)

DOCUMENT_RIBBON = RibbonProfileSpec(
    key="accountpayable.vendor",
    name="Vendor",
    default_tab="options",
    tabs=(),
    menus=(),
)

Attach it to the list view so list rows carry the profile key:

python
class VendorListView(BaseListView):
    model = Vendor
    ribbon_profile = DOCUMENT_RIBBON

Attach it to the detail view as well. This also makes direct/bookmarked detail URLs resolve the profile without relying on the list page:

python
class VendorDetailView(BaseDetailView):
    model = Vendor
    ribbon_profile = DOCUMENT_RIBBON

get_ribbon_profile() can be used instead of the class attribute when the profile is dynamic:

python
class VendorListView(BaseListView):
    model = Vendor

    def get_ribbon_profile(self):
        return DOCUMENT_RIBBON

2. Add a tab

Use RibbonTabSpec:

python
tabs=(
    RibbonTabSpec(
        key="approval",
        label="Approval",
        context="both",
        order=10,
    ),
),

Tab icons are optional. Omit icon for a text-only tab.

context controls where the tab is available:

Context Available in
list List ribbon only
detail Detail ribbon only
both List and detail ribbons

Use tabs for a shared tab. Use list_tabs or detail_tabs when the same tab key needs different labels, ordering or visibility per context.

python
RibbonProfileSpec(
    key="vendor",
    list_tabs=(
        RibbonTabSpec(key="approval", label="Approval", context="list", order=10),
    ),
    detail_tabs=(
        RibbonTabSpec(key="approval", label="Approval", context="detail", order=10),
    ),
)

Tab order is the order value. Base tabs (Home, Options, View) are merged automatically; a custom tab is appended in its order position.


3. Add a group

Groups are declared on the menu items, not separately. Give related items the same group title:

python
menus=(
    RibbonMenuItemSpec(
        code="submit-for-review",
        label="Submit for Review",
        tab="approval",
        group="Status",
        group_order=1,
        order=1,
        kind="action",
        url_name="vendor_submit",
    ),
    RibbonMenuItemSpec(
        code="approve",
        label="Approve",
        tab="approval",
        group="Status",
        group_order=1,
        order=2,
        kind="action",
        url_name="vendor_approve",
    ),
)

Both items appear in one Status group inside the Approval tab.

  • group is the visible group title.
  • group_order controls the group's position within the tab.
  • order controls the item's position within the group.

A group is limited to three items vertically. A fourth item automatically flows into a new column inside the same group. Add items in the order you want them to fill the grid.


4. Add a menu item

An item is attached by two values:

  • tab — the tab key (home, options, view, or a custom key such as approval).
  • group — the group title inside that tab.

If the group title already exists in the tab, the item joins it. If not, the group is created automatically. There is no separate group registration step.

Quick recipes

Add a button to an existing group

python
menus=(
    RibbonMenuItemSpec(
        code="vendor-export",
        label="Export",
        tab="options",
        group="Output",
        group_order=1,
        order=2,
        context="both",
        kind="link",
        url_name="vendor_export",
        display_mode="full_page",
    ),
),

Add a new group

python
RibbonMenuItemSpec(
    code="vendor-audit",
    label="Audit",
    tab="manage",
    group="Compliance",
    group_order=3,
    order=1,
    context="both",
    kind="link",
    url_name="vendor_audit",
)

The new Compliance group is created in the Manage tab because no other item uses that group title.

Add to the base Home, Options or View tabs

python
RibbonMenuItemSpec(
    code="home-refresh",
    label="Refresh record",
    tab="home",
    group="Record",
    group_order=1,
    order=1,
    context="detail",
    kind="action",
    url_name="vendor_refresh",
)

Use tab="options" for the default action tab, tab="view" for display actions, or tab="home" for primary actions.

Add to a custom tab

python
RibbonTabSpec(
    key="approval",
    label="Approval",
    context="both",
    order=10,
)
python
RibbonMenuItemSpec(
    code="approve",
    label="Approve",
    tab="approval",
    group="Status",
    group_order=1,
    order=1,
    context="both",
    kind="action",
    url_name="vendor_approve",
    confirm_message="Approve this vendor?",
)

Add list-only, detail-only or both

python
context="list"     # list ribbon only, acts on the selected row
context="detail"   # detail ribbon only, acts on the open record
context="both"     # both ribbons

If the menu should appear in the list ribbon, the list view must also have ribbon_profile.

Add an action item

python
RibbonMenuItemSpec(
    code="put-on-hold",
    label="Put on Hold",
    tab="options",
    group="Status",
    order=1,
    context="both",
    kind="action",
    url_name="vendor_put_on_hold",
    confirm_message="Put this vendor on hold?",
)

The action view:

python
from enezaframework.utils import auto_url_register
from enezaframework.views import BaseAction


@auto_url_register
class VendorPutOnHoldAction(BaseAction):
    model = Vendor
    action_name = "vendor_put_on_hold"

    def process_action(self, pk):
        Vendor.objects.filter(pk=pk).update(status="on_hold")

action_name="vendor_put_on_hold" registers the URL vendor_put_on_hold_action. The menu may use either url_name="vendor_put_on_hold" or url_name="vendor_put_on_hold_action".

Add a report/link item

python
RibbonMenuItemSpec(
    code="print",
    label="Print",
    icon="printer",
    tab="options",
    group="Output",
    group_order=1,
    order=1,
    context="both",
    kind="link",
    url_name="vendor_report",
    display_mode="full_page",
    target="_blank",
)

Add a related-record item

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

Add a dropdown

python
RibbonMenuItemSpec(
    code="more",
    label="More",
    tab="options",
    group="Actions",
    order=1,
    context="both",
    kind="dropdown",
    children=(
        RibbonMenuItemSpec(
            code="audit",
            label="Audit",
            kind="link",
            url_name="vendor_audit",
        ),
        RibbonMenuItemSpec(
            code="archive",
            label="Archive",
            kind="action",
            url_name="vendor_archive",
            confirm_message="Archive this vendor?",
        ),
    ),
)

Add permissions

python
RibbonMenuItemSpec(
    code="approve",
    label="Approve",
    tab="approval",
    group="Status",
    context="both",
    kind="action",
    url_name="vendor_approve",
    permissions=("accountpayable.approve_vendor",),
    groups=("Credit Control",),
)

All permissions must be held; if groups are listed, the user must belong to at least one.

Item ordering and columns

  • group_order positions the group inside the tab.
  • order positions the item inside the group.
  • Groups show three items vertically. The fourth item flows into a new column in the same group, then the seventh into the next column.

Item kinds

RibbonMenuItemSpec supports five kinds:

Action

Runs a registered BaseAction URL. url_name may be the action name without the _action suffix; the resolver tries both forms.

python
RibbonMenuItemSpec(
    code="put-on-hold",
    label="Put on Hold",
    icon="check",
    tab="approval",
    group="Status",
    kind="action",
    url_name="vendor_put_on_hold",
    confirm_message="Put this vendor on hold?",
)

The action view keeps the business logic:

python
from enezaframework.views import BaseAction
from enezaframework.utils import auto_url_register


@auto_url_register
class VendorPutOnHoldAction(BaseAction):
    model = Vendor
    action_name = "vendor_put_on_hold"

    def process_action(self, pk):
        Vendor.objects.filter(pk=pk).update(status="on_hold")

Opens a normal URL. Use display_mode="full_page" for a report or another page.

python
RibbonMenuItemSpec(
    code="print",
    label="Print",
    icon="printer",
    tab="options",
    group="Output",
    kind="link",
    url_name="vendor_report",
    display_mode="full_page",
    target="_blank",
)

Opens another record's list, setup page, detail page or inline panel.

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 Behavior
inline Existing inline list/detail views in a panel.
workspace Related list/detail/create/edit inside the parent content area.
full_page Full browser navigation, useful for setup/master-detail pages.
python
RibbonMenuItemSpec(
    code="more",
    label="More",
    tab="options",
    group="Actions",
    kind="dropdown",
    children=(
        RibbonMenuItemSpec(
            code="audit",
            label="Audit",
            kind="link",
            url_name="vendor_audit",
        ),
        RibbonMenuItemSpec(
            code="archive",
            label="Archive",
            kind="action",
            url_name="vendor_archive",
            confirm_message="Archive this vendor?",
        ),
    ),
)

Only one dropdown level is supported.

Separator

python
RibbonMenuItemSpec(
    code="status-separator",
    label="",
    tab="approval",
    group="Status",
    kind="separator",
)

5. Choose list, detail or both

Set the menu context explicitly. Do not rely on the tab context alone.

python
RibbonMenuItemSpec(
    code="approve",
    label="Approve",
    tab="approval",
    group="Status",
    context="both",   # list and detail
    kind="action",
    url_name="vendor_approve",
)
Context Appears in
list List ribbon. Acts on the selected row.
detail Detail ribbon. Acts on the open record.
both Both.

List-context items with requires_object=True are disabled until exactly one row is selected.


6. Available menu fields

Field Default Purpose
code required Stable identifier.
label required Visible text.
icon empty Icon name loaded from static/icons.
tab options Tab key.
context detail list, detail or both.
group, group_order, order General, 0, 0 Group title and ordering.
kind action action, link, related, dropdown, separator.
url_name empty Django URL name.
http_method POST for actions Actions are executed as POST.
confirm_message empty Confirmation prompt.
display_mode workspace inline, workspace, full_page.
target_kind custom list, setup, detail, custom.
target_app_label, target_model empty Related target model.
target_url_name empty Explicit target URL.
parent_lookup empty FK/O2O on the target model.
requires_object True Requires a saved/selected record.
show_in_edit, show_in_create True Mode visibility.
permissions, groups empty Optional visibility rules.
children empty Dropdown children.
is_active True Enable/disable.

7. Complete example

The Framework Test Document profile is a working example:

python
FRAMEWORK_TEST_DOCUMENT_RIBBON = RibbonProfileSpec(
    key="frameworkapp.frameworktestdocument",
    name="Framework Test Document",
    default_tab="options",
    tabs=(
        RibbonTabSpec(
            key="approval",
            label="Approval",
            context="both",
            order=10,
        ),
    ),
    menus=(
        RibbonMenuItemSpec(
            code="print-document",
            label="Print",
            icon="printer",
            tab="options",
            group="Output",
            group_order=1,
            order=1,
            context="both",
            kind="link",
            url_name="frameworktestdocument_report",
            display_mode="full_page",
            target="_blank",
        ),
        RibbonMenuItemSpec(
            code="approve-document",
            label="Approve",
            icon="check",
            tab="approval",
            group="Status",
            group_order=2,
            order=2,
            context="both",
            kind="action",
            url_name="frameworktestdocument_approve",
            confirm_message="Approve this document?",
        ),
    ),
)

This produces:

  • Options → Output → Print
  • Approval → Status → Approve
  • On both list and detail ribbons.

8. Checklist

  1. Define a RibbonProfileSpec with a stable key.
  2. Add tabs (or list_tabs/detail_tabs) if you need custom tabs.
  3. Add RibbonMenuItemSpec entries with tab, group, group_order and order.
  4. Set menu context to list, detail or both.
  5. Attach ribbon_profile to the list view and detail view.
  6. For custom actions, create a BaseAction and set action_name.
  7. Check permissions and requires_object behavior.
  8. Refresh the page; invalid URL names fail fast in DEBUG.

Common mistakes

Problem Fix
Custom tab missing in list Use context="both" in tabs, or add the tab to list_tabs.
Menus appear on list but should not Set context="detail".
Menus missing on list Ensure the list view has ribbon_profile and context="both"/"list".
Group not merging Use the same exact group title on the items.
Fourth item goes below instead of a new column The ribbon CSS handles this automatically; do not override .ez-ribbon-cmds with a flex column.
Action URL not found Use the full app:url_name or the action name without _action; check URL_REGISTRY.
Direct detail URL loses the profile Add ribbon_profile to the detail view or register the profile at import time.
List menu does nothing Select exactly one row; requires_object items are disabled otherwise.