Skip to content

Commit

Permalink
#9072: Implement a mechanism for dynamically registering model detail…
Browse files Browse the repository at this point in the history
… views
  • Loading branch information
jeremystretch committed Oct 6, 2022
1 parent 664d5db commit 0d7851e
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 26 deletions.
1 change: 1 addition & 0 deletions netbox/extras/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ def __delitem__(self, key):
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
}
registry['denormalized_fields'] = collections.defaultdict(list)
registry['views'] = collections.defaultdict(dict)
22 changes: 22 additions & 0 deletions netbox/netbox/models/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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',
Expand Down Expand Up @@ -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}
)
30 changes: 4 additions & 26 deletions netbox/templates/generic/object.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load tabs %}

{% comment %}
Blocks:
Expand Down Expand Up @@ -83,34 +84,11 @@
<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 %}

Expand Down
8 changes: 8 additions & 0 deletions netbox/utilities/templates/tabs/model_view_tabs.html
Original file line number Diff line number Diff line change
@@ -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 changes: 50 additions & 0 deletions netbox/utilities/templatetags/tabs.py
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions netbox/utilities/urls.py
Original file line number Diff line number Diff line change
@@ -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 changes: 38 additions & 0 deletions netbox/utilities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
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
Expand Down Expand Up @@ -122,3 +130,33 @@ def get_return_url(self, request, obj=None):

# 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 {},
})

0 comments on commit 0d7851e

Please sign in to comment.