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

added support for plugin nav bar links

John Anderson 6 лет назад
Родитель
Сommit
981c982237

+ 88 - 1
netbox/extras/plugins/__init__.py

@@ -1,13 +1,18 @@
 import collections
+import importlib
 import inspect
 
 from django.core.exceptions import ImproperlyConfigured
 from django.template.loader import get_template
 
 from extras.utils import registry
-from .signals import register_detail_page_content_classes
+from .signals import register_detail_page_content_classes, register_nav_menu_link_classes
 
 
+#
+# Template content injection
+#
+
 class PluginTemplateContent:
     """
     This class is used to register plugin content to be injected into core NetBox templates.
@@ -68,6 +73,9 @@ class PluginTemplateContent:
 
 
 def register_content_classes():
+    """
+    Helper method that populates the registry with all template content classes that have been registered by plugins
+    """
     registry.plugin_template_content_classes = collections.defaultdict(list)
 
     responses = register_detail_page_content_classes.send('registration_event')
@@ -86,7 +94,86 @@ def register_content_classes():
 
 
 def get_content_classes(model):
+    """
+    Given a model string, return the list of all registered template content classes.
+    Populate the registry if it is empty.
+    """
     if not hasattr(registry, 'plugin_template_content_classes'):
         register_content_classes()
 
     return registry.plugin_template_content_classes.get(model, [])
+
+
+#
+# Nav menu links
+#
+
+class PluginNavMenuLink:
+    """
+    This class represents a nav menu item. This constitutes primary link and its text, but also allows for
+    specifying additional link buttons that appear to the right of the item in the van menu.
+
+    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 = []
+
+
+class PluginNavMenuButton:
+    """
+    This class represents a button which is a part of the nav menu link item.
+    Note that button colors should come from ButtonColorChoices
+    """
+    def __init__(self, link, title, icon_class, color, permission=None):
+        self.link = link
+        self.title = title
+        self.icon_class = icon_class
+        self.color = color
+        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:
+
+        # Import the app config for the plugin to get the name to be used as the nav menu section text
+        module = importlib.import_module(receiver.__module__.split('.')[0])
+        default_app_config = getattr(module, 'default_app_config')
+        module, app_config = default_app_config.rsplit('.', 1)
+        app_config = getattr(importlib.import_module(module), app_config)
+        section_name = app_config.NetBoxPluginMeta.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 not hasattr(registry, 'plugin_nav_menu_link_classes'):
+        register_nav_menu_links()
+
+    return registry.plugin_nav_menu_link_classes

+ 12 - 0
netbox/extras/plugins/context_processors.py

@@ -0,0 +1,12 @@
+from . import get_nav_menu_link_classes
+
+
+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
+    }

+ 9 - 1
netbox/extras/plugins/signals.py

@@ -27,8 +27,16 @@ class PluginSignal(django.dispatch.Signal):
 
 
 """
-This signal collects templates which render content for object detail pages
+This signal collects template content classes which render content for object detail pages
 """
 register_detail_page_content_classes = PluginSignal(
     providing_args=[]
 )
+
+
+"""
+This signal collects nav menu link classes
+"""
+register_nav_menu_link_classes = PluginSignal(
+    providing_args=[]
+)

+ 3 - 0
netbox/templates/inc/nav_menu.html

@@ -504,6 +504,9 @@
                         </li>
                     </ul>
                 </li>
+                {% if plugin_nav_menu_links %}
+                    {% include 'inc/plugin_nav_menu_items.html' %}
+                {% endif %}
             </ul>
             {% endif %}
             <ul class="nav navbar-nav navbar-right">

+ 29 - 0
netbox/templates/inc/plugin_nav_menu_items.html

@@ -0,0 +1,29 @@
+<li class="dropdown">
+    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
+    <ul class="dropdown-menu">
+        {% 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 %}>
+                {% else %}
+                    <li>
+                {% endif %}
+                        {% if link_item.buttons %}
+                            <div class="buttons pull-right">
+                                {% for button in link_item.buttons %}
+                                    {% if not button.permission or button.permission in perms %}
+                                        <a href="{% url button.link %}" class="btn btn-xs btn-{{ button.color }}" title="{{ button.title }}"><i class="fa {{ button.icon_class }}"></i></a>
+                                    {% endif %}
+                                {% endfor %}
+                            </div>
+                        {% endif %}
+                        <a href="{% url link_item.link %}">{{ link_item.link_text }}</a>
+                    </li>
+            {% endfor %}
+            {% if not forloop.last %}
+                <li class="divider"></li>
+            {% endif %}
+        {% endfor %}
+    </ul>
+</li>

+ 26 - 0
netbox/utilities/choices.py

@@ -78,3 +78,29 @@ def unpack_grouped_choices(choices):
         else:
             unpacked_choices.append((key, value))
     return unpacked_choices
+
+
+#
+# Button color choices
+#
+
+class ButtonColorChoices(ChoiceSet):
+    """
+    Map standard button color choices to Bootstrap color classes
+    """
+
+    BLUE = 'primary'
+    GREY = 'secondary'
+    GREEN = 'success'
+    RED = 'danger'
+    YELLOW = 'warning'
+    BLACK = 'dark'
+
+    CHOICES = (
+        (BLUE, 'Blue'),
+        (GREY, 'Grey'),
+        (GREEN, 'Green'),
+        (RED, 'Red'),
+        (YELLOW, 'Yellow'),
+        (BLACK, 'Black')
+    )