import collections import inspect from django.apps import AppConfig from django.template.loader import get_template from django.utils.module_loading import import_string from extras.registry import registry from utilities.choices import ButtonColorChoices # Initialize plugin registry stores registry['plugin_template_extensions'] = collections.defaultdict(list) registry['plugin_menu_items'] = {} # # 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 = [] # Caching configuration caching_config = {} # Default integration paths. Plugin authors can override these to customize the paths to # integrated components. template_extensions = 'template_content.template_extensions' menu_items = 'navigation.menu_items' def ready(self): # Register template content try: template_extensions = import_string(f"{self.__module__}.{self.template_extensions}") register_template_extensions(template_extensions) except ImportError: pass # Register navigation menu items (if defined) try: menu_items = import_string(f"{self.__module__}.{self.menu_items}") register_menu_items(self.verbose_name, menu_items) except ImportError: pass # # 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 '.'. """ model = None def __init__(self, obj, context): self.obj = obj self.context = context def render(self, template, extra_context=None): """ Convenience method for rendering the provided template name. The detail page object is automatically passed into the template context as `obj` and the original detail page's context is available as `obj_context`. An additional context dictionary may be passed as `extra_context`. """ context = { 'obj': self.obj, 'obj_context': self.context } if isinstance(extra_context, dict): context.update(extra_context) return get_template(template).render(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 passes 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['plugin_template_extensions'][template_extension.model].append(template_extension) # # Navigation menu links # 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. """ def __init__(self, link, link_text, permission=None, buttons=None): self.link = link self.link_text = link_text self.permission = permission if buttons is None: self.buttons = [] else: self.buttons = buttons class PluginMenuButton: """ This class represents a button within a PluginMenuItem. Note that button colors should come from ButtonColorChoices. """ color = ButtonColorChoices.DEFAULT def __init__(self, link, title, icon_class, color=None, permission=None): self.link = link self.title = title self.icon_class = icon_class self.permission = permission if color is not None: self.color = color 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['plugin_menu_items'][section_name] = class_list