Merge branch 'feature' into 14731-plugins-catalog

This commit is contained in:
Arthur Hanson 2024-07-08 14:23:39 +07:00
commit 92fac68817
9 changed files with 93 additions and 31 deletions

View File

@ -191,7 +191,7 @@ class MyView(generic.ObjectView):
### Extra Template Content ### Extra Template Content
Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired method(s) to render custom content. Five methods are available: Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, optionally designating one or more particular NetBox models, and defining the desired method(s) to render custom content. Five methods are available:
| Method | View | Description | | Method | View | Description |
|---------------------|-------------|-----------------------------------------------------| |---------------------|-------------|-----------------------------------------------------|
@ -206,7 +206,9 @@ Plugins can inject custom content into certain areas of core NetBox views. This
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: To control where the custom content is injected, plugin authors can specify an iterable of models by overriding the `models` attribute on the subclass. Extensions which do not specify a set of models will be invoked on every view, where supported.
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data includes:
* `object` - The object being viewed (object views only) * `object` - The object being viewed (object views only)
* `model` - The model of the list view (list views only) * `model` - The model of the list view (list views only)
@ -223,7 +225,7 @@ from netbox.plugins import PluginTemplateExtension
from .models import Animal from .models import Animal
class SiteAnimalCount(PluginTemplateExtension): class SiteAnimalCount(PluginTemplateExtension):
model = 'dcim.site' models = ['dcim.site']
def right_page(self): def right_page(self):
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={ return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={

View File

@ -12,7 +12,14 @@
### Enhancements ### Enhancements
* [#7537](https://github.com/netbox-community/netbox/issues/7537) - Add a serial number field for virtual machines * [#7537](https://github.com/netbox-community/netbox/issues/7537) - Add a serial number field for virtual machines
* [#8984](https://github.com/netbox-community/netbox/issues/8984) - Enable filtering of custom script output by log level
* [#15156](https://github.com/netbox-community/netbox/issues/15156) - Add `display_url` field to all REST API serializers
* [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar * [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar
* [#16580](https://github.com/netbox-community/netbox/issues/16580) - Enable individual views to enforce `LOGIN_REQUIRED` selectively (remove `AUTH_EXEMPT_PATHS`)
### Plugins
* [#16726](https://github.com/netbox-community/netbox/issues/16726) - Extend `PluginTemplateExtension` to enable registering multiple models
### Other Changes ### Other Changes

View File

@ -1,3 +1,5 @@
from extras.choices import LogLevelChoices
# Events # Events
EVENT_CREATE = 'create' EVENT_CREATE = 'create'
EVENT_UPDATE = 'update' EVENT_UPDATE = 'update'
@ -135,3 +137,12 @@ DEFAULT_DASHBOARD = [
} }
}, },
] ]
LOG_LEVEL_RANK = {
LogLevelChoices.LOG_DEFAULT: 0,
LogLevelChoices.LOG_DEBUG: 1,
LogLevelChoices.LOG_SUCCESS: 2,
LogLevelChoices.LOG_INFO: 3,
LogLevelChoices.LOG_WARNING: 4,
LogLevelChoices.LOG_FAILURE: 5,
}

View File

@ -14,6 +14,7 @@ from core.forms import ManagedFileForm
from core.models import Job from core.models import Job
from core.tables import JobTable from core.tables import JobTable
from dcim.models import Device, DeviceRole, Platform from dcim.models import Device, DeviceRole, Platform
from extras.choices import LogLevelChoices
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
@ -30,6 +31,7 @@ from utilities.templatetags.builtins.filters import render_markdown
from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .constants import LOG_LEVEL_RANK
from .models import * from .models import *
from .scripts import run_script from .scripts import run_script
from .tables import ReportResultsTable, ScriptResultsTable from .tables import ReportResultsTable, ScriptResultsTable
@ -1119,12 +1121,17 @@ class ScriptResultView(TableMixin, generic.ObjectView):
tests = None tests = None
table = None table = None
index = 0 index = 0
log_threshold = LOG_LEVEL_RANK.get(request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT))
if job.data: if job.data:
if 'log' in job.data: if 'log' in job.data:
if 'tests' in job.data: if 'tests' in job.data:
tests = job.data['tests'] tests = job.data['tests']
for log in job.data['log']: for log in job.data['log']:
log_level = LOG_LEVEL_RANK.get(log.get('status'), LogLevelChoices.LOG_DEFAULT)
if log_level >= log_threshold:
index += 1 index += 1
result = { result = {
'index': index, 'index': index,
@ -1146,6 +1153,8 @@ class ScriptResultView(TableMixin, generic.ObjectView):
for method, test_data in tests.items(): for method, test_data in tests.items():
if 'log' in test_data: if 'log' in test_data:
for time, status, obj, url, message in test_data['log']: for time, status, obj, url, message in test_data['log']:
log_level = LOG_LEVEL_RANK.get(status, LogLevelChoices.LOG_DEFAULT)
if log_level >= log_threshold:
index += 1 index += 1
result = { result = {
'index': index, 'index': index,
@ -1174,6 +1183,8 @@ class ScriptResultView(TableMixin, generic.ObjectView):
'script': job.object, 'script': job.object,
'job': job, 'job': job,
'table': table, 'table': table,
'log_levels': dict(LogLevelChoices),
'log_threshold': request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT)
} }
if job.data and 'log' in job.data: if job.data and 'log' in job.data:

View File

@ -18,8 +18,8 @@ def register_template_extensions(class_list):
""" """
Register a list of PluginTemplateExtension classes Register a list of PluginTemplateExtension classes
""" """
# Validation
for template_extension in class_list: for template_extension in class_list:
# Validation
if not inspect.isclass(template_extension): if not inspect.isclass(template_extension):
raise TypeError( raise TypeError(
_("PluginTemplateExtension class {template_extension} was passed as an instance!").format( _("PluginTemplateExtension class {template_extension} was passed as an instance!").format(
@ -33,7 +33,17 @@ def register_template_extensions(class_list):
) )
) )
registry['plugins']['template_extensions'][template_extension.model].append(template_extension) if template_extension.models:
# Registration for multiple models
models = template_extension.models
elif template_extension.model:
# Registration for a single model
models = [template_extension.model]
else:
# Global registration (no specific models)
models = [None]
for model in models:
registry['plugins']['template_extensions'][model].append(template_extension)
def register_menu(menu): def register_menu(menu):

View File

@ -20,6 +20,7 @@ class PluginTemplateExtension:
* settings - Global NetBox settings * settings - Global NetBox settings
* config - Plugin-specific configuration parameters * config - Plugin-specific configuration parameters
""" """
models = None
model = None model = None
def __init__(self, context): def __init__(self, context):

View File

@ -8,7 +8,7 @@ class GlobalContent(PluginTemplateExtension):
class SiteContent(PluginTemplateExtension): class SiteContent(PluginTemplateExtension):
model = 'dcim.site' models = ['dcim.site']
def left_page(self): def left_page(self):
return "SITE CONTENT - LEFT PAGE" return "SITE CONTENT - LEFT PAGE"

View File

@ -42,8 +42,26 @@
<div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab"> <div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab">
{# Object table controls #} {# Object table controls #}
<div class="row mb-3"> <div class="d-flex align-items-center mb-3">
<div class="col-auto ms-auto d-print-none"> <div>{% trans "Log threshold" %}</div>
<div class="px-2 d-print-none">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ log_levels|get_key:log_threshold }}
</button>
<div class="dropdown-menu">
{% for level, name in log_levels.items %}
<a class="dropdown-item d-flex justify-content-between" href="{% url 'extras:script_result' job_pk=job.pk %}?log_threshold={{ level }}">
{{ name }}
{% if level == log_threshold %}<span class="badge bg-green ms-auto"></span>{% endif %}
</a>
{% endfor %}
</div>
</div>
</div>
<div class="ms-auto d-print-none">
{% if request.user.is_authenticated and job.completed %} {% if request.user.is_authenticated and job.completed %}
<div class="table-configure input-group"> <div class="table-configure input-group">
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config" <button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config"

View File

@ -22,8 +22,10 @@ def _get_registered_content(obj, method, template_context):
'perms': template_context['perms'], 'perms': template_context['perms'],
} }
model_name = obj._meta.label_lower if obj is not None else None template_extensions = list(registry['plugins']['template_extensions'].get(None, []))
template_extensions = registry['plugins']['template_extensions'].get(model_name, []) if obj is not None:
model_name = obj._meta.label_lower
template_extensions.extend(registry['plugins']['template_extensions'].get(model_name, []))
for template_extension in template_extensions: for template_extension in template_extensions:
# If the class has not overridden the specified method, we can skip it (because we know it # If the class has not overridden the specified method, we can skip it (because we know it