Simplify approach for registering plugin navigation items #3503

Closed
opened 2025-12-29 18:29:33 +01:00 by adam · 1 comment
Owner

Originally created by @jeremystretch on GitHub (Mar 25, 2020).

Proposed Changes

I'd like a to adopt a simpler mechanism than what currently exists in the 3351-plugins branch for plugins to register navigation menu items. We have a few options, which I'll outline here.

Current approach: Signals

The current approach requires three components. First, we declare the menu items in a file:

navigation.py

class NavLink1(PluginNavMenuLink):
    link = 'plugins:my_plugin:view1'
    link_text = 'Link A'

class NavLink2(PluginNavMenuLink):
    link = 'plugins:my_plugin:view2'
    link_text = 'Link B'

Next, we create a function to return these classes, and register it as a signal receiver:

signals.py

@receiver(register_nav_menu_link_classes)
def nav_menu_link_classes(**kwargs):
    return [NavLink1, NavLink2]

Finally, we extend our PluginApp to import this function on ready(). (We could probably build this part into the PluginApp class.)

init.py

class MyPluginConfig(PluginConfig):
    ...

    def ready(self):
        from . import signals

Proposal: Import of a known variable

The first option I propose is to direct plugin authors to define a single variable within a standard file, i.e. navigation.py, which contains all navigation menu items for their plugin. (This is very similar in concept to the way a plugin currently declares URL patterns in urls.py.)

navigation.py

class NavLink1(PluginNavMenuLink):
    link = 'plugins:my_plugin:view1'
    link_text = 'Link A'

class NavLink2(PluginNavMenuLink):
    link = 'plugins:my_plugin:view2'
    link_text = 'Link B'

menu_items = (NavLink1, NavLink2)

NetBox would automatically import and register the classes in menu_items (if defined by the plugin). This could be handled by the PluginConfig's ready() method, like how we're currently handling signals.

With a minor tweak to the PluginNavMenuLink class to accept attributes on initialization, the above example could be further simplified to skip naming the individual instances:

navigation.py

menu_items = (
    PluginNavMenuLink(
        link='plugins:my_plugin:view1',
        link_text='Link A'
    ),
    PluginNavMenuLink(
        link='plugins:my_plugin:view2',
        link_text='Link B'
    ),
)

Justification

I prefer this approach for four reasons:

  1. It minimizes the amount of code needed from the plugin author.
  2. It is much easier to understand what's happening, especially for someone unfamiliar with Django signals.
  3. It provides the simplest possible integration point: A single variable.
  4. This approach is already well-established by Django's URL patterns mechanism (wherein each app provides a urlpatterns variable).
Originally created by @jeremystretch on GitHub (Mar 25, 2020). ### Proposed Changes I'd like a to adopt a simpler mechanism than what currently exists in the `3351-plugins` branch for plugins to register navigation menu items. We have a few options, which I'll outline here. ### Current approach: Signals The current approach requires three components. First, we declare the menu items in a file: **navigation.py** ```python class NavLink1(PluginNavMenuLink): link = 'plugins:my_plugin:view1' link_text = 'Link A' class NavLink2(PluginNavMenuLink): link = 'plugins:my_plugin:view2' link_text = 'Link B' ``` Next, we create a function to return these classes, and register it as a signal receiver: **signals.py** ```python @receiver(register_nav_menu_link_classes) def nav_menu_link_classes(**kwargs): return [NavLink1, NavLink2] ``` Finally, we extend our PluginApp to import this function on `ready()`. (We could probably build this part into the PluginApp class.) **__init__.py** ```python class MyPluginConfig(PluginConfig): ... def ready(self): from . import signals ``` ### Proposal: Import of a known variable The first option I propose is to direct plugin authors to define a single variable within a standard file, i.e. `navigation.py`, which contains all navigation menu items for their plugin. (This is very similar in concept to the way a plugin currently declares URL patterns in `urls.py`.) **navigation.py** ```python class NavLink1(PluginNavMenuLink): link = 'plugins:my_plugin:view1' link_text = 'Link A' class NavLink2(PluginNavMenuLink): link = 'plugins:my_plugin:view2' link_text = 'Link B' menu_items = (NavLink1, NavLink2) ``` NetBox would automatically import and register the classes in `menu_items` (if defined by the plugin). This could be handled by the PluginConfig's `ready()` method, like how we're currently handling signals. With a minor tweak to the PluginNavMenuLink class to accept attributes on initialization, the above example could be further simplified to skip naming the individual instances: **navigation.py** ```python menu_items = ( PluginNavMenuLink( link='plugins:my_plugin:view1', link_text='Link A' ), PluginNavMenuLink( link='plugins:my_plugin:view2', link_text='Link B' ), ) ``` ### Justification I prefer this approach for four reasons: 1. It minimizes the amount of code needed from the plugin author. 2. It is much easier to understand what's happening, especially for someone unfamiliar with Django signals. 3. It provides the simplest possible integration point: A single variable. 4. This approach is already well-established by Django's URL patterns mechanism (wherein each app provides a `urlpatterns` variable).
adam added the status: accepted label 2025-12-29 18:29:33 +01:00
adam closed this issue 2025-12-29 18:29:33 +01:00
Author
Owner

@jeremystretch commented on GitHub (Mar 25, 2020):

I should also point out that we do want to require a list/tuple of items specifically, to ensure that the menu items are added in the desired order.

@jeremystretch commented on GitHub (Mar 25, 2020): I should also point out that we do want to require a list/tuple of items specifically, to ensure that the menu items are added in the desired order.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#3503