Merge pull request #4403 from netbox-community/4401-plugins-navlinks

Closes #4401: Simplify registration process for pluin menu items
This commit is contained in:
Jeremy Stretch 2020-03-25 14:55:21 -04:00 committed by GitHub
commit 1d9fbeed81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 71 deletions

View File

@ -106,6 +106,7 @@ class AnimalSoundsConfig(PluginConfig):
* `max_version`: Maximum version of NetBox with which the plugin is compatible * `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. * `middleware`: A list of middleware classes to append after NetBox's build-in middleware.
* `caching_config`: Plugin-specific cache configuration * `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 ### 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 ## 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 ```python
from extras.plugins import PluginNavMenuLink from extras.plugins import PluginNavMenuButton, PluginNavMenuLink
from utilities.choices import ButtonColorChoices
class RandomSoundLink(PluginNavMenuLink): menu_items = (
link = 'plugins:netbox_animal_sounds:random_sound' PluginNavMenuLink(
link_text = 'Random sound' 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 * `link` - The name of the URL path to which this menu item links
from django.dispatch import receiver * `link_text` - The text presented to the user
from extras.plugins.signals import register_nav_menu_link_classes * `permission` - The name of the permission required to display this link (optional)
from .navigation import RandomSoundLink * `buttons` - An iterable of PluginNavMenuButton instances to display (optional)
@receiver(register_nav_menu_link_classes)
def nav_menu_link_classes(**kwargs): A `PluginNavMenuButton` has the following attributes:
return [RandomSoundLink]
``` * `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)

View File

@ -1,11 +1,16 @@
import collections import collections
import inspect import inspect
from django.apps import AppConfig, apps from django.apps import AppConfig
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.module_loading import import_string
from extras.registry import registry 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 configuration
caching_config = {} 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 # Template content injection
@ -138,7 +156,7 @@ def get_content_classes(model):
# #
# Nav menu links # Navigation menu links
# #
class PluginNavMenuLink: class PluginNavMenuLink:
@ -149,10 +167,14 @@ class PluginNavMenuLink:
Links are specified as Django reverse URL strings. Links are specified as Django reverse URL strings.
Buttons are each specified as a list of PluginNavMenuButton instances. Buttons are each specified as a list of PluginNavMenuButton instances.
""" """
link = None def __init__(self, link, link_text, permission=None, buttons=None):
link_text = None self.link = link
link_permission = None self.link_text = link_text
buttons = [] self.permission = permission
if buttons is None:
self.buttons = []
else:
self.buttons = buttons
class PluginNavMenuButton: class PluginNavMenuButton:
@ -168,42 +190,16 @@ class PluginNavMenuButton:
self.permission = permission 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:
responses = register_nav_menu_link_classes.send('registration_event') if not isinstance(menu_link, PluginNavMenuLink):
for receiver, response in responses: raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginNavMenuLink")
for button in menu_link.buttons:
# 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): if not isinstance(button, PluginNavMenuButton):
raise TypeError('{} must be an instance of PluginNavMenuButton!'.format(button)) raise TypeError(f"{button} must be an instance of extras.plugins.PluginNavMenuButton")
registry['plugin_nav_menu_link_classes'][section_name] = response registry['plugin_nav_menu_links'][section_name] = class_list
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']

View File

@ -1,12 +1,10 @@
from . import get_nav_menu_link_classes from extras.registry import registry
def nav_menu_links(request): def nav_menu_links(request):
""" """
Retrieve and expose all plugin registered nav links Retrieve and expose all plugin registered nav links
""" """
nav_menu_links = get_nav_menu_link_classes()
return { return {
'plugin_nav_menu_links': nav_menu_links 'plugin_nav_menu_links': registry['plugin_nav_menu_links']
} }

View File

@ -32,11 +32,3 @@ This signal collects template content classes which render content for object de
register_detail_page_content_classes = PluginSignal( register_detail_page_content_classes = PluginSignal(
providing_args=[] providing_args=[]
) )
"""
This signal collects nav menu link classes
"""
register_nav_menu_link_classes = PluginSignal(
providing_args=[]
)

View File

@ -4,8 +4,8 @@
{% for section_name, link_items in plugin_nav_menu_links.items %} {% for section_name, link_items in plugin_nav_menu_links.items %}
<li class="dropdown-header">{{ section_name }}</li> <li class="dropdown-header">{{ section_name }}</li>
{% for link_item in link_items %} {% for link_item in link_items %}
{% if link_item.link_permission %} {% if link_item.permission %}
<li{% if not link_item.link_permission in perms %} class="disabled"{% endif %}> <li{% if not link_item.permission in perms %} class="disabled"{% endif %}>
{% else %} {% else %}
<li> <li>
{% endif %} {% endif %}