diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index af94b9373..f3ce7dcad 100644 --- a/netbox/extras/plugins/__init__.py +++ b/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 diff --git a/netbox/extras/plugins/context_processors.py b/netbox/extras/plugins/context_processors.py new file mode 100644 index 000000000..14b05b874 --- /dev/null +++ b/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 + } diff --git a/netbox/extras/plugins/signals.py b/netbox/extras/plugins/signals.py index 0e5576b67..7ebb549db 100644 --- a/netbox/extras/plugins/signals.py +++ b/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=[] +) diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index e65d42623..d42bb7737 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -504,6 +504,9 @@ + {% if plugin_nav_menu_links %} + {% include 'inc/plugin_nav_menu_items.html' %} + {% endif %} {% endif %}