mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
added support for plugin nav bar links
This commit is contained in:
parent
457354c244
commit
981c982237
@ -1,13 +1,18 @@
|
|||||||
import collections
|
import collections
|
||||||
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
|
||||||
from extras.utils import registry
|
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:
|
class PluginTemplateContent:
|
||||||
"""
|
"""
|
||||||
This class is used to register plugin content to be injected into core NetBox templates.
|
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():
|
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)
|
registry.plugin_template_content_classes = collections.defaultdict(list)
|
||||||
|
|
||||||
responses = register_detail_page_content_classes.send('registration_event')
|
responses = register_detail_page_content_classes.send('registration_event')
|
||||||
@ -86,7 +94,86 @@ def register_content_classes():
|
|||||||
|
|
||||||
|
|
||||||
def get_content_classes(model):
|
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'):
|
if not hasattr(registry, 'plugin_template_content_classes'):
|
||||||
register_content_classes()
|
register_content_classes()
|
||||||
|
|
||||||
return registry.plugin_template_content_classes.get(model, [])
|
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
netbox/extras/plugins/context_processors.py
Normal file
12
netbox/extras/plugins/context_processors.py
Normal file
@ -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
|
||||||
|
}
|
@ -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(
|
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=[]
|
||||||
|
)
|
||||||
|
@ -504,6 +504,9 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
{% if plugin_nav_menu_links %}
|
||||||
|
{% include 'inc/plugin_nav_menu_items.html' %}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
29
netbox/templates/inc/plugin_nav_menu_items.html
Normal file
29
netbox/templates/inc/plugin_nav_menu_items.html
Normal file
@ -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>
|
@ -78,3 +78,29 @@ def unpack_grouped_choices(choices):
|
|||||||
else:
|
else:
|
||||||
unpacked_choices.append((key, value))
|
unpacked_choices.append((key, value))
|
||||||
return unpacked_choices
|
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')
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user