diff --git a/docs/plugins/development.md b/docs/plugins/development.md index 71ff255cf..341f53624 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -106,6 +106,7 @@ class AnimalSoundsConfig(PluginConfig): * `max_version`: Maximum version of NetBox with which the plugin is compatible * `middleware`: A list of middleware classes to append after NetBox's build-in middleware. * `caching_config`: Plugin-specific cache configuration +* `template_content`: The dotted path to the list of template content classes (default: `template_content.template_contnet`) * `menu_items`: The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) ### Install the Plugin for Development @@ -305,3 +306,40 @@ A `PluginNavMenuButton` has the following attributes: * `color` - Button color (one of the choices provided by `ButtonColorChoices`) * `icon_class` - Button icon CSS class * `permission` - The name of the permission required to display this button (optional) + +## Template Content + +Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateContent`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available: + +* `left_page()` - Inject content on the left side of the page +* `right_page()` - Inject content on the right side of the page +* `full_width_page()` - Inject content across the entire bottom of the page +* `buttons()` - Add buttons to the top of the page + +Each of these methods must return HTML content suitable for inclusion in the object template. Two instance attributes are available for context: + +* `self.obj` - The object being viewed +* `self.context` - The current template context + +Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data. Its use is optional, however. + +Declared subclasses should be gathered into a list or tuple for integration with NetBox. By default, NetBox looks for an iterable named `template_content` within a `template_content.py` file. (This can be overridden by setting `template_content` to a custom value on the plugin's PluginConfig.) An example is below. + +```python +from extras.plugins import PluginTemplateContent + +class AddSiteAnimal(PluginTemplateContent): + model = 'dcim.site' + + def full_width_page(self): + return self.render('netbox_animal_sounds/site.html') + +class AddRackAnimal(PluginTemplateContent): + model = 'dcim.rack' + + def left_page(self): + extra_data = {'foo': 123} + return self.render('netbox_animal_sounds/rack.html', extra_data) + +template_content_classes = [AddSiteAnimal, AddRackAnimal] +``` diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index a0ebec7e9..0a5f8de37 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -6,10 +6,10 @@ from django.template.loader import get_template from django.utils.module_loading import import_string from extras.registry import registry -from .signals import register_detail_page_content_classes # Initialize plugin registry stores +registry['plugin_template_content_classes'] = collections.defaultdict(list) registry['plugin_nav_menu_links'] = {} @@ -48,10 +48,18 @@ class PluginConfig(AppConfig): # Default integration paths. Plugin authors can override these to customize the paths to # integrated components. + template_content_classes = 'template_content.template_content_classes' menu_items = 'navigation.menu_items' def ready(self): + # Register template content + try: + class_list = import_string(f"{self.__module__}.{self.template_content_classes}") + register_template_content_classes(class_list) + except ImportError: + pass + # Register navigation menu items (if defined) try: menu_items = import_string(f"{self.__module__}.{self.menu_items}") @@ -67,7 +75,7 @@ class PluginConfig(AppConfig): class PluginTemplateContent: """ This class is used to register plugin content to be injected into core NetBox templates. - It contains methods that are overriden by plugin authors to return template content. + 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 '.'. @@ -80,8 +88,8 @@ class PluginTemplateContent: def render(self, template, extra_context=None): """ - Convenience menthod for rendering the provided template name. The detail page object is automatically - passed into the template context as `obj` and the origional detail page's context is available as + 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 = { @@ -123,36 +131,20 @@ class PluginTemplateContent: raise NotImplementedError -def register_content_classes(): +def register_template_content_classes(class_list): """ - Helper method that populates the registry with all template content classes that have been registered by plugins + Register a list of PluginTemplateContent classes """ - registry['plugin_template_content_classes'] = collections.defaultdict(list) + # Validation + for template_content_class in class_list: + if not inspect.isclass(template_content_class): + raise TypeError('Plugin content class {} was passes as an instance!'.format(template_content_class)) + if not issubclass(template_content_class, PluginTemplateContent): + raise TypeError('{} is not a subclass of extras.plugins.PluginTemplateContent!'.format(template_content_class)) + if template_content_class.model is None: + raise TypeError('Plugin content class {} does not define a valid model!'.format(template_content_class)) - responses = register_detail_page_content_classes.send('registration_event') - for receiver, response in responses: - if not isinstance(response, list): - response = [response] - for template_class in response: - if not inspect.isclass(template_class): - raise TypeError('Plugin content class {} was passes as an instance!'.format(template_class)) - if not issubclass(template_class, PluginTemplateContent): - raise TypeError('{} is not a subclass of extras.plugins.PluginTemplateContent!'.format(template_class)) - if template_class.model is None: - raise TypeError('Plugin content class {} does not define a valid model!'.format(template_class)) - - registry['plugin_template_content_classes'][template_class.model].append(template_class) - - -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 'plugin_template_content_classes' not in registry: - register_content_classes() - - return registry['plugin_template_content_classes'].get(model, []) + registry['plugin_template_content_classes'][template_content_class.model].append(template_content_class) # diff --git a/netbox/extras/plugins/signals.py b/netbox/extras/plugins/signals.py index fbe30a310..165b5b9cc 100644 --- a/netbox/extras/plugins/signals.py +++ b/netbox/extras/plugins/signals.py @@ -3,7 +3,9 @@ from django.dispatch.dispatcher import NO_RECEIVERS class PluginSignal(django.dispatch.Signal): - + """ + FUTURE USE + """ def _sorted_receivers(self, sender): orig_list = self._live_receivers(sender) sorted_list = sorted( @@ -24,11 +26,3 @@ class PluginSignal(django.dispatch.Signal): response = receiver(signal=self, sender=sender, **kwargs) responses.append((receiver, response)) return responses - - -""" -This signal collects template content classes which render content for object detail pages -""" -register_detail_page_content_classes = PluginSignal( - providing_args=[] -) diff --git a/netbox/extras/templatetags/plugins.py b/netbox/extras/templatetags/plugins.py index 384b08b6a..d2b10669a 100644 --- a/netbox/extras/templatetags/plugins.py +++ b/netbox/extras/templatetags/plugins.py @@ -1,9 +1,7 @@ from django import template as template_ -from django.template.loader import get_template from django.utils.safestring import mark_safe -from extras.plugins import get_content_classes - +from extras.registry import registry register = template_.Library() @@ -15,7 +13,8 @@ def _get_registered_content(obj, method, context): """ html = '' - plugin_template_classes = get_content_classes(obj._meta.label_lower) + model_name = obj._meta.label_lower + plugin_template_classes = registry['plugin_template_content_classes'].get(model_name, []) for plugin_template_class in plugin_template_classes: plugin_template_renderer = plugin_template_class(obj, context) try: