1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge pull request #4407 from netbox-community/4402-plugins-template-content

Closes #4402: Rework template content registration for plugins
This commit is contained in:
Jeremy Stretch
2020-03-26 09:05:07 -04:00
committed by GitHub
4 changed files with 67 additions and 44 deletions

View File

@ -106,6 +106,7 @@ class AnimalSoundsConfig(PluginConfig):
* `max_version`: Maximum version of NetBox with which the plugin is compatible * `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. * `middleware`: A list of middleware classes to append after NetBox's build-in middleware.
* `caching_config`: Plugin-specific cache configuration * `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`) * `menu_items`: The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`)
### Install the Plugin for Development ### 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`) * `color` - Button color (one of the choices provided by `ButtonColorChoices`)
* `icon_class` - Button icon CSS class * `icon_class` - Button icon CSS class
* `permission` - The name of the permission required to display this button (optional) * `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]
```

View File

@ -6,10 +6,10 @@ from django.template.loader import get_template
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from extras.registry import registry from extras.registry import registry
from .signals import register_detail_page_content_classes
# Initialize plugin registry stores # Initialize plugin registry stores
registry['plugin_template_content_classes'] = collections.defaultdict(list)
registry['plugin_nav_menu_links'] = {} 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 # Default integration paths. Plugin authors can override these to customize the paths to
# integrated components. # integrated components.
template_content_classes = 'template_content.template_content_classes'
menu_items = 'navigation.menu_items' menu_items = 'navigation.menu_items'
def ready(self): 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) # Register navigation menu items (if defined)
try: try:
menu_items = import_string(f"{self.__module__}.{self.menu_items}") menu_items = import_string(f"{self.__module__}.{self.menu_items}")
@ -67,7 +75,7 @@ class PluginConfig(AppConfig):
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.
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 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>'. content for. It should be set as a string in the form '<app_label>.<model_name>'.
@ -80,8 +88,8 @@ class PluginTemplateContent:
def render(self, template, extra_context=None): def render(self, template, extra_context=None):
""" """
Convenience menthod for rendering the provided template name. The detail page object is automatically Convenience method 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 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`. `obj_context`. An additional context dictionary may be passed as `extra_context`.
""" """
context = { context = {
@ -123,36 +131,20 @@ class PluginTemplateContent:
raise NotImplementedError 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') registry['plugin_template_content_classes'][template_content_class.model].append(template_content_class)
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, [])
# #

View File

@ -3,7 +3,9 @@ from django.dispatch.dispatcher import NO_RECEIVERS
class PluginSignal(django.dispatch.Signal): class PluginSignal(django.dispatch.Signal):
"""
FUTURE USE
"""
def _sorted_receivers(self, sender): def _sorted_receivers(self, sender):
orig_list = self._live_receivers(sender) orig_list = self._live_receivers(sender)
sorted_list = sorted( sorted_list = sorted(
@ -24,11 +26,3 @@ class PluginSignal(django.dispatch.Signal):
response = receiver(signal=self, sender=sender, **kwargs) response = receiver(signal=self, sender=sender, **kwargs)
responses.append((receiver, response)) responses.append((receiver, response))
return responses return responses
"""
This signal collects template content classes which render content for object detail pages
"""
register_detail_page_content_classes = PluginSignal(
providing_args=[]
)

View File

@ -1,9 +1,7 @@
from django import template as template_ from django import template as template_
from django.template.loader import get_template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from extras.plugins import get_content_classes from extras.registry import registry
register = template_.Library() register = template_.Library()
@ -15,7 +13,8 @@ def _get_registered_content(obj, method, context):
""" """
html = '' 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: for plugin_template_class in plugin_template_classes:
plugin_template_renderer = plugin_template_class(obj, context) plugin_template_renderer = plugin_template_class(obj, context)
try: try: