mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 17:08:41 -06:00
#9072: Implement a mechanism for dynamically registering model detail views
This commit is contained in:
parent
664d5db5eb
commit
0d7851ed9d
@ -29,3 +29,4 @@ registry['model_features'] = {
|
|||||||
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
||||||
}
|
}
|
||||||
registry['denormalized_fields'] = collections.defaultdict(list)
|
registry['denormalized_fields'] = collections.defaultdict(list)
|
||||||
|
registry['views'] = collections.defaultdict(dict)
|
||||||
|
@ -13,6 +13,7 @@ from extras.utils import is_taggable, register_features
|
|||||||
from netbox.signals import post_clean
|
from netbox.signals import post_clean
|
||||||
from utilities.json import CustomFieldJSONEncoder
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
|
from utilities.views import register_model_view
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ChangeLoggingMixin',
|
'ChangeLoggingMixin',
|
||||||
@ -292,3 +293,24 @@ def _register_features(sender, **kwargs):
|
|||||||
feature for feature, cls in FEATURES_MAP if issubclass(sender, cls)
|
feature for feature, cls in FEATURES_MAP if issubclass(sender, cls)
|
||||||
}
|
}
|
||||||
register_features(sender, features)
|
register_features(sender, features)
|
||||||
|
|
||||||
|
# Feature view registration
|
||||||
|
if issubclass(sender, JournalingMixin):
|
||||||
|
register_model_view(
|
||||||
|
sender,
|
||||||
|
'journal',
|
||||||
|
'netbox.views.generic.ObjectJournalView',
|
||||||
|
tab_label='Journal',
|
||||||
|
tab_badge=lambda x: x.journal_entries.count(),
|
||||||
|
tab_permission='extras.view_journalentry',
|
||||||
|
kwargs={'model': sender}
|
||||||
|
)
|
||||||
|
if issubclass(sender, ChangeLoggingMixin):
|
||||||
|
register_model_view(
|
||||||
|
sender,
|
||||||
|
'changelog',
|
||||||
|
'netbox.views.generic.ObjectChangeLogView',
|
||||||
|
tab_label='Changelog',
|
||||||
|
tab_permission='extras.view_objectchange',
|
||||||
|
kwargs={'model': sender}
|
||||||
|
)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load perms %}
|
{% load perms %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load tabs %}
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
Blocks:
|
Blocks:
|
||||||
@ -83,34 +84,11 @@ Context:
|
|||||||
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# Include any additional tabs #}
|
{# Include any extra tabs passed by the view #}
|
||||||
{% block extra_tabs %}{% endblock %}
|
{% block extra_tabs %}{% endblock %}
|
||||||
|
|
||||||
{# Object journal #}
|
{# Include tabs for registered model views #}
|
||||||
{% if perms.extras.view_journalentry %}
|
{% model_view_tabs object %}
|
||||||
{% with journal_viewname=object|viewname:'journal' %}
|
|
||||||
{% url journal_viewname pk=object.pk as journal_url %}
|
|
||||||
{% if journal_url %}
|
|
||||||
<li role="presentation" class="nav-item">
|
|
||||||
<a href="{{ journal_url }}" class="nav-link{% if active_tab == 'journal'%} active{% endif %}">
|
|
||||||
Journal {% badge object.journal_entries.count %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Object changelog #}
|
|
||||||
{% if perms.extras.view_objectchange %}
|
|
||||||
{% with changelog_viewname=object|viewname:'changelog' %}
|
|
||||||
{% url changelog_viewname pk=object.pk as changelog_url %}
|
|
||||||
{% if changelog_url %}
|
|
||||||
<li role="presentation" class="nav-item">
|
|
||||||
<a href="{{ changelog_url }}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock tabs %}
|
{% endblock tabs %}
|
||||||
|
|
||||||
|
8
netbox/utilities/templates/tabs/model_view_tabs.html
Normal file
8
netbox/utilities/templates/tabs/model_view_tabs.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% 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 }}
|
||||||
|
{% if tab.badge_value %}{% badge tab.badge_value %}{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
50
netbox/utilities/templatetags/tabs.py
Normal file
50
netbox/utilities/templatetags/tabs.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from django import template
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
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 view in views:
|
||||||
|
if view['tab_label'] and (not view['tab_permission'] or user.has_perm(view['tab_permission'])):
|
||||||
|
|
||||||
|
# Determine the value of the tab's badge (if any)
|
||||||
|
if view['tab_badge'] and callable(view['tab_badge']):
|
||||||
|
badge_value = view['tab_badge'](instance)
|
||||||
|
elif view['tab_badge']:
|
||||||
|
badge_value = view['tab_badge']
|
||||||
|
else:
|
||||||
|
badge_value = None
|
||||||
|
|
||||||
|
tabs.append({
|
||||||
|
'name': view['name'],
|
||||||
|
'url': reverse(f"{app_label}:{model_name}_{view['name']}", args=[instance.pk]),
|
||||||
|
'label': view['tab_label'],
|
||||||
|
'badge_value': badge_value,
|
||||||
|
'is_active': context.get('active_tab') == view['name'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'tabs': tabs,
|
||||||
|
}
|
35
netbox/utilities/urls.py
Normal file
35
netbox/utilities/urls.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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 view in views:
|
||||||
|
# Import the view class or function
|
||||||
|
callable = import_string(view['path'])
|
||||||
|
if issubclass(callable, View):
|
||||||
|
callable = callable.as_view()
|
||||||
|
# Create a path to the view
|
||||||
|
paths.append(
|
||||||
|
path(f"{view['name']}/", callable, name=f"{model_name}_{view['name']}", kwargs=view['kwargs'])
|
||||||
|
)
|
||||||
|
|
||||||
|
return paths
|
@ -3,8 +3,16 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
|
|
||||||
|
from extras.registry import registry
|
||||||
from .permissions import resolve_permission
|
from .permissions import resolve_permission
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ContentTypePermissionRequiredMixin',
|
||||||
|
'GetReturnURLMixin',
|
||||||
|
'ObjectPermissionRequiredMixin',
|
||||||
|
'register_model_view',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# View Mixins
|
# View Mixins
|
||||||
@ -122,3 +130,33 @@ class GetReturnURLMixin:
|
|||||||
|
|
||||||
# If all else fails, return home. Ideally this should never happen.
|
# If all else fails, return home. Ideally this should never happen.
|
||||||
return reverse('home')
|
return reverse('home')
|
||||||
|
|
||||||
|
|
||||||
|
def register_model_view(model, name, view_path, tab_label=None, tab_badge=None, tab_permission=None, kwargs=None):
|
||||||
|
"""
|
||||||
|
Register a subview for a core model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: The Django model class with which this view will be associated
|
||||||
|
name: The name to register when creating a URL path
|
||||||
|
view_path: A dotted path to the view class or function (e.g. 'myplugin.views.FooView')
|
||||||
|
tab_label: The label to display for the view's tab under the model view (optional)
|
||||||
|
tab_badge: A static value or callable to display a badge within the view's tab (optional). If a callable is
|
||||||
|
specified, it must accept the current object as its single positional argument.
|
||||||
|
tab_permission: The name of the permission required to display the tab (optional)
|
||||||
|
kwargs: A dictionary of keyword arguments to send to the view (optional)
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
'path': view_path,
|
||||||
|
'tab_label': tab_label,
|
||||||
|
'tab_badge': tab_badge,
|
||||||
|
'tab_permission': tab_permission,
|
||||||
|
'kwargs': kwargs or {},
|
||||||
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user