-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Move extras.plugins to netbox.plugins & add deprecation warnings * Move plugin template tags from extras to utilities * Move plugins tests from extras to netbox * Add TODO reminders for v4.0
- Loading branch information
1 parent
7efbfab
commit 3f40ee5
Showing
38 changed files
with
579 additions
and
528 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,148 +1,9 @@ | ||
import collections | ||
from importlib import import_module | ||
|
||
from django.apps import AppConfig | ||
from django.core.exceptions import ImproperlyConfigured | ||
from django.utils.module_loading import import_string | ||
from packaging import version | ||
|
||
from netbox.registry import registry | ||
from netbox.search import register_search | ||
from .navigation import * | ||
from .registration import * | ||
from .templates import * | ||
from .utils import * | ||
from netbox.plugins import PluginConfig | ||
|
||
# Initialize plugin registry | ||
registry['plugins'].update({ | ||
'graphql_schemas': [], | ||
'menus': [], | ||
'menu_items': {}, | ||
'preferences': {}, | ||
'template_extensions': collections.defaultdict(list), | ||
}) | ||
|
||
DEFAULT_RESOURCE_PATHS = { | ||
'search_indexes': 'search.indexes', | ||
'graphql_schema': 'graphql.schema', | ||
'menu': 'navigation.menu', | ||
'menu_items': 'navigation.menu_items', | ||
'template_extensions': 'template_content.template_extensions', | ||
'user_preferences': 'preferences.preferences', | ||
} | ||
|
||
|
||
# | ||
# Plugin AppConfig class | ||
# | ||
|
||
class PluginConfig(AppConfig): | ||
""" | ||
Subclass of Django's built-in AppConfig class, to be used for NetBox plugins. | ||
""" | ||
# Plugin metadata | ||
author = '' | ||
author_email = '' | ||
description = '' | ||
version = '' | ||
|
||
# Root URL path under /plugins. If not set, the plugin's label will be used. | ||
base_url = None | ||
|
||
# Minimum/maximum compatible versions of NetBox | ||
min_version = None | ||
max_version = None | ||
|
||
# Default configuration parameters | ||
default_settings = {} | ||
|
||
# Mandatory configuration parameters | ||
required_settings = [] | ||
|
||
# Middleware classes provided by the plugin | ||
middleware = [] | ||
|
||
# Django-rq queues dedicated to the plugin | ||
queues = [] | ||
|
||
# Django apps to append to INSTALLED_APPS when plugin requires them. | ||
django_apps = [] | ||
|
||
# Optional plugin resources | ||
search_indexes = None | ||
graphql_schema = None | ||
menu = None | ||
menu_items = None | ||
template_extensions = None | ||
user_preferences = None | ||
|
||
def _load_resource(self, name): | ||
# Import from the configured path, if defined. | ||
if path := getattr(self, name, None): | ||
return import_string(f"{self.__module__}.{path}") | ||
|
||
# Fall back to the resource's default path. Return None if the module has not been provided. | ||
default_path = f'{self.__module__}.{DEFAULT_RESOURCE_PATHS[name]}' | ||
default_module, resource_name = default_path.rsplit('.', 1) | ||
try: | ||
module = import_module(default_module) | ||
return getattr(module, resource_name, None) | ||
except ModuleNotFoundError: | ||
pass | ||
|
||
def ready(self): | ||
plugin_name = self.name.rsplit('.', 1)[-1] | ||
|
||
# Register search extensions (if defined) | ||
search_indexes = self._load_resource('search_indexes') or [] | ||
for idx in search_indexes: | ||
register_search(idx) | ||
|
||
# Register template content (if defined) | ||
if template_extensions := self._load_resource('template_extensions'): | ||
register_template_extensions(template_extensions) | ||
|
||
# Register navigation menu and/or menu items (if defined) | ||
if menu := self._load_resource('menu'): | ||
register_menu(menu) | ||
if menu_items := self._load_resource('menu_items'): | ||
register_menu_items(self.verbose_name, menu_items) | ||
|
||
# Register GraphQL schema (if defined) | ||
if graphql_schema := self._load_resource('graphql_schema'): | ||
register_graphql_schema(graphql_schema) | ||
|
||
# Register user preferences (if defined) | ||
if user_preferences := self._load_resource('user_preferences'): | ||
register_user_preferences(plugin_name, user_preferences) | ||
|
||
@classmethod | ||
def validate(cls, user_config, netbox_version): | ||
|
||
# Enforce version constraints | ||
current_version = version.parse(netbox_version) | ||
if cls.min_version is not None: | ||
min_version = version.parse(cls.min_version) | ||
if current_version < min_version: | ||
raise ImproperlyConfigured( | ||
f"Plugin {cls.__module__} requires NetBox minimum version {cls.min_version}." | ||
) | ||
if cls.max_version is not None: | ||
max_version = version.parse(cls.max_version) | ||
if current_version > max_version: | ||
raise ImproperlyConfigured( | ||
f"Plugin {cls.__module__} requires NetBox maximum version {cls.max_version}." | ||
) | ||
|
||
# Verify required configuration settings | ||
for setting in cls.required_settings: | ||
if setting not in user_config: | ||
raise ImproperlyConfigured( | ||
f"Plugin {cls.__module__} requires '{setting}' to be present in the PLUGINS_CONFIG section of " | ||
f"configuration.py." | ||
) | ||
|
||
# Apply default configuration values | ||
for setting, value in cls.default_settings.items(): | ||
if setting not in user_config: | ||
user_config[setting] = value | ||
# TODO: Remove in v4.0 | ||
warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,72 +1,7 @@ | ||
from netbox.navigation import MenuGroup | ||
from utilities.choices import ButtonColorChoices | ||
from django.utils.text import slugify | ||
import warnings | ||
|
||
__all__ = ( | ||
'PluginMenu', | ||
'PluginMenuButton', | ||
'PluginMenuItem', | ||
) | ||
from netbox.plugins.navigation import * | ||
|
||
|
||
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 | ||
|
||
@property | ||
def name(self): | ||
return slugify(self.label) | ||
|
||
|
||
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, staff_only=False, permissions=None, buttons=None): | ||
self.link = link | ||
self.link_text = link_text | ||
self.staff_only = staff_only | ||
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 | ||
# TODO: Remove in v4.0 | ||
warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +1,7 @@ | ||
import inspect | ||
import warnings | ||
|
||
from netbox.registry import registry | ||
from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem | ||
from .templates import PluginTemplateExtension | ||
from netbox.plugins.registration import * | ||
|
||
__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 | ||
# TODO: Remove in v4.0 | ||
warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,73 +1,7 @@ | ||
from django.template.loader import get_template | ||
import warnings | ||
|
||
__all__ = ( | ||
'PluginTemplateExtension', | ||
) | ||
from netbox.plugins.templates import * | ||
|
||
|
||
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 list_buttons(self): | ||
""" | ||
Buttons that will be rendered and added to the existing list of buttons on the list 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 | ||
# TODO: Remove in v4.0 | ||
warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning) |
Oops, something went wrong.