Menu System
If you're already familiar with how Pinmok menus work, expand the Quick Start below and skip the rest.
Quick Start
1. Create menus.py in your app's root directory
2. Define your menus
# Import the menu definition function
from pinmok.core import menu
# admin_menu is a reserved variable — do not change it
admin_menu = [
menu('blog', title='Blog', icon='tabler-book', sort_order=0), # root item, no parent_key
menu('posts', title='Posts', url='posts', parent_key='blog', sort_order=100), # child item
menu('category', title='Category', url='category', parent_key='blog', sort_order=200),
]
Parameters:
key: First positional argument. Unique identifier for the menu item, must be unique within the same app.title: Menu label shown in the admin navigation. Write plain strings — i18n is handled automatically at render time. Just maintain the translations in your.pofiles.url: Page URL. Accepts a URL name or an absolute path starting with/. Can be omitted for root items with no direct link.parent_key: Thekeyof the parent item. Omit to make this a root item.sort_order: Sort weight. Lower values appear first. Default is10000.icon: Icon name from the admin icon library or a custom icon, e.g.tabler-book.remark: Developer note. Not displayed in the frontend.
3. Log in to the admin, click the red ☰ icon at the top of the left sidebar, and confirm the sync
Refresh the page — your menus will appear.
Overview
Django Admin's navigation is entirely driven by registered models. Without a corresponding model, there is no menu entry, making it
difficult to integrate non-CRUD business functions into the admin. Pinmok extends this by allowing developers to define menu items in
menus.py that can point to any URL, so pure business logic pages, utility views, statistics dashboards, and other non-model functions can
all appear in the admin navigation. You can build apps that have no models at all, or mix model management and custom business views within
the same menu structure.
Defining Menus
File Location
Create a menus.py file in your app's root directory. During menu synchronization, Pinmok scans the root directory of each installed app
for a menus.py file and loads the definitions. The file must be in the app root — placing it in a subdirectory or sub-package will
prevent it from being discovered. The filename is a system convention and cannot be changed.
Basic Structure
The structure of menus.py is straightforward: import the menu() function from pinmok.core, define all menu items as a list, and assign
it to the reserved variable name admin_menu. The system reads menu definitions from this variable during synchronization. The variable
name cannot be changed.
from pinmok.core import menu
admin_menu = [
menu('blog', title='Blog Management', icon='tabler-book', sort_order=0),
menu('posts', title='Posts', url='posts', parent_key='blog', sort_order=100),
menu('category', title='Category', url='category', parent_key='blog', sort_order=200),
menu(
'stat',
title='Statistics',
url='post_stat',
parent_key='blog',
sort_order=300,
remark='Relevant data statistics of the posts.',
),
menu('dashboard', title='Data Dashboard', url='post_dashboard', parent_key='stat', sort_order=100),
menu('traffic', title='Traffic Analysis', url='post_traffic', parent_key='stat', sort_order=200),
]
The menu structure this produces in the admin:
Blog Management
├─ Posts
├─ Category
└─ Statistics
├── Data Dashboard
└── Traffic Analysis
An app can define multiple root menu items — there is no requirement to nest everything under a single root.
Parameters
The first argument to menu() is key, a positional parameter. All others are keyword arguments:
key (required)
A unique identifier for the menu item, str type. Must be unique within the same app. Used to establish parent-child relationships via
parent_key and for internal indexing.
title (required)
The menu label displayed in the admin navigation, str type. Write plain strings — do not wrap with gettext. Internationalization is
automatic — Pinmok translates titles at render time using Django's standard i18n system. Simply add the translations to your .po files.`
url
The URL the menu item points to, str type, default None. Two formats are supported:
- Starting with
/— treated as an absolute path and used as-is - Otherwise, treated as a URL name; the system calls
reverse()at sync time to resolve it to an actual path
If the URL name cannot be resolved, the sync operation will fail immediately with an error. Root items with no direct link typically do not
need a url.
parent_key
The key of the parent menu item, str type, default None. Omit to make this a root item.
sort_order
Sort weight, int type, default 10000. Items at the same level are sorted in ascending order — lower values appear first.
icon
Menu icon, str type, default None. Specify an icon name from the admin icon library or your custom icon set, e.g. tabler-book.
permissions
Permission list, list[str] type, default None. Uses Django's standard permission string format, e.g. ["app.view_model"].
Note: The
permissionsparameter is reserved for future use. Permission filtering is not implemented in the current version and has no effect.
remark
Developer note, str type, default None. Not displayed in the frontend — for internal documentation only.
Depth Limit
The admin interface renders menus up to 3 levels deep (root → child → grandchild). Menu definitions themselves have no depth limit, but items nested beyond the third level will not appear. Keep this in mind when designing your menu structure.
Synchronizing Menus
Once you have defined your menus, you need to run a manual sync to write the menu data to the database before the admin can display them.
How to Sync
Log in to the admin with a superuser account. At the top of the left sidebar, there is a red ☰ icon — this entry is only visible to
superusers. Clicking it will show a confirmation prompt. The sync operation clears all existing menu data and rewrites it from scratch; this
cannot be undone. After confirming, refresh the page and your new menus will be active.
When to Re-sync
Any time your menu definitions change, you need to re-sync. Common cases include:
- Installing a new app
- Modifying menu definitions in
menus.py
The sync operation automatically clears the menu cache — no manual cache management is needed.
Relationship with Model Menus
When rendering the admin navigation, Pinmok automatically merges the menus defined in menus.py with Django Admin's model registration
menus (app_list). Under the same app_label, both sources are merged under a single root node rather than appearing as two separate root
menus.
If an app defines multiple root menu items, model menus will be merged under the first root item in the list. The current version does not
support specifying a different merge target — if you need to control which root item receives the model menus, adjust the order of root
items in your admin_menu list.
After merging, all items are sorted by sort_order. For model menu items, Pinmok adds a menu_sort_order attribute to ModelAdmin to
control their position within the merged menu. The default value is 10000:
@padmin.register(EmailConfig)
class EmailConfigAdmin(ConfigModelAdmin):
menu_sort_order = 2000
If your admin has a mixed menu, make sure to set sort_order values thoughtfully to avoid unexpected ordering.
Menu Caching
Pinmok applies two layers of caching to reduce database query overhead.
Layer 1: Database menu cache
The full set of menu data loaded from the database is cached as a whole, with a TTL of one hour. The cache key includes a version number that increments automatically after each sync, immediately invalidating the previous cache.
Layer 2: Per-user menu cache
The final merged menu tree — after combining with app_list — is cached per user, also using the version-based invalidation mechanism. It
is cleared automatically after each sync.
During development: After modifying
menus.py, always re-sync. The sync will clear the cache automatically. If your menu changes are not showing up, check whether the sync has been run.