Просмотр исходного кода

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

Closes #4401: Simplify registration process for pluin menu items
Jeremy Stretch 5 лет назад
Родитель
Сommit
1d9fbeed81

+ 28 - 15
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
-
-class RandomSoundLink(PluginNavMenuLink):
-    link = 'plugins:netbox_animal_sounds:random_sound'
-    link_text = 'Random sound'
+from extras.plugins import PluginNavMenuButton, PluginNavMenuLink
+from utilities.choices import ButtonColorChoices
+
+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)

+ 39 - 43
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():
-    """
-    Helper method that populates the registry with all nav menu link classes that have been registered by plugins
-    """
-    registry['plugin_nav_menu_link_classes'] = {}
-
-    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():
+def register_menu_items(section_name, class_list):
     """
-    Return the list of all registered nav menu link classes.
-    Populate the registry if it is empty.
+    Register a list of PluginNavMenuLink instances for a given menu section (e.g. plugin name)
     """
-    if 'plugin_nav_menu_link_classes' not in registry:
-        register_nav_menu_links()
+    # 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")
 
-    return registry['plugin_nav_menu_link_classes']
+    registry['plugin_nav_menu_links'][section_name] = class_list

+ 2 - 4
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']
     }

+ 0 - 8
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=[]
-)

+ 2 - 2
netbox/templates/inc/plugin_nav_menu_items.html

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