diff --git a/docs/plugins/development.md b/docs/plugins/development.md index 67abeb8d5..71ff255cf 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -106,6 +106,7 @@ class AnimalSoundsConfig(PluginConfig): * `max_version`: Maximum version of NetBox with which the plugin is compatible * `middleware`: A list of middleware classes to append after NetBox's build-in middleware. * `caching_config`: Plugin-specific cache configuration +* `menu_items`: The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) ### Install the Plugin for Development @@ -271,24 +272,36 @@ With these three components in place, we can request `/api/plugins/animal-sounds ## Navigation Menu Items -To make its views easily accessible to users, a plugin can inject items in NetBox's navigation menu. This is done by instantiating NetBox's `PluginNavMenuLink` class. Each instance of this class appears in the navigation menu under the header for its plugin. We'll create a link in the file `navigation.py`: +To make its views easily accessible to users, a plugin can inject items in NetBox's navigation menu. Menu items are added by defining a list of PluginNavMenuLink instances. By default, this should be a variable named `menu_items` in the file `navigations.py`. An example is shown below. ```python -from extras.plugins import PluginNavMenuLink +from extras.plugins import PluginNavMenuButton, PluginNavMenuLink +from utilities.choices import ButtonColorChoices -class RandomSoundLink(PluginNavMenuLink): - link = 'plugins:netbox_animal_sounds:random_sound' - link_text = 'Random sound' +menu_items = ( + PluginNavMenuLink( + link='plugins:netbox_animal_sounds:random_sound', + link_text='Random sound', + buttons=( + PluginNavMenuButton('home', 'Button A', 'fa-info', ButtonColorChoices.BLUE), + PluginNavMenuButton('home', 'Button B', 'fa-warning', ButtonColorChoices.GREEN), + ) + ), +) ``` -Once we have our menu item defined, we need to register it in `signals.py`: +A `PluginNavMenuLink` has the following attributes: -```python -from django.dispatch import receiver -from extras.plugins.signals import register_nav_menu_link_classes -from .navigation import RandomSoundLink +* `link` - The name of the URL path to which this menu item links +* `link_text` - The text presented to the user +* `permission` - The name of the permission required to display this link (optional) +* `buttons` - An iterable of PluginNavMenuButton instances to display (optional) -@receiver(register_nav_menu_link_classes) -def nav_menu_link_classes(**kwargs): - return [RandomSoundLink] -``` + +A `PluginNavMenuButton` has the following attributes: + +* `link` - The name of the URL path to which this menu item links +* `title` - The tooltip text (displayed when the mouse hovers over the button) +* `color` - Button color (one of the choices provided by `ButtonColorChoices`) +* `icon_class` - Button icon CSS class +* `permission` - The name of the permission required to display this button (optional) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index b88af8c1c..a0ebec7e9 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -1,11 +1,16 @@ import collections import inspect -from django.apps import AppConfig, apps +from django.apps import AppConfig from django.template.loader import get_template +from django.utils.module_loading import import_string from extras.registry import registry -from .signals import register_detail_page_content_classes, register_nav_menu_link_classes +from .signals import register_detail_page_content_classes + + +# Initialize plugin registry stores +registry['plugin_nav_menu_links'] = {} # @@ -41,6 +46,19 @@ class PluginConfig(AppConfig): # Caching configuration caching_config = {} + # Default integration paths. Plugin authors can override these to customize the paths to + # integrated components. + menu_items = 'navigation.menu_items' + + def ready(self): + + # Register navigation menu items (if defined) + try: + menu_items = import_string(f"{self.__module__}.{self.menu_items}") + register_menu_items(self.verbose_name, menu_items) + except ImportError: + pass + # # Template content injection @@ -138,7 +156,7 @@ def get_content_classes(model): # -# Nav menu links +# Navigation menu links # class PluginNavMenuLink: @@ -149,10 +167,14 @@ class PluginNavMenuLink: Links are specified as Django reverse URL strings. Buttons are each specified as a list of PluginNavMenuButton instances. """ - link = None - link_text = None - link_permission = None - buttons = [] + def __init__(self, link, link_text, permission=None, buttons=None): + self.link = link + self.link_text = link_text + self.permission = permission + if buttons is None: + self.buttons = [] + else: + self.buttons = buttons class PluginNavMenuButton: @@ -168,42 +190,16 @@ class PluginNavMenuButton: self.permission = permission -def register_nav_menu_links(): +def register_menu_items(section_name, class_list): """ - Helper method that populates the registry with all nav menu link classes that have been registered by plugins + Register a list of PluginNavMenuLink instances for a given menu section (e.g. plugin name) """ - registry['plugin_nav_menu_link_classes'] = {} + # Validation + for menu_link in class_list: + if not isinstance(menu_link, PluginNavMenuLink): + raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginNavMenuLink") + for button in menu_link.buttons: + if not isinstance(button, PluginNavMenuButton): + raise TypeError(f"{button} must be an instance of extras.plugins.PluginNavMenuButton") - responses = register_nav_menu_link_classes.send('registration_event') - for receiver, response in responses: - - # Derive menu section header from plugin name - app_config = apps.get_app_config(receiver.__module__.split('.')[0]) - section_name = getattr(app_config, 'verbose_name', app_config.name) - - if not isinstance(response, list): - response = [response] - for link_class in response: - if not inspect.isclass(link_class): - raise TypeError('Plugin nav menu link class {} was passes as an instance!'.format(link_class)) - if not issubclass(link_class, PluginNavMenuLink): - raise TypeError('{} is not a subclass of extras.plugins.PluginNavMenuLink!'.format(link_class)) - if link_class.link is None or link_class.link_text is None: - raise TypeError('Plugin nav menu link {} must specify at least link and link_text'.format(link_class)) - - for button in link_class.buttons: - if not isinstance(button, PluginNavMenuButton): - raise TypeError('{} must be an instance of PluginNavMenuButton!'.format(button)) - - registry['plugin_nav_menu_link_classes'][section_name] = response - - -def get_nav_menu_link_classes(): - """ - Return the list of all registered nav menu link classes. - Populate the registry if it is empty. - """ - if 'plugin_nav_menu_link_classes' not in registry: - register_nav_menu_links() - - return registry['plugin_nav_menu_link_classes'] + registry['plugin_nav_menu_links'][section_name] = class_list diff --git a/netbox/extras/plugins/context_processors.py b/netbox/extras/plugins/context_processors.py index 14b05b874..fbb4e8c03 100644 --- a/netbox/extras/plugins/context_processors.py +++ b/netbox/extras/plugins/context_processors.py @@ -1,12 +1,10 @@ -from . import get_nav_menu_link_classes +from extras.registry import registry def nav_menu_links(request): """ Retrieve and expose all plugin registered nav links """ - nav_menu_links = get_nav_menu_link_classes() - return { - 'plugin_nav_menu_links': nav_menu_links + 'plugin_nav_menu_links': registry['plugin_nav_menu_links'] } diff --git a/netbox/extras/plugins/signals.py b/netbox/extras/plugins/signals.py index 7ebb549db..fbe30a310 100644 --- a/netbox/extras/plugins/signals.py +++ b/netbox/extras/plugins/signals.py @@ -32,11 +32,3 @@ This signal collects template content classes which render content for object de register_detail_page_content_classes = PluginSignal( providing_args=[] ) - - -""" -This signal collects nav menu link classes -""" -register_nav_menu_link_classes = PluginSignal( - providing_args=[] -) diff --git a/netbox/templates/inc/plugin_nav_menu_items.html b/netbox/templates/inc/plugin_nav_menu_items.html index b79044273..55aab84c1 100644 --- a/netbox/templates/inc/plugin_nav_menu_items.html +++ b/netbox/templates/inc/plugin_nav_menu_items.html @@ -4,8 +4,8 @@ {% for section_name, link_items in plugin_nav_menu_links.items %} {% for link_item in link_items %} - {% if link_item.link_permission %} - + {% if link_item.permission %} + {% else %}
  • {% endif %}