mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-09 00:58:16 -06:00
Merge branch 'feature' into 14731-plugins-catalog
This commit is contained in:
commit
92fac68817
@ -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={
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
@ -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,22 +1121,27 @@ 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']:
|
||||||
index += 1
|
log_level = LOG_LEVEL_RANK.get(log.get('status'), LogLevelChoices.LOG_DEFAULT)
|
||||||
result = {
|
if log_level >= log_threshold:
|
||||||
'index': index,
|
index += 1
|
||||||
'time': log.get('time'),
|
result = {
|
||||||
'status': log.get('status'),
|
'index': index,
|
||||||
'message': log.get('message'),
|
'time': log.get('time'),
|
||||||
'object': log.get('obj'),
|
'status': log.get('status'),
|
||||||
'url': log.get('url'),
|
'message': log.get('message'),
|
||||||
}
|
'object': log.get('obj'),
|
||||||
data.append(result)
|
'url': log.get('url'),
|
||||||
|
}
|
||||||
|
data.append(result)
|
||||||
|
|
||||||
table = ScriptResultsTable(data, user=request.user)
|
table = ScriptResultsTable(data, user=request.user)
|
||||||
table.configure(request)
|
table.configure(request)
|
||||||
@ -1146,17 +1153,19 @@ 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']:
|
||||||
index += 1
|
log_level = LOG_LEVEL_RANK.get(status, LogLevelChoices.LOG_DEFAULT)
|
||||||
result = {
|
if log_level >= log_threshold:
|
||||||
'index': index,
|
index += 1
|
||||||
'method': method,
|
result = {
|
||||||
'time': time,
|
'index': index,
|
||||||
'status': status,
|
'method': method,
|
||||||
'object': obj,
|
'time': time,
|
||||||
'url': url,
|
'status': status,
|
||||||
'message': message,
|
'object': obj,
|
||||||
}
|
'url': url,
|
||||||
data.append(result)
|
'message': message,
|
||||||
|
}
|
||||||
|
data.append(result)
|
||||||
|
|
||||||
table = ReportResultsTable(data, user=request.user)
|
table = ReportResultsTable(data, user=request.user)
|
||||||
table.configure(request)
|
table.configure(request)
|
||||||
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user