mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge pull request #10597 from netbox-community/9072-plugin-view-tabs
#9072: Custom model view tabs for plugins
This commit is contained in:
7
netbox/utilities/templates/tabs/model_view_tabs.html
Normal file
7
netbox/utilities/templates/tabs/model_view_tabs.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% for tab in tabs %}
|
||||
<li role="presentation" class="nav-item">
|
||||
<a href="{{ tab.url }}" class="nav-link{% if tab.is_active %} active{% endif %}">
|
||||
{{ tab.label }} {% badge tab.badge %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
48
netbox/utilities/templatetags/tabs.py
Normal file
48
netbox/utilities/templatetags/tabs.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from extras.registry import registry
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
#
|
||||
# Object detail view tabs
|
||||
#
|
||||
|
||||
@register.inclusion_tag('tabs/model_view_tabs.html', takes_context=True)
|
||||
def model_view_tabs(context, instance):
|
||||
app_label = instance._meta.app_label
|
||||
model_name = instance._meta.model_name
|
||||
user = context['request'].user
|
||||
tabs = []
|
||||
|
||||
# Retrieve registered views for this model
|
||||
try:
|
||||
views = registry['views'][app_label][model_name]
|
||||
except KeyError:
|
||||
# No views have been registered for this model
|
||||
views = []
|
||||
|
||||
# Compile a list of tabs to be displayed in the UI
|
||||
for config in views:
|
||||
view = import_string(config['view']) if type(config['view']) is str else config['view']
|
||||
if tab := getattr(view, 'tab', None):
|
||||
if tab.permission and not user.has_perm(tab.permission):
|
||||
continue
|
||||
|
||||
if attrs := tab.render(instance):
|
||||
viewname = f"{app_label}:{model_name}_{config['name']}"
|
||||
active_tab = context.get('tab')
|
||||
tabs.append({
|
||||
'name': config['name'],
|
||||
'url': reverse(viewname, args=[instance.pk]),
|
||||
'label': attrs['label'],
|
||||
'badge': attrs['badge'],
|
||||
'is_active': active_tab and active_tab == tab,
|
||||
})
|
||||
|
||||
return {
|
||||
'tabs': tabs,
|
||||
}
|
||||
39
netbox/utilities/urls.py
Normal file
39
netbox/utilities/urls.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.urls import path
|
||||
from django.utils.module_loading import import_string
|
||||
from django.views.generic import View
|
||||
|
||||
from extras.registry import registry
|
||||
|
||||
|
||||
def get_model_urls(app_label, model_name):
|
||||
"""
|
||||
Return a list of URL paths for detail views registered to the given model.
|
||||
|
||||
Args:
|
||||
app_label: App/plugin name
|
||||
model_name: Model name
|
||||
"""
|
||||
paths = []
|
||||
|
||||
# Retrieve registered views for this model
|
||||
try:
|
||||
views = registry['views'][app_label][model_name]
|
||||
except KeyError:
|
||||
# No views have been registered for this model
|
||||
views = []
|
||||
|
||||
for config in views:
|
||||
# Import the view class or function
|
||||
if type(config['view']) is str:
|
||||
view_ = import_string(config['view'])
|
||||
else:
|
||||
view_ = config['view']
|
||||
if issubclass(view_, View):
|
||||
view_ = view_.as_view()
|
||||
|
||||
# Create a path to the view
|
||||
paths.append(
|
||||
path(f"{config['path']}/", view_, name=f"{model_name}_{config['name']}", kwargs=config['kwargs'])
|
||||
)
|
||||
|
||||
return paths
|
||||
@@ -3,8 +3,17 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
|
||||
from extras.registry import registry
|
||||
from .permissions import resolve_permission
|
||||
|
||||
__all__ = (
|
||||
'ContentTypePermissionRequiredMixin',
|
||||
'GetReturnURLMixin',
|
||||
'ObjectPermissionRequiredMixin',
|
||||
'ViewTab',
|
||||
'register_model_view',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# View Mixins
|
||||
@@ -122,3 +131,75 @@ class GetReturnURLMixin:
|
||||
|
||||
# If all else fails, return home. Ideally this should never happen.
|
||||
return reverse('home')
|
||||
|
||||
|
||||
class ViewTab:
|
||||
"""
|
||||
ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for
|
||||
a particular object.
|
||||
|
||||
Args:
|
||||
label: Human-friendly text
|
||||
badge: A static value or callable to display alongside the label (optional). If a callable is used, it must accept a single
|
||||
argument representing the object being viewed.
|
||||
permission: The permission required to display the tab (optional).
|
||||
"""
|
||||
def __init__(self, label, badge=None, permission=None):
|
||||
self.label = label
|
||||
self.badge = badge
|
||||
self.permission = permission
|
||||
|
||||
def render(self, instance):
|
||||
"""Return the attributes needed to render a tab in HTML."""
|
||||
badge_value = self._get_badge_value(instance)
|
||||
if self.badge and not badge_value:
|
||||
return None
|
||||
return {
|
||||
'label': self.label,
|
||||
'badge': badge_value,
|
||||
}
|
||||
|
||||
def _get_badge_value(self, instance):
|
||||
if not self.badge:
|
||||
return None
|
||||
if callable(self.badge):
|
||||
return self.badge(instance)
|
||||
return self.badge
|
||||
|
||||
|
||||
def register_model_view(model, name, path=None, kwargs=None):
|
||||
"""
|
||||
This decorator can be used to "attach" a view to any model in NetBox. This is typically used to inject
|
||||
additional tabs within a model's detail view. For example, to add a custom tab to NetBox's dcim.Site model:
|
||||
|
||||
@netbox_model_view(Site, 'myview', path='my-custom-view')
|
||||
class MyView(ObjectView):
|
||||
...
|
||||
|
||||
This will automatically create a URL path for MyView at `/dcim/sites/<id>/my-custom-view/` which can be
|
||||
resolved using the view name `dcim:site_myview'.
|
||||
|
||||
Args:
|
||||
model: The Django model class with which this view will be associated.
|
||||
name: The string used to form the view's name for URL resolution (e.g. via `reverse()`). This will be appended
|
||||
to the name of the base view for the model using an underscore.
|
||||
path: The URL path by which the view can be reached (optional). If not provided, `name` will be used.
|
||||
kwargs: A dictionary of keyword arguments for the view to include when registering its URL path (optional).
|
||||
"""
|
||||
def _wrapper(cls):
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
if model_name not in registry['views'][app_label]:
|
||||
registry['views'][app_label][model_name] = []
|
||||
|
||||
registry['views'][app_label][model_name].append({
|
||||
'name': name,
|
||||
'view': cls,
|
||||
'path': path or name,
|
||||
'kwargs': kwargs or {},
|
||||
})
|
||||
|
||||
return cls
|
||||
|
||||
return _wrapper
|
||||
|
||||
Reference in New Issue
Block a user