Просмотр исходного кода

#9072: Implement a mechanism for dynamically registering model detail views

jeremystretch 3 лет назад
Родитель
Сommit
0d7851ed9d

+ 1 - 0
netbox/extras/registry.py

@@ -29,3 +29,4 @@ registry['model_features'] = {
     feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
 }
 registry['denormalized_fields'] = collections.defaultdict(list)
+registry['views'] = collections.defaultdict(dict)

+ 22 - 0
netbox/netbox/models/features.py

@@ -13,6 +13,7 @@ from extras.utils import is_taggable, register_features
 from netbox.signals import post_clean
 from utilities.json import CustomFieldJSONEncoder
 from utilities.utils import serialize_object
+from utilities.views import register_model_view
 
 __all__ = (
     'ChangeLoggingMixin',
@@ -292,3 +293,24 @@ def _register_features(sender, **kwargs):
         feature for feature, cls in FEATURES_MAP if issubclass(sender, cls)
     }
     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 - 26
netbox/templates/generic/object.html

@@ -4,6 +4,7 @@
 {% load helpers %}
 {% load perms %}
 {% load plugins %}
+{% load tabs %}
 
 {% comment %}
 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>
     </li>
 
-    {# Include any additional tabs #}
+    {# Include any extra tabs passed by the view #}
     {% block extra_tabs %}{% endblock %}
 
-    {# Object journal #}
-    {% if perms.extras.view_journalentry %}
-      {% 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 %}
+    {# Include tabs for registered model views #}
+    {% model_view_tabs object %}
   </ul>
 {% endblock tabs %}
 

+ 8 - 0
netbox/utilities/templates/tabs/model_view_tabs.html

@@ -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 - 0
netbox/utilities/templatetags/tabs.py

@@ -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 - 0
netbox/utilities/urls.py

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

+ 38 - 0
netbox/utilities/views.py

@@ -3,8 +3,16 @@ 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',
+    'register_model_view',
+)
+
 
 #
 # View Mixins
@@ -122,3 +130,33 @@ class GetReturnURLMixin:
 
         # If all else fails, return home. Ideally this should never happen.
         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 {},
+    })