1
0
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:
Jeremy Stretch
2022-10-07 15:22:04 -04:00
committed by GitHub
33 changed files with 659 additions and 637 deletions

View 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 %}

View 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
View 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

View File

@@ -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