mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Reorganize plugin resources
This commit is contained in:
parent
07730ccd33
commit
e7f54c5867
@ -1,17 +1,15 @@
|
|||||||
import collections
|
import collections
|
||||||
import inspect
|
|
||||||
from packaging import version
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.template.loader import get_template
|
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
from netbox.navigation import MenuGroup
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from netbox.search import register_search
|
from netbox.search import register_search
|
||||||
from utilities.choices import ButtonColorChoices
|
from .navigation import *
|
||||||
|
from .registration import *
|
||||||
|
from .templates import *
|
||||||
|
|
||||||
# Initialize plugin registry
|
# Initialize plugin registry
|
||||||
registry['plugins'] = {
|
registry['plugins'] = {
|
||||||
@ -142,188 +140,3 @@ class PluginConfig(AppConfig):
|
|||||||
for setting, value in cls.default_settings.items():
|
for setting, value in cls.default_settings.items():
|
||||||
if setting not in user_config:
|
if setting not in user_config:
|
||||||
user_config[setting] = value
|
user_config[setting] = value
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Template content injection
|
|
||||||
#
|
|
||||||
|
|
||||||
class PluginTemplateExtension:
|
|
||||||
"""
|
|
||||||
This class is used to register plugin content to be injected into core NetBox templates. It contains methods
|
|
||||||
that are overridden by plugin authors to return template content.
|
|
||||||
|
|
||||||
The `model` attribute on the class defines the which model detail page this class renders content for. It
|
|
||||||
should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
|
|
||||||
|
|
||||||
* object - The object being viewed
|
|
||||||
* request - The current request
|
|
||||||
* settings - Global NetBox settings
|
|
||||||
* config - Plugin-specific configuration parameters
|
|
||||||
"""
|
|
||||||
model = None
|
|
||||||
|
|
||||||
def __init__(self, context):
|
|
||||||
self.context = context
|
|
||||||
|
|
||||||
def render(self, template_name, extra_context=None):
|
|
||||||
"""
|
|
||||||
Convenience method for rendering the specified Django template using the default context data. An additional
|
|
||||||
context dictionary may be passed as `extra_context`.
|
|
||||||
"""
|
|
||||||
if extra_context is None:
|
|
||||||
extra_context = {}
|
|
||||||
elif not isinstance(extra_context, dict):
|
|
||||||
raise TypeError("extra_context must be a dictionary")
|
|
||||||
|
|
||||||
return get_template(template_name).render({**self.context, **extra_context})
|
|
||||||
|
|
||||||
def left_page(self):
|
|
||||||
"""
|
|
||||||
Content that will be rendered on the left of the detail page view. Content should be returned as an
|
|
||||||
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def right_page(self):
|
|
||||||
"""
|
|
||||||
Content that will be rendered on the right of the detail page view. Content should be returned as an
|
|
||||||
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def full_width_page(self):
|
|
||||||
"""
|
|
||||||
Content that will be rendered within the full width of the detail page view. Content should be returned as an
|
|
||||||
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def buttons(self):
|
|
||||||
"""
|
|
||||||
Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
|
|
||||||
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
|
|
||||||
automatically handled.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
def register_template_extensions(class_list):
|
|
||||||
"""
|
|
||||||
Register a list of PluginTemplateExtension classes
|
|
||||||
"""
|
|
||||||
# Validation
|
|
||||||
for template_extension in class_list:
|
|
||||||
if not inspect.isclass(template_extension):
|
|
||||||
raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
|
|
||||||
if not issubclass(template_extension, PluginTemplateExtension):
|
|
||||||
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
|
|
||||||
if template_extension.model is None:
|
|
||||||
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
|
|
||||||
|
|
||||||
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Navigation menu links
|
|
||||||
#
|
|
||||||
|
|
||||||
class PluginMenu:
|
|
||||||
icon_class = 'mdi mdi-puzzle'
|
|
||||||
|
|
||||||
def __init__(self, label, groups, icon_class=None):
|
|
||||||
self.label = label
|
|
||||||
self.groups = [
|
|
||||||
MenuGroup(label, items) for label, items in groups
|
|
||||||
]
|
|
||||||
if icon_class is not None:
|
|
||||||
self.icon_class = icon_class
|
|
||||||
|
|
||||||
|
|
||||||
class PluginMenuItem:
|
|
||||||
"""
|
|
||||||
This class represents a navigation 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 PluginMenuButton instances.
|
|
||||||
"""
|
|
||||||
permissions = []
|
|
||||||
buttons = []
|
|
||||||
|
|
||||||
def __init__(self, link, link_text, permissions=None, buttons=None):
|
|
||||||
self.link = link
|
|
||||||
self.link_text = link_text
|
|
||||||
if permissions is not None:
|
|
||||||
if type(permissions) not in (list, tuple):
|
|
||||||
raise TypeError("Permissions must be passed as a tuple or list.")
|
|
||||||
self.permissions = permissions
|
|
||||||
if buttons is not None:
|
|
||||||
if type(buttons) not in (list, tuple):
|
|
||||||
raise TypeError("Buttons must be passed as a tuple or list.")
|
|
||||||
self.buttons = buttons
|
|
||||||
|
|
||||||
|
|
||||||
class PluginMenuButton:
|
|
||||||
"""
|
|
||||||
This class represents a button within a PluginMenuItem. Note that button colors should come from
|
|
||||||
ButtonColorChoices.
|
|
||||||
"""
|
|
||||||
color = ButtonColorChoices.DEFAULT
|
|
||||||
permissions = []
|
|
||||||
|
|
||||||
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
|
||||||
self.link = link
|
|
||||||
self.title = title
|
|
||||||
self.icon_class = icon_class
|
|
||||||
if permissions is not None:
|
|
||||||
if type(permissions) not in (list, tuple):
|
|
||||||
raise TypeError("Permissions must be passed as a tuple or list.")
|
|
||||||
self.permissions = permissions
|
|
||||||
if color is not None:
|
|
||||||
if color not in ButtonColorChoices.values():
|
|
||||||
raise ValueError("Button color must be a choice within ButtonColorChoices.")
|
|
||||||
self.color = color
|
|
||||||
|
|
||||||
|
|
||||||
def register_menu(menu):
|
|
||||||
if not isinstance(menu, PluginMenu):
|
|
||||||
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
|
|
||||||
registry['plugins']['menus'].append(menu)
|
|
||||||
|
|
||||||
|
|
||||||
def register_menu_items(section_name, class_list):
|
|
||||||
"""
|
|
||||||
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
|
|
||||||
"""
|
|
||||||
# Validation
|
|
||||||
for menu_link in class_list:
|
|
||||||
if not isinstance(menu_link, PluginMenuItem):
|
|
||||||
raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
|
|
||||||
for button in menu_link.buttons:
|
|
||||||
if not isinstance(button, PluginMenuButton):
|
|
||||||
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
|
|
||||||
|
|
||||||
registry['plugins']['menu_items'][section_name] = class_list
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# GraphQL schemas
|
|
||||||
#
|
|
||||||
|
|
||||||
def register_graphql_schema(graphql_schema):
|
|
||||||
"""
|
|
||||||
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
|
|
||||||
"""
|
|
||||||
registry['plugins']['graphql_schemas'].append(graphql_schema)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# User preferences
|
|
||||||
#
|
|
||||||
|
|
||||||
def register_user_preferences(plugin_name, preferences):
|
|
||||||
"""
|
|
||||||
Register a list of user preferences defined by a plugin.
|
|
||||||
"""
|
|
||||||
registry['plugins']['preferences'][plugin_name] = preferences
|
|
||||||
|
66
netbox/extras/plugins/navigation.py
Normal file
66
netbox/extras/plugins/navigation.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from netbox.navigation import MenuGroup
|
||||||
|
from utilities.choices import ButtonColorChoices
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'PluginMenu',
|
||||||
|
'PluginMenuButton',
|
||||||
|
'PluginMenuItem',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginMenu:
|
||||||
|
icon_class = 'mdi mdi-puzzle'
|
||||||
|
|
||||||
|
def __init__(self, label, groups, icon_class=None):
|
||||||
|
self.label = label
|
||||||
|
self.groups = [
|
||||||
|
MenuGroup(label, items) for label, items in groups
|
||||||
|
]
|
||||||
|
if icon_class is not None:
|
||||||
|
self.icon_class = icon_class
|
||||||
|
|
||||||
|
|
||||||
|
class PluginMenuItem:
|
||||||
|
"""
|
||||||
|
This class represents a navigation 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 PluginMenuButton instances.
|
||||||
|
"""
|
||||||
|
permissions = []
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
def __init__(self, link, link_text, permissions=None, buttons=None):
|
||||||
|
self.link = link
|
||||||
|
self.link_text = link_text
|
||||||
|
if permissions is not None:
|
||||||
|
if type(permissions) not in (list, tuple):
|
||||||
|
raise TypeError("Permissions must be passed as a tuple or list.")
|
||||||
|
self.permissions = permissions
|
||||||
|
if buttons is not None:
|
||||||
|
if type(buttons) not in (list, tuple):
|
||||||
|
raise TypeError("Buttons must be passed as a tuple or list.")
|
||||||
|
self.buttons = buttons
|
||||||
|
|
||||||
|
|
||||||
|
class PluginMenuButton:
|
||||||
|
"""
|
||||||
|
This class represents a button within a PluginMenuItem. Note that button colors should come from
|
||||||
|
ButtonColorChoices.
|
||||||
|
"""
|
||||||
|
color = ButtonColorChoices.DEFAULT
|
||||||
|
permissions = []
|
||||||
|
|
||||||
|
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
||||||
|
self.link = link
|
||||||
|
self.title = title
|
||||||
|
self.icon_class = icon_class
|
||||||
|
if permissions is not None:
|
||||||
|
if type(permissions) not in (list, tuple):
|
||||||
|
raise TypeError("Permissions must be passed as a tuple or list.")
|
||||||
|
self.permissions = permissions
|
||||||
|
if color is not None:
|
||||||
|
if color not in ButtonColorChoices.values():
|
||||||
|
raise ValueError("Button color must be a choice within ButtonColorChoices.")
|
||||||
|
self.color = color
|
64
netbox/extras/plugins/registration.py
Normal file
64
netbox/extras/plugins/registration.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
from netbox.registry import registry
|
||||||
|
from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
|
||||||
|
from .templates import PluginTemplateExtension
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'register_graphql_schema',
|
||||||
|
'register_menu',
|
||||||
|
'register_menu_items',
|
||||||
|
'register_template_extensions',
|
||||||
|
'register_user_preferences',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_template_extensions(class_list):
|
||||||
|
"""
|
||||||
|
Register a list of PluginTemplateExtension classes
|
||||||
|
"""
|
||||||
|
# Validation
|
||||||
|
for template_extension in class_list:
|
||||||
|
if not inspect.isclass(template_extension):
|
||||||
|
raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
|
||||||
|
if not issubclass(template_extension, PluginTemplateExtension):
|
||||||
|
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
|
||||||
|
if template_extension.model is None:
|
||||||
|
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
|
||||||
|
|
||||||
|
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
|
||||||
|
|
||||||
|
|
||||||
|
def register_menu(menu):
|
||||||
|
if not isinstance(menu, PluginMenu):
|
||||||
|
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
|
||||||
|
registry['plugins']['menus'].append(menu)
|
||||||
|
|
||||||
|
|
||||||
|
def register_menu_items(section_name, class_list):
|
||||||
|
"""
|
||||||
|
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
|
||||||
|
"""
|
||||||
|
# Validation
|
||||||
|
for menu_link in class_list:
|
||||||
|
if not isinstance(menu_link, PluginMenuItem):
|
||||||
|
raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
|
||||||
|
for button in menu_link.buttons:
|
||||||
|
if not isinstance(button, PluginMenuButton):
|
||||||
|
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
|
||||||
|
|
||||||
|
registry['plugins']['menu_items'][section_name] = class_list
|
||||||
|
|
||||||
|
|
||||||
|
def register_graphql_schema(graphql_schema):
|
||||||
|
"""
|
||||||
|
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
|
||||||
|
"""
|
||||||
|
registry['plugins']['graphql_schemas'].append(graphql_schema)
|
||||||
|
|
||||||
|
|
||||||
|
def register_user_preferences(plugin_name, preferences):
|
||||||
|
"""
|
||||||
|
Register a list of user preferences defined by a plugin.
|
||||||
|
"""
|
||||||
|
registry['plugins']['preferences'][plugin_name] = preferences
|
65
netbox/extras/plugins/templates.py
Normal file
65
netbox/extras/plugins/templates.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from django.template.loader import get_template
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'PluginTemplateExtension',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginTemplateExtension:
|
||||||
|
"""
|
||||||
|
This class is used to register plugin content to be injected into core NetBox templates. It contains methods
|
||||||
|
that are overridden by plugin authors to return template content.
|
||||||
|
|
||||||
|
The `model` attribute on the class defines the which model detail page this class renders content for. It
|
||||||
|
should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
|
||||||
|
|
||||||
|
* object - The object being viewed
|
||||||
|
* request - The current request
|
||||||
|
* settings - Global NetBox settings
|
||||||
|
* config - Plugin-specific configuration parameters
|
||||||
|
"""
|
||||||
|
model = None
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
def render(self, template_name, extra_context=None):
|
||||||
|
"""
|
||||||
|
Convenience method for rendering the specified Django template using the default context data. An additional
|
||||||
|
context dictionary may be passed as `extra_context`.
|
||||||
|
"""
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
elif not isinstance(extra_context, dict):
|
||||||
|
raise TypeError("extra_context must be a dictionary")
|
||||||
|
|
||||||
|
return get_template(template_name).render({**self.context, **extra_context})
|
||||||
|
|
||||||
|
def left_page(self):
|
||||||
|
"""
|
||||||
|
Content that will be rendered on the left of the detail page view. Content should be returned as an
|
||||||
|
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def right_page(self):
|
||||||
|
"""
|
||||||
|
Content that will be rendered on the right of the detail page view. Content should be returned as an
|
||||||
|
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def full_width_page(self):
|
||||||
|
"""
|
||||||
|
Content that will be rendered within the full width of the detail page view. Content should be returned as an
|
||||||
|
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def buttons(self):
|
||||||
|
"""
|
||||||
|
Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
|
||||||
|
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
|
||||||
|
automatically handled.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
Loading…
Reference in New Issue
Block a user