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:
@ -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]
|
||||||
|
```
|
||||||
|
@ -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, [])
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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=[]
|
|
||||||
)
|
|
||||||
|
@ -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:
|
||||||
|
Reference in New Issue
Block a user