Closes #16359: Add navbar() method to PluginTemplateExtension

This commit is contained in:
Jeremy Stretch 2024-06-04 14:24:57 -04:00
parent 87109f5539
commit 50169365a9
7 changed files with 37 additions and 14 deletions

View File

@ -195,12 +195,15 @@ Plugins can inject custom content into certain areas of core NetBox views. This
| Method | View | Description | | Method | View | Description |
|---------------------|-------------|-----------------------------------------------------| |---------------------|-------------|-----------------------------------------------------|
| `navbar()` | All | Inject content inside the top navigation bar |
| `left_page()` | Object view | Inject content on the left side of the page | | `left_page()` | Object view | Inject content on the left side of the page |
| `right_page()` | Object view | Inject content on the right side of the page | | `right_page()` | Object view | Inject content on the right side of the page |
| `full_width_page()` | Object view | Inject content across the entire bottom of the page | | `full_width_page()` | Object view | Inject content across the entire bottom of the page |
| `buttons()` | Object view | Add buttons to the top of the page | | `buttons()` | Object view | Add buttons to the top of the page |
| `list_buttons()` | List view | Add buttons to the top of the page | | `list_buttons()` | List view | Add buttons to the top of the page |
!!! info "The `navbar()` method was introduced in NetBox v4.1."
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however. Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include: When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:

View File

@ -32,21 +32,13 @@ def register_template_extensions(class_list):
template_extension=template_extension template_extension=template_extension
) )
) )
if template_extension.model is None:
raise TypeError(
_("PluginTemplateExtension class {template_extension} does not define a valid model!").format(
template_extension=template_extension
)
)
registry['plugins']['template_extensions'][template_extension.model].append(template_extension) registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
def register_menu(menu): def register_menu(menu):
if not isinstance(menu, PluginMenu): if not isinstance(menu, PluginMenu):
raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format( raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(item=menu))
item=menu_link
))
registry['plugins']['menus'].append(menu) registry['plugins']['menus'].append(menu)

View File

@ -14,7 +14,8 @@ class PluginTemplateExtension:
The `model` attribute on the class defines the which model detail page this class renders content for. It 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: should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
* object - The object being viewed * object - The object being viewed (object views only)
* model - The type of object being viewed (list views only)
* request - The current request * request - The current request
* settings - Global NetBox settings * settings - Global NetBox settings
* config - Plugin-specific configuration parameters * config - Plugin-specific configuration parameters
@ -36,6 +37,13 @@ class PluginTemplateExtension:
return get_template(template_name).render({**self.context, **extra_context}) return get_template(template_name).render({**self.context, **extra_context})
def navbar(self):
"""
Content that will be rendered inside the top navigation menu. 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 left_page(self): def left_page(self):
""" """
Content that will be rendered on the left of the detail page view. Content should be returned as an Content that will be rendered on the left of the detail page view. Content should be returned as an

View File

@ -1,6 +1,12 @@
from netbox.plugins.templates import PluginTemplateExtension from netbox.plugins.templates import PluginTemplateExtension
class GlobalContent(PluginTemplateExtension):
def navbar(self):
return "GLOBAL CONTENT - NAVBAR"
class SiteContent(PluginTemplateExtension): class SiteContent(PluginTemplateExtension):
model = 'dcim.site' model = 'dcim.site'
@ -20,4 +26,4 @@ class SiteContent(PluginTemplateExtension):
return "SITE CONTENT - LIST BUTTONS" return "SITE CONTENT - LIST BUTTONS"
template_extensions = [SiteContent] template_extensions = [GlobalContent, SiteContent]

View File

@ -99,8 +99,9 @@ class PluginTest(TestCase):
""" """
Check that plugin TemplateExtensions are registered. Check that plugin TemplateExtensions are registered.
""" """
from netbox.tests.dummy_plugin.template_content import SiteContent from netbox.tests.dummy_plugin.template_content import GlobalContent, SiteContent
self.assertIn(GlobalContent, registry['plugins']['template_extensions'][None])
self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site']) self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site'])
def test_registered_columns(self): def test_registered_columns(self):

View File

@ -2,6 +2,7 @@
{% extends 'base/base.html' %} {% extends 'base/base.html' %}
{% load helpers %} {% load helpers %}
{% load navigation %} {% load navigation %}
{% load plugins %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
@ -51,8 +52,12 @@ Blocks:
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-nav flex-row align-items-center order-md-last"> <div class="navbar-nav flex-row align-items-center order-md-last">
{# Plugin content #}
{% plugin_navbar %}
{# Dark/light mode toggle #} {# Dark/light mode toggle #}
<div class="d-none d-md-flex"> <div class="d-none d-md-flex ms-2">
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom"> <button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i> <i class="mdi mdi-lightbulb"></i>
</button> </button>

View File

@ -22,7 +22,7 @@ def _get_registered_content(obj, method, template_context):
'perms': template_context['perms'], 'perms': template_context['perms'],
} }
model_name = obj._meta.label_lower model_name = obj._meta.label_lower if obj is not None else None
template_extensions = registry['plugins']['template_extensions'].get(model_name, []) template_extensions = registry['plugins']['template_extensions'].get(model_name, [])
for template_extension in template_extensions: for template_extension in template_extensions:
@ -43,6 +43,14 @@ def _get_registered_content(obj, method, template_context):
return mark_safe(html) return mark_safe(html)
@register.simple_tag(takes_context=True)
def plugin_navbar(context):
"""
Render any navbar content embedded by plugins
"""
return _get_registered_content(None, 'navbar', context)
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def plugin_buttons(context, obj): def plugin_buttons(context, obj):
""" """