From 0d7851ed9de2792ea6d9ed223c315c235290ddd7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 6 Oct 2022 16:20:35 -0400 Subject: [PATCH 1/7] #9072: Implement a mechanism for dynamically registering model detail views --- netbox/extras/registry.py | 1 + netbox/netbox/models/features.py | 22 ++++++++ netbox/templates/generic/object.html | 30 ++--------- .../templates/tabs/model_view_tabs.html | 8 +++ netbox/utilities/templatetags/tabs.py | 50 +++++++++++++++++++ netbox/utilities/urls.py | 35 +++++++++++++ netbox/utilities/views.py | 38 ++++++++++++++ 7 files changed, 158 insertions(+), 26 deletions(-) create mode 100644 netbox/utilities/templates/tabs/model_view_tabs.html create mode 100644 netbox/utilities/templatetags/tabs.py create mode 100644 netbox/utilities/urls.py diff --git a/netbox/extras/registry.py b/netbox/extras/registry.py index e1437c00e8..b748b6f908 100644 --- a/netbox/extras/registry.py +++ b/netbox/extras/registry.py @@ -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) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index ce80cec3e7..0d519a8baf 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -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', @@ -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} + ) diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index ef95ccdc01..2c3c76329d 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -4,6 +4,7 @@ {% load helpers %} {% load perms %} {% load plugins %} +{% load tabs %} {% comment %} Blocks: @@ -83,34 +84,11 @@ {{ object|meta:"verbose_name"|bettertitle }} - {# 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 %} - - {% 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 %} - - {% endif %} - {% endwith %} - {% endif %} + {# Include tabs for registered model views #} + {% model_view_tabs object %} {% endblock tabs %} diff --git a/netbox/utilities/templates/tabs/model_view_tabs.html b/netbox/utilities/templates/tabs/model_view_tabs.html new file mode 100644 index 0000000000..2c6a9046d9 --- /dev/null +++ b/netbox/utilities/templates/tabs/model_view_tabs.html @@ -0,0 +1,8 @@ +{% for tab in tabs %} + +{% endfor %} diff --git a/netbox/utilities/templatetags/tabs.py b/netbox/utilities/templatetags/tabs.py new file mode 100644 index 0000000000..13b4a5f632 --- /dev/null +++ b/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, + } diff --git a/netbox/utilities/urls.py b/netbox/utilities/urls.py new file mode 100644 index 0000000000..3920889b33 --- /dev/null +++ b/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 diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 858e7b4913..a4f5c79a9a 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -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 @@ -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 {}, + }) From a0bae06ff7fb6071dc90acdc6035f0897b94b194 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 6 Oct 2022 16:21:23 -0400 Subject: [PATCH 2/7] Replace static journaling, changelog URL paths with dynamic resolution --- netbox/circuits/urls.py | 17 ++++---- netbox/dcim/urls.py | 79 ++++++++++++++++------------------- netbox/extras/urls.py | 27 +++++------- netbox/ipam/urls.py | 51 +++++++++------------- netbox/tenancy/urls.py | 17 ++++---- netbox/virtualization/urls.py | 17 ++++---- netbox/wireless/urls.py | 13 +++--- 7 files changed, 90 insertions(+), 131 deletions(-) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 5b15b29aca..55ceeddc32 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -1,9 +1,9 @@ -from django.urls import path +from django.urls import include, path from dcim.views import PathTraceView -from netbox.views.generic import ObjectChangeLogView, ObjectJournalView +from utilities.urls import get_model_urls from . import views -from .models import * +from .models import CircuitTermination app_name = 'circuits' urlpatterns = [ @@ -17,8 +17,7 @@ path('providers//', views.ProviderView.as_view(), name='provider'), path('providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), - path('providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), - path('providers//journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}), + path('providers//', include(get_model_urls('circuits', 'provider'))), # Provider networks path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'), @@ -29,8 +28,7 @@ path('provider-networks//', views.ProviderNetworkView.as_view(), name='providernetwork'), path('provider-networks//edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'), path('provider-networks//delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'), - path('provider-networks//changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}), - path('provider-networks//journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}), + path('provider-networks//', include(get_model_urls('circuits', 'providernetwork'))), # Circuit types path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), @@ -41,7 +39,7 @@ path('circuit-types//', views.CircuitTypeView.as_view(), name='circuittype'), path('circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), path('circuit-types//delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'), - path('circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), + path('circuit-types//', include(get_model_urls('circuits', 'circuittype'))), # Circuits path('circuits/', views.CircuitListView.as_view(), name='circuit_list'), @@ -52,9 +50,8 @@ path('circuits//', views.CircuitView.as_view(), name='circuit'), path('circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'), path('circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), - path('circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), - path('circuits//journal/', ObjectJournalView.as_view(), name='circuit_journal', kwargs={'model': Circuit}), path('circuits//terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'), + path('circuits//', include(get_model_urls('circuits', 'circuit'))), # Circuit terminations path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'), diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c11a92a99d..86d28e2246 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -1,8 +1,10 @@ -from django.urls import path +from django.urls import include, path -from netbox.views.generic import ObjectChangeLogView, ObjectJournalView +from utilities.urls import get_model_urls from . import views -from .models import * +from .models import ( + ConsolePort, ConsoleServerPort, FrontPort, Interface, PowerFeed, PowerPort, PowerOutlet, RearPort, +) app_name = 'dcim' urlpatterns = [ @@ -16,7 +18,7 @@ path('regions//', views.RegionView.as_view(), name='region'), path('regions//edit/', views.RegionEditView.as_view(), name='region_edit'), path('regions//delete/', views.RegionDeleteView.as_view(), name='region_delete'), - path('regions//changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), + path('regions//', include(get_model_urls('dcim', 'region'))), # Site groups path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'), @@ -27,7 +29,7 @@ path('site-groups//', views.SiteGroupView.as_view(), name='sitegroup'), path('site-groups//edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'), path('site-groups//delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'), - path('site-groups//changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}), + path('site-groups//', include(get_model_urls('dcim', 'sitegroup'))), # Sites path('sites/', views.SiteListView.as_view(), name='site_list'), @@ -38,8 +40,7 @@ path('sites//', views.SiteView.as_view(), name='site'), path('sites//edit/', views.SiteEditView.as_view(), name='site_edit'), path('sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'), - path('sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), - path('sites//journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}), + path('sites//', include(get_model_urls('dcim', 'site'))), # Locations path('locations/', views.LocationListView.as_view(), name='location_list'), @@ -50,7 +51,7 @@ path('locations//', views.LocationView.as_view(), name='location'), path('locations//edit/', views.LocationEditView.as_view(), name='location_edit'), path('locations//delete/', views.LocationDeleteView.as_view(), name='location_delete'), - path('locations//changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}), + path('locations//', include(get_model_urls('dcim', 'location'))), # Rack roles path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), @@ -61,7 +62,7 @@ path('rack-roles//', views.RackRoleView.as_view(), name='rackrole'), path('rack-roles//edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), path('rack-roles//delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'), - path('rack-roles//changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), + path('rack-roles//', include(get_model_urls('dcim', 'rackrole'))), # Rack reservations path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), @@ -72,8 +73,7 @@ path('rack-reservations//', views.RackReservationView.as_view(), name='rackreservation'), path('rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), path('rack-reservations//delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), - path('rack-reservations//changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), - path('rack-reservations//journal/', ObjectJournalView.as_view(), name='rackreservation_journal', kwargs={'model': RackReservation}), + path('rack-reservations//', include(get_model_urls('dcim', 'rackreservation'))), # Racks path('racks/', views.RackListView.as_view(), name='rack_list'), @@ -85,8 +85,7 @@ path('racks//', views.RackView.as_view(), name='rack'), path('racks//edit/', views.RackEditView.as_view(), name='rack_edit'), path('racks//delete/', views.RackDeleteView.as_view(), name='rack_delete'), - path('racks//changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), - path('racks//journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}), + path('racks//', include(get_model_urls('dcim', 'rack'))), # Manufacturers path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), @@ -97,7 +96,7 @@ path('manufacturers//', views.ManufacturerView.as_view(), name='manufacturer'), path('manufacturers//edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), path('manufacturers//delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'), - path('manufacturers//changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), + path('manufacturers//', include(get_model_urls('dcim', 'manufacturer'))), # Device types path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), @@ -118,8 +117,7 @@ path('device-types//inventory-items/', views.DeviceTypeInventoryItemsView.as_view(), name='devicetype_inventoryitems'), path('device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), - path('device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), - path('device-types//journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}), + path('device-types//', include(get_model_urls('dcim', 'devicetype'))), # Module types path('module-types/', views.ModuleTypeListView.as_view(), name='moduletype_list'), @@ -137,8 +135,7 @@ path('module-types//rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'), path('module-types//edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'), path('module-types//delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'), - path('module-types//changelog/', ObjectChangeLogView.as_view(), name='moduletype_changelog', kwargs={'model': ModuleType}), - path('module-types//journal/', ObjectJournalView.as_view(), name='moduletype_journal', kwargs={'model': ModuleType}), + path('module-types//', include(get_model_urls('dcim', 'moduletype'))), # Console port templates path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'), @@ -229,7 +226,7 @@ path('device-roles//', views.DeviceRoleView.as_view(), name='devicerole'), path('device-roles//edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), path('device-roles//delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'), - path('device-roles//changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), + path('device-roles//', include(get_model_urls('dcim', 'devicerole'))), # Platforms path('platforms/', views.PlatformListView.as_view(), name='platform_list'), @@ -240,7 +237,7 @@ path('platforms//', views.PlatformView.as_view(), name='platform'), path('platforms//edit/', views.PlatformEditView.as_view(), name='platform_edit'), path('platforms//delete/', views.PlatformDeleteView.as_view(), name='platform_delete'), - path('platforms//changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), + path('platforms//', include(get_model_urls('dcim', 'platform'))), # Devices path('devices/', views.DeviceListView.as_view(), name='device_list'), @@ -264,11 +261,10 @@ path('devices//device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'), path('devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), path('devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), - path('devices//changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), - path('devices//journal/', ObjectJournalView.as_view(), name='device_journal', kwargs={'model': Device}), path('devices//status/', views.DeviceStatusView.as_view(), name='device_status'), path('devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), path('devices//config/', views.DeviceConfigView.as_view(), name='device_config'), + path('devices//', include(get_model_urls('dcim', 'device'))), # Modules path('modules/', views.ModuleListView.as_view(), name='module_list'), @@ -279,8 +275,7 @@ path('modules//', views.ModuleView.as_view(), name='module'), path('modules//edit/', views.ModuleEditView.as_view(), name='module_edit'), path('modules//delete/', views.ModuleDeleteView.as_view(), name='module_delete'), - path('modules//changelog/', ObjectChangeLogView.as_view(), name='module_changelog', kwargs={'model': Module}), - path('modules//journal/', ObjectJournalView.as_view(), name='module_journal', kwargs={'model': Module}), + path('modules//', include(get_model_urls('dcim', 'module'))), # Console ports path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), @@ -293,8 +288,8 @@ path('console-ports//', views.ConsolePortView.as_view(), name='consoleport'), path('console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path('console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), - path('console-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}), path('console-ports//trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), + path('console-ports//', include(get_model_urls('dcim', 'consoleport'))), path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), # Console server ports @@ -308,8 +303,8 @@ path('console-server-ports//', views.ConsoleServerPortView.as_view(), name='consoleserverport'), path('console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path('console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), - path('console-server-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}), path('console-server-ports//trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), + path('console-server-ports//', include(get_model_urls('dcim', 'consoleserverport'))), path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), # Power ports @@ -323,8 +318,8 @@ path('power-ports//', views.PowerPortView.as_view(), name='powerport'), path('power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path('power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), - path('power-ports//changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}), path('power-ports//trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), + path('power-ports//', include(get_model_urls('dcim', 'powerport'))), path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), # Power outlets @@ -338,8 +333,8 @@ path('power-outlets//', views.PowerOutletView.as_view(), name='poweroutlet'), path('power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path('power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), - path('power-outlets//changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}), path('power-outlets//trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), + path('power-outlets//', include(get_model_urls('dcim', 'poweroutlet'))), path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), # Interfaces @@ -353,8 +348,8 @@ path('interfaces//', views.InterfaceView.as_view(), name='interface'), path('interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path('interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), - path('interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), path('interfaces//trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), + path('interfaces//', include(get_model_urls('dcim', 'interface'))), path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), # Front ports @@ -368,8 +363,8 @@ path('front-ports//', views.FrontPortView.as_view(), name='frontport'), path('front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path('front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), - path('front-ports//changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}), path('front-ports//trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), + path('front-ports//', include(get_model_urls('dcim', 'frontport'))), # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # Rear ports @@ -383,8 +378,8 @@ path('rear-ports//', views.RearPortView.as_view(), name='rearport'), path('rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path('rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), - path('rear-ports//changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}), path('rear-ports//trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), + path('rear-ports//', include(get_model_urls('dcim', 'rearport'))), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # Module bays @@ -397,7 +392,7 @@ path('module-bays//', views.ModuleBayView.as_view(), name='modulebay'), path('module-bays//edit/', views.ModuleBayEditView.as_view(), name='modulebay_edit'), path('module-bays//delete/', views.ModuleBayDeleteView.as_view(), name='modulebay_delete'), - path('module-bays//changelog/', ObjectChangeLogView.as_view(), name='modulebay_changelog', kwargs={'model': ModuleBay}), + path('module-bays//', include(get_model_urls('dcim', 'modulebay'))), path('devices/module-bays/add/', views.DeviceBulkAddModuleBayView.as_view(), name='device_bulk_add_modulebay'), # Device bays @@ -410,9 +405,9 @@ path('device-bays//', views.DeviceBayView.as_view(), name='devicebay'), path('device-bays//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path('device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), - path('device-bays//changelog/', ObjectChangeLogView.as_view(), name='devicebay_changelog', kwargs={'model': DeviceBay}), path('device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path('device-bays//depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), + path('device-bays//', include(get_model_urls('dcim', 'devicebay'))), path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), # Inventory items @@ -425,10 +420,10 @@ path('inventory-items//', views.InventoryItemView.as_view(), name='inventoryitem'), path('inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), path('inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), - path('inventory-items//changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}), + path('inventory-items//', include(get_model_urls('dcim', 'inventoryitem'))), path('devices/inventory-items/add/', views.DeviceBulkAddInventoryItemView.as_view(), name='device_bulk_add_inventoryitem'), - # Device roles + # Inventory item roles path('inventory-item-roles/', views.InventoryItemRoleListView.as_view(), name='inventoryitemrole_list'), path('inventory-item-roles/add/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_add'), path('inventory-item-roles/import/', views.InventoryItemRoleBulkImportView.as_view(), name='inventoryitemrole_import'), @@ -437,7 +432,7 @@ path('inventory-item-roles//', views.InventoryItemRoleView.as_view(), name='inventoryitemrole'), path('inventory-item-roles//edit/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_edit'), path('inventory-item-roles//delete/', views.InventoryItemRoleDeleteView.as_view(), name='inventoryitemrole_delete'), - path('inventory-item-roles//changelog/', ObjectChangeLogView.as_view(), name='inventoryitemrole_changelog', kwargs={'model': InventoryItemRole}), + path('inventory-item-roles//', include(get_model_urls('dcim', 'inventoryitemrole'))), # Cables path('cables/', views.CableListView.as_view(), name='cable_list'), @@ -448,8 +443,7 @@ path('cables//', views.CableView.as_view(), name='cable'), path('cables//edit/', views.CableEditView.as_view(), name='cable_edit'), path('cables//delete/', views.CableDeleteView.as_view(), name='cable_delete'), - path('cables//changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), - path('cables//journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}), + path('cables//', include(get_model_urls('dcim', 'cable'))), # Console/power/interface connections (read-only) path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), @@ -465,9 +459,8 @@ path('virtual-chassis//', views.VirtualChassisView.as_view(), name='virtualchassis'), path('virtual-chassis//edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), path('virtual-chassis//delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), - path('virtual-chassis//changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), - path('virtual-chassis//journal/', ObjectJournalView.as_view(), name='virtualchassis_journal', kwargs={'model': VirtualChassis}), path('virtual-chassis//add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), + path('virtual-chassis//', include(get_model_urls('dcim', 'virtualchassis'))), path('virtual-chassis-members//delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), # Power panels @@ -479,8 +472,7 @@ path('power-panels//', views.PowerPanelView.as_view(), name='powerpanel'), path('power-panels//edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), path('power-panels//delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), - path('power-panels//changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), - path('power-panels//journal/', ObjectJournalView.as_view(), name='powerpanel_journal', kwargs={'model': PowerPanel}), + path('power-panels//', include(get_model_urls('dcim', 'powerpanel'))), # Power feeds path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), @@ -493,7 +485,6 @@ path('power-feeds//edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), path('power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), path('power-feeds//trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}), - path('power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), - path('power-feeds//journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}), + path('power-feeds//', include(get_model_urls('dcim', 'powerfeed'))), ] diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index ced3bd4b94..18d0314bf7 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,7 +1,7 @@ -from django.urls import path, re_path +from django.urls import include, path, re_path -from extras import models, views -from netbox.views.generic import ObjectChangeLogView +from extras import views +from utilities.urls import get_model_urls app_name = 'extras' @@ -16,8 +16,7 @@ path('custom-fields//', views.CustomFieldView.as_view(), name='customfield'), path('custom-fields//edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'), path('custom-fields//delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'), - path('custom-fields//changelog/', ObjectChangeLogView.as_view(), name='customfield_changelog', - kwargs={'model': models.CustomField}), + path('custom-fields//', include(get_model_urls('extras', 'customfield'))), # Custom links path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'), @@ -28,8 +27,7 @@ path('custom-links//', views.CustomLinkView.as_view(), name='customlink'), path('custom-links//edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'), path('custom-links//delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'), - path('custom-links//changelog/', ObjectChangeLogView.as_view(), name='customlink_changelog', - kwargs={'model': models.CustomLink}), + path('custom-links//', include(get_model_urls('extras', 'customlink'))), # Export templates path('export-templates/', views.ExportTemplateListView.as_view(), name='exporttemplate_list'), @@ -40,8 +38,7 @@ path('export-templates//', views.ExportTemplateView.as_view(), name='exporttemplate'), path('export-templates//edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'), path('export-templates//delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'), - path('export-templates//changelog/', ObjectChangeLogView.as_view(), name='exporttemplate_changelog', - kwargs={'model': models.ExportTemplate}), + path('export-templates//', include(get_model_urls('extras', 'exporttemplate'))), # Webhooks path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'), @@ -52,8 +49,7 @@ path('webhooks//', views.WebhookView.as_view(), name='webhook'), path('webhooks//edit/', views.WebhookEditView.as_view(), name='webhook_edit'), path('webhooks//delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'), - path('webhooks//changelog/', ObjectChangeLogView.as_view(), name='webhook_changelog', - kwargs={'model': models.Webhook}), + path('webhooks//', include(get_model_urls('extras', 'webhook'))), # Tags path('tags/', views.TagListView.as_view(), name='tag_list'), @@ -64,8 +60,7 @@ path('tags//', views.TagView.as_view(), name='tag'), path('tags//edit/', views.TagEditView.as_view(), name='tag_edit'), path('tags//delete/', views.TagDeleteView.as_view(), name='tag_delete'), - path('tags//changelog/', ObjectChangeLogView.as_view(), name='tag_changelog', - kwargs={'model': models.Tag}), + path('tags//', include(get_model_urls('extras', 'tag'))), # Config contexts path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'), @@ -75,8 +70,7 @@ path('config-contexts//', views.ConfigContextView.as_view(), name='configcontext'), path('config-contexts//edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), path('config-contexts//delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), - path('config-contexts//changelog/', ObjectChangeLogView.as_view(), name='configcontext_changelog', - kwargs={'model': models.ConfigContext}), + path('config-contexts//', include(get_model_urls('extras', 'configcontext'))), # Image attachments path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'), @@ -91,8 +85,7 @@ path('journal-entries//', views.JournalEntryView.as_view(), name='journalentry'), path('journal-entries//edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'), path('journal-entries//delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'), - path('journal-entries//changelog/', ObjectChangeLogView.as_view(), name='journalentry_changelog', - kwargs={'model': models.JournalEntry}), + path('journal-entries//', include(get_model_urls('extras', 'journalentry'))), # Change logging path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index d27209fd20..76ea2934b7 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -1,8 +1,7 @@ -from django.urls import path +from django.urls import include, path -from netbox.views.generic import ObjectChangeLogView, ObjectJournalView +from utilities.urls import get_model_urls from . import views -from .models import * app_name = 'ipam' urlpatterns = [ @@ -16,8 +15,7 @@ path('asns//', views.ASNView.as_view(), name='asn'), path('asns//edit/', views.ASNEditView.as_view(), name='asn_edit'), path('asns//delete/', views.ASNDeleteView.as_view(), name='asn_delete'), - path('asns//changelog/', ObjectChangeLogView.as_view(), name='asn_changelog', kwargs={'model': ASN}), - path('asns//journal/', ObjectJournalView.as_view(), name='asn_journal', kwargs={'model': ASN}), + path('asns//', include(get_model_urls('ipam', 'asn'))), # VRFs path('vrfs/', views.VRFListView.as_view(), name='vrf_list'), @@ -28,8 +26,7 @@ path('vrfs//', views.VRFView.as_view(), name='vrf'), path('vrfs//edit/', views.VRFEditView.as_view(), name='vrf_edit'), path('vrfs//delete/', views.VRFDeleteView.as_view(), name='vrf_delete'), - path('vrfs//changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), - path('vrfs//journal/', ObjectJournalView.as_view(), name='vrf_journal', kwargs={'model': VRF}), + path('vrfs//', include(get_model_urls('ipam', 'vrf'))), # Route targets path('route-targets/', views.RouteTargetListView.as_view(), name='routetarget_list'), @@ -40,8 +37,7 @@ path('route-targets//', views.RouteTargetView.as_view(), name='routetarget'), path('route-targets//edit/', views.RouteTargetEditView.as_view(), name='routetarget_edit'), path('route-targets//delete/', views.RouteTargetDeleteView.as_view(), name='routetarget_delete'), - path('route-targets//changelog/', ObjectChangeLogView.as_view(), name='routetarget_changelog', kwargs={'model': RouteTarget}), - path('route-targets//journal/', ObjectJournalView.as_view(), name='routetarget_journal', kwargs={'model': RouteTarget}), + path('route-targets//', include(get_model_urls('ipam', 'routetarget'))), # RIRs path('rirs/', views.RIRListView.as_view(), name='rir_list'), @@ -52,7 +48,7 @@ path('rirs//', views.RIRView.as_view(), name='rir'), path('rirs//edit/', views.RIREditView.as_view(), name='rir_edit'), path('rirs//delete/', views.RIRDeleteView.as_view(), name='rir_delete'), - path('rirs//changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), + path('rirs//', include(get_model_urls('ipam', 'rir'))), # Aggregates path('aggregates/', views.AggregateListView.as_view(), name='aggregate_list'), @@ -64,8 +60,7 @@ path('aggregates//prefixes/', views.AggregatePrefixesView.as_view(), name='aggregate_prefixes'), path('aggregates//edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), path('aggregates//delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), - path('aggregates//changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), - path('aggregates//journal/', ObjectJournalView.as_view(), name='aggregate_journal', kwargs={'model': Aggregate}), + path('aggregates//', include(get_model_urls('ipam', 'aggregate'))), # Roles path('roles/', views.RoleListView.as_view(), name='role_list'), @@ -76,7 +71,7 @@ path('roles//', views.RoleView.as_view(), name='role'), path('roles//edit/', views.RoleEditView.as_view(), name='role_edit'), path('roles//delete/', views.RoleDeleteView.as_view(), name='role_delete'), - path('roles//changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), + path('roles//', include(get_model_urls('ipam', 'role'))), # Prefixes path('prefixes/', views.PrefixListView.as_view(), name='prefix_list'), @@ -87,11 +82,10 @@ path('prefixes//', views.PrefixView.as_view(), name='prefix'), path('prefixes//edit/', views.PrefixEditView.as_view(), name='prefix_edit'), path('prefixes//delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), - path('prefixes//changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), - path('prefixes//journal/', ObjectJournalView.as_view(), name='prefix_journal', kwargs={'model': Prefix}), path('prefixes//prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), path('prefixes//ip-ranges/', views.PrefixIPRangesView.as_view(), name='prefix_ipranges'), path('prefixes//ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), + path('prefixes//', include(get_model_urls('ipam', 'prefix'))), # IP ranges path('ip-ranges/', views.IPRangeListView.as_view(), name='iprange_list'), @@ -102,9 +96,8 @@ path('ip-ranges//', views.IPRangeView.as_view(), name='iprange'), path('ip-ranges//edit/', views.IPRangeEditView.as_view(), name='iprange_edit'), path('ip-ranges//delete/', views.IPRangeDeleteView.as_view(), name='iprange_delete'), - path('ip-ranges//changelog/', ObjectChangeLogView.as_view(), name='iprange_changelog', kwargs={'model': IPRange}), - path('ip-ranges//journal/', ObjectJournalView.as_view(), name='iprange_journal', kwargs={'model': IPRange}), path('ip-ranges//ip-addresses/', views.IPRangeIPAddressesView.as_view(), name='iprange_ipaddresses'), + path('ip-ranges//', include(get_model_urls('ipam', 'iprange'))), # IP addresses path('ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'), @@ -113,12 +106,11 @@ path('ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), path('ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), path('ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), - path('ip-addresses//changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), - path('ip-addresses//journal/', ObjectJournalView.as_view(), name='ipaddress_journal', kwargs={'model': IPAddress}), path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), path('ip-addresses//', views.IPAddressView.as_view(), name='ipaddress'), path('ip-addresses//edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'), path('ip-addresses//delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), + path('ip-addresses//', include(get_model_urls('ipam', 'ipaddress'))), # FHRP groups path('fhrp-groups/', views.FHRPGroupListView.as_view(), name='fhrpgroup_list'), @@ -129,8 +121,7 @@ path('fhrp-groups//', views.FHRPGroupView.as_view(), name='fhrpgroup'), path('fhrp-groups//edit/', views.FHRPGroupEditView.as_view(), name='fhrpgroup_edit'), path('fhrp-groups//delete/', views.FHRPGroupDeleteView.as_view(), name='fhrpgroup_delete'), - path('fhrp-groups//changelog/', ObjectChangeLogView.as_view(), name='fhrpgroup_changelog', kwargs={'model': FHRPGroup}), - path('fhrp-groups//journal/', ObjectJournalView.as_view(), name='fhrpgroup_journal', kwargs={'model': FHRPGroup}), + path('fhrp-groups//', include(get_model_urls('ipam', 'fhrpgroup'))), # FHRP group assignments path('fhrp-group-assignments/add/', views.FHRPGroupAssignmentEditView.as_view(), name='fhrpgroupassignment_add'), @@ -146,7 +137,7 @@ path('vlan-groups//', views.VLANGroupView.as_view(), name='vlangroup'), path('vlan-groups//edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), path('vlan-groups//delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'), - path('vlan-groups//changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), + path('vlan-groups//', include(get_model_urls('ipam', 'vlangroup'))), # VLANs path('vlans/', views.VLANListView.as_view(), name='vlan_list'), @@ -159,8 +150,7 @@ path('vlans//vm-interfaces/', views.VLANVMInterfacesView.as_view(), name='vlan_vminterfaces'), path('vlans//edit/', views.VLANEditView.as_view(), name='vlan_edit'), path('vlans//delete/', views.VLANDeleteView.as_view(), name='vlan_delete'), - path('vlans//changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), - path('vlans//journal/', ObjectJournalView.as_view(), name='vlan_journal', kwargs={'model': VLAN}), + path('vlans//', include(get_model_urls('ipam', 'vlan'))), # Service templates path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'), @@ -171,8 +161,7 @@ path('service-templates//', views.ServiceTemplateView.as_view(), name='servicetemplate'), path('service-templates//edit/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_edit'), path('service-templates//delete/', views.ServiceTemplateDeleteView.as_view(), name='servicetemplate_delete'), - path('service-templates//changelog/', ObjectChangeLogView.as_view(), name='servicetemplate_changelog', kwargs={'model': ServiceTemplate}), - path('service-templates//journal/', ObjectJournalView.as_view(), name='servicetemplate_journal', kwargs={'model': ServiceTemplate}), + path('service-templates//', include(get_model_urls('ipam', 'servicetemplate'))), # Services path('services/', views.ServiceListView.as_view(), name='service_list'), @@ -183,8 +172,7 @@ path('services//', views.ServiceView.as_view(), name='service'), path('services//edit/', views.ServiceEditView.as_view(), name='service_edit'), path('services//delete/', views.ServiceDeleteView.as_view(), name='service_delete'), - path('services//changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), - path('services//journal/', ObjectJournalView.as_view(), name='service_journal', kwargs={'model': Service}), + path('services//', include(get_model_urls('ipam', 'service'))), # L2VPN path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'), @@ -195,9 +183,9 @@ path('l2vpns//', views.L2VPNView.as_view(), name='l2vpn'), path('l2vpns//edit/', views.L2VPNEditView.as_view(), name='l2vpn_edit'), path('l2vpns//delete/', views.L2VPNDeleteView.as_view(), name='l2vpn_delete'), - path('l2vpns//changelog/', ObjectChangeLogView.as_view(), name='l2vpn_changelog', kwargs={'model': L2VPN}), - path('l2vpns//journal/', ObjectJournalView.as_view(), name='l2vpn_journal', kwargs={'model': L2VPN}), + path('l2vpns//', include(get_model_urls('ipam', 'l2vpn'))), + # L2VPN terminations path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'), path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'), path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'), @@ -206,6 +194,5 @@ path('l2vpn-terminations//', views.L2VPNTerminationView.as_view(), name='l2vpntermination'), path('l2vpn-terminations//edit/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_edit'), path('l2vpn-terminations//delete/', views.L2VPNTerminationDeleteView.as_view(), name='l2vpntermination_delete'), - path('l2vpn-terminations//changelog/', ObjectChangeLogView.as_view(), name='l2vpntermination_changelog', kwargs={'model': L2VPNTermination}), - path('l2vpn-terminations//journal/', ObjectJournalView.as_view(), name='l2vpntermination_journal', kwargs={'model': L2VPNTermination}), + path('l2vpn-terminations//', include(get_model_urls('ipam', 'l2vpntermination'))), ] diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index 2141002757..b55e949dd4 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -1,8 +1,7 @@ -from django.urls import path +from django.urls import include, path -from netbox.views.generic import ObjectChangeLogView, ObjectJournalView +from utilities.urls import get_model_urls from . import views -from .models import * app_name = 'tenancy' urlpatterns = [ @@ -16,7 +15,7 @@ path('tenant-groups//', views.TenantGroupView.as_view(), name='tenantgroup'), path('tenant-groups//edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), path('tenant-groups//delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'), - path('tenant-groups//changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}), + path('tenant-groups//', include(get_model_urls('tenancy', 'tenantgroup'))), # Tenants path('tenants/', views.TenantListView.as_view(), name='tenant_list'), @@ -27,8 +26,7 @@ path('tenants//', views.TenantView.as_view(), name='tenant'), path('tenants//edit/', views.TenantEditView.as_view(), name='tenant_edit'), path('tenants//delete/', views.TenantDeleteView.as_view(), name='tenant_delete'), - path('tenants//changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), - path('tenants//journal/', ObjectJournalView.as_view(), name='tenant_journal', kwargs={'model': Tenant}), + path('tenants//', include(get_model_urls('tenancy', 'tenant'))), # Contact groups path('contact-groups/', views.ContactGroupListView.as_view(), name='contactgroup_list'), @@ -39,7 +37,7 @@ path('contact-groups//', views.ContactGroupView.as_view(), name='contactgroup'), path('contact-groups//edit/', views.ContactGroupEditView.as_view(), name='contactgroup_edit'), path('contact-groups//delete/', views.ContactGroupDeleteView.as_view(), name='contactgroup_delete'), - path('contact-groups//changelog/', ObjectChangeLogView.as_view(), name='contactgroup_changelog', kwargs={'model': ContactGroup}), + path('contact-groups//', include(get_model_urls('tenancy', 'contactgroup'))), # Contact roles path('contact-roles/', views.ContactRoleListView.as_view(), name='contactrole_list'), @@ -50,7 +48,7 @@ path('contact-roles//', views.ContactRoleView.as_view(), name='contactrole'), path('contact-roles//edit/', views.ContactRoleEditView.as_view(), name='contactrole_edit'), path('contact-roles//delete/', views.ContactRoleDeleteView.as_view(), name='contactrole_delete'), - path('contact-roles//changelog/', ObjectChangeLogView.as_view(), name='contactrole_changelog', kwargs={'model': ContactRole}), + path('contact-roles//', include(get_model_urls('tenancy', 'contactrole'))), # Contacts path('contacts/', views.ContactListView.as_view(), name='contact_list'), @@ -61,8 +59,7 @@ path('contacts//', views.ContactView.as_view(), name='contact'), path('contacts//edit/', views.ContactEditView.as_view(), name='contact_edit'), path('contacts//delete/', views.ContactDeleteView.as_view(), name='contact_delete'), - path('contacts//changelog/', ObjectChangeLogView.as_view(), name='contact_changelog', kwargs={'model': Contact}), - path('contacts//journal/', ObjectJournalView.as_view(), name='contact_journal', kwargs={'model': Contact}), + path('contacts//', include(get_model_urls('tenancy', 'contact'))), # Contact assignments path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'), diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index e01dbc0596..8968414bc6 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -1,8 +1,7 @@ -from django.urls import path +from django.urls import include, path -from netbox.views.generic import ObjectChangeLogView, ObjectJournalView +from utilities.urls import get_model_urls from . import views -from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface app_name = 'virtualization' urlpatterns = [ @@ -16,7 +15,7 @@ path('cluster-types//', views.ClusterTypeView.as_view(), name='clustertype'), path('cluster-types//edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), path('cluster-types//delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'), - path('cluster-types//changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), + path('cluster-types//', include(get_model_urls('virtualization', 'clustertype'))), # Cluster groups path('cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'), @@ -27,7 +26,7 @@ path('cluster-groups//', views.ClusterGroupView.as_view(), name='clustergroup'), path('cluster-groups//edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), path('cluster-groups//delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'), - path('cluster-groups//changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), + path('cluster-groups//', include(get_model_urls('virtualization', 'clustergroup'))), # Clusters path('clusters/', views.ClusterListView.as_view(), name='cluster_list'), @@ -40,10 +39,9 @@ path('clusters//virtual-machines/', views.ClusterVirtualMachinesView.as_view(), name='cluster_virtualmachines'), path('clusters//edit/', views.ClusterEditView.as_view(), name='cluster_edit'), path('clusters//delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), - path('clusters//changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}), - path('clusters//journal/', ObjectJournalView.as_view(), name='cluster_journal', kwargs={'model': Cluster}), path('clusters//devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), path('clusters//devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), + path('clusters//', include(get_model_urls('virtualization', 'cluster'))), # Virtual machines path('virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), @@ -56,8 +54,7 @@ path('virtual-machines//edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), path('virtual-machines//delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), path('virtual-machines//config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), - path('virtual-machines//changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), - path('virtual-machines//journal/', ObjectJournalView.as_view(), name='virtualmachine_journal', kwargs={'model': VirtualMachine}), + path('virtual-machines//', include(get_model_urls('virtualization', 'virtualmachine'))), # VM interfaces path('interfaces/', views.VMInterfaceListView.as_view(), name='vminterface_list'), @@ -69,7 +66,7 @@ path('interfaces//', views.VMInterfaceView.as_view(), name='vminterface'), path('interfaces//edit/', views.VMInterfaceEditView.as_view(), name='vminterface_edit'), path('interfaces//delete/', views.VMInterfaceDeleteView.as_view(), name='vminterface_delete'), - path('interfaces//changelog/', ObjectChangeLogView.as_view(), name='vminterface_changelog', kwargs={'model': VMInterface}), + path('interfaces//', include(get_model_urls('virtualization', 'vminterface'))), path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_vminterface'), ] diff --git a/netbox/wireless/urls.py b/netbox/wireless/urls.py index cef96fd5e3..d6e84b1b83 100644 --- a/netbox/wireless/urls.py +++ b/netbox/wireless/urls.py @@ -1,8 +1,7 @@ -from django.urls import path +from django.urls import include, path -from netbox.views.generic import ObjectChangeLogView, ObjectJournalView +from utilities.urls import get_model_urls from . import views -from .models import * app_name = 'wireless' urlpatterns = ( @@ -16,7 +15,7 @@ path('wireless-lan-groups//', views.WirelessLANGroupView.as_view(), name='wirelesslangroup'), path('wireless-lan-groups//edit/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_edit'), path('wireless-lan-groups//delete/', views.WirelessLANGroupDeleteView.as_view(), name='wirelesslangroup_delete'), - path('wireless-lan-groups//changelog/', ObjectChangeLogView.as_view(), name='wirelesslangroup_changelog', kwargs={'model': WirelessLANGroup}), + path('wireless-lan-groups//', include(get_model_urls('wireless', 'wirelesslangroup'))), # Wireless LANs path('wireless-lans/', views.WirelessLANListView.as_view(), name='wirelesslan_list'), @@ -27,8 +26,7 @@ path('wireless-lans//', views.WirelessLANView.as_view(), name='wirelesslan'), path('wireless-lans//edit/', views.WirelessLANEditView.as_view(), name='wirelesslan_edit'), path('wireless-lans//delete/', views.WirelessLANDeleteView.as_view(), name='wirelesslan_delete'), - path('wireless-lans//changelog/', ObjectChangeLogView.as_view(), name='wirelesslan_changelog', kwargs={'model': WirelessLAN}), - path('wireless-lans//journal/', ObjectJournalView.as_view(), name='wirelesslan_journal', kwargs={'model': WirelessLAN}), + path('wireless-lans//', include(get_model_urls('wireless', 'wirelesslan'))), # Wireless links path('wireless-links/', views.WirelessLinkListView.as_view(), name='wirelesslink_list'), @@ -39,7 +37,6 @@ path('wireless-links//', views.WirelessLinkView.as_view(), name='wirelesslink'), path('wireless-links//edit/', views.WirelessLinkEditView.as_view(), name='wirelesslink_edit'), path('wireless-links//delete/', views.WirelessLinkDeleteView.as_view(), name='wirelesslink_delete'), - path('wireless-links//changelog/', ObjectChangeLogView.as_view(), name='wirelesslink_changelog', kwargs={'model': WirelessLink}), - path('wireless-links//journal/', ObjectJournalView.as_view(), name='wirelesslink_journal', kwargs={'model': WirelessLink}), + path('wireless-links//', include(get_model_urls('wireless', 'wirelesslink'))), ) From 4c999daacd94588ef328d4b663319970cf82131a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 7 Oct 2022 10:54:34 -0400 Subject: [PATCH 3/7] Introduce ViewTab --- netbox/netbox/models/features.py | 7 +----- netbox/netbox/views/generic/feature_views.py | 11 ++++++++++ netbox/utilities/urls.py | 13 ++++++----- netbox/utilities/views.py | 23 +++++++++++--------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 0d519a8baf..5325cbcd1e 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -13,7 +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 +from utilities.views import ViewTab, register_model_view __all__ = ( 'ChangeLoggingMixin', @@ -300,9 +300,6 @@ def _register_features(sender, **kwargs): 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): @@ -310,7 +307,5 @@ def _register_features(sender, **kwargs): sender, 'changelog', 'netbox.views.generic.ObjectChangeLogView', - tab_label='Changelog', - tab_permission='extras.view_objectchange', kwargs={'model': sender} ) diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 85e675a69c..963fad1964 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -1,10 +1,12 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.shortcuts import get_object_or_404, render +from django.utils.translation import gettext as _ from django.views.generic import View from extras import forms, tables from extras.models import * +from utilities.views import ViewTab __all__ = ( 'ObjectChangeLogView', @@ -23,6 +25,10 @@ class ObjectChangeLogView(View): base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. """ base_template = None + tab = ViewTab( + label=_('Changelog'), + permission='extras.view_objectchange' + ) def get(self, request, model, **kwargs): @@ -71,6 +77,11 @@ class ObjectJournalView(View): base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. """ base_template = None + tab = ViewTab( + label=_('Journal'), + badge=lambda obj: obj.journal_entries.count(), + permission='extras.view_journalentry' + ) def get(self, request, model, **kwargs): diff --git a/netbox/utilities/urls.py b/netbox/utilities/urls.py index 3920889b33..2db8bc91ff 100644 --- a/netbox/utilities/urls.py +++ b/netbox/utilities/urls.py @@ -22,14 +22,17 @@ def get_model_urls(app_label, model_name): # No views have been registered for this model views = [] - for view in views: + for config in views: # Import the view class or function - callable = import_string(view['path']) - if issubclass(callable, View): - callable = callable.as_view() + 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"{view['name']}/", callable, name=f"{model_name}_{view['name']}", kwargs=view['kwargs']) + path(f"{config['name']}/", view_, name=f"{model_name}_{config['name']}", kwargs=config['kwargs']) ) return paths diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index a4f5c79a9a..1200112bea 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -10,6 +10,7 @@ 'ContentTypePermissionRequiredMixin', 'GetReturnURLMixin', 'ObjectPermissionRequiredMixin', + 'ViewTab', 'register_model_view', ) @@ -132,18 +133,23 @@ def get_return_url(self, request, obj=None): return reverse('home') -def register_model_view(model, name, view_path, tab_label=None, tab_badge=None, tab_permission=None, kwargs=None): +class ViewTab: + + def __init__(self, label, badge=None, permission=None, always_display=True): + self.label = label + self.badge = badge + self.permission = permission + self.always_display = always_display + + +def register_model_view(model, name, view, 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) + view: A class-based or function view, or the dotted path to it (e.g. 'myplugin.views.FooView') kwargs: A dictionary of keyword arguments to send to the view (optional) """ app_label = model._meta.app_label @@ -154,9 +160,6 @@ def register_model_view(model, name, view_path, tab_label=None, tab_badge=None, registry['views'][app_label][model_name].append({ 'name': name, - 'path': view_path, - 'tab_label': tab_label, - 'tab_badge': tab_badge, - 'tab_permission': tab_permission, + 'view': view, 'kwargs': kwargs or {}, }) From bfe26b46a6a0bb93bfcf8d16fe088e61d3d51295 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 7 Oct 2022 11:36:14 -0400 Subject: [PATCH 4/7] Wrap model detail views with register_model_view() --- netbox/dcim/urls.py | 27 --- netbox/dcim/views.py | 217 +++++++++++++++++- netbox/extras/views.py | 2 +- netbox/ipam/urls.py | 5 - netbox/ipam/views.py | 38 ++- netbox/netbox/models/features.py | 8 +- netbox/templates/dcim/device/base.html | 89 ------- netbox/templates/dcim/devicetype/base.html | 82 ------- netbox/templates/dcim/moduletype/base.html | 58 ----- netbox/templates/ipam/aggregate/base.html | 10 - netbox/templates/ipam/iprange/base.html | 10 - netbox/templates/ipam/prefix/base.html | 18 -- .../virtualization/cluster/base.html | 17 -- .../virtualization/virtualmachine/base.html | 15 -- netbox/utilities/templatetags/tabs.py | 28 ++- netbox/utilities/urls.py | 3 +- netbox/utilities/views.py | 45 ++-- netbox/virtualization/urls.py | 4 - netbox/virtualization/views.py | 27 ++- 19 files changed, 320 insertions(+), 383 deletions(-) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 86d28e2246..b92a0eec95 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -105,16 +105,6 @@ path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), path('device-types//', views.DeviceTypeView.as_view(), name='devicetype'), - path('device-types//console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'), - path('device-types//console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'), - path('device-types//power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'), - path('device-types//power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'), - path('device-types//interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'), - path('device-types//front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'), - path('device-types//rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'), - path('device-types//module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'), - path('device-types//device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'), - path('device-types//inventory-items/', views.DeviceTypeInventoryItemsView.as_view(), name='devicetype_inventoryitems'), path('device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), path('device-types//', include(get_model_urls('dcim', 'devicetype'))), @@ -126,13 +116,6 @@ path('module-types/edit/', views.ModuleTypeBulkEditView.as_view(), name='moduletype_bulk_edit'), path('module-types/delete/', views.ModuleTypeBulkDeleteView.as_view(), name='moduletype_bulk_delete'), path('module-types//', views.ModuleTypeView.as_view(), name='moduletype'), - path('module-types//console-ports/', views.ModuleTypeConsolePortsView.as_view(), name='moduletype_consoleports'), - path('module-types//console-server-ports/', views.ModuleTypeConsoleServerPortsView.as_view(), name='moduletype_consoleserverports'), - path('module-types//power-ports/', views.ModuleTypePowerPortsView.as_view(), name='moduletype_powerports'), - path('module-types//power-outlets/', views.ModuleTypePowerOutletsView.as_view(), name='moduletype_poweroutlets'), - path('module-types//interfaces/', views.ModuleTypeInterfacesView.as_view(), name='moduletype_interfaces'), - path('module-types//front-ports/', views.ModuleTypeFrontPortsView.as_view(), name='moduletype_frontports'), - path('module-types//rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'), path('module-types//edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'), path('module-types//delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'), path('module-types//', include(get_model_urls('dcim', 'moduletype'))), @@ -250,17 +233,7 @@ path('devices//', views.DeviceView.as_view(), name='device'), path('devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), path('devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'), - path('devices//console-ports/', views.DeviceConsolePortsView.as_view(), name='device_consoleports'), - path('devices//console-server-ports/', views.DeviceConsoleServerPortsView.as_view(), name='device_consoleserverports'), - path('devices//power-ports/', views.DevicePowerPortsView.as_view(), name='device_powerports'), - path('devices//power-outlets/', views.DevicePowerOutletsView.as_view(), name='device_poweroutlets'), - path('devices//interfaces/', views.DeviceInterfacesView.as_view(), name='device_interfaces'), - path('devices//front-ports/', views.DeviceFrontPortsView.as_view(), name='device_frontports'), - path('devices//rear-ports/', views.DeviceRearPortsView.as_view(), name='device_rearports'), - path('devices//module-bays/', views.DeviceModuleBaysView.as_view(), name='device_modulebays'), - path('devices//device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'), path('devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), - path('devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), path('devices//status/', views.DeviceStatusView.as_view(), name='device_status'), path('devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), path('devices//config/', views.DeviceConfigView.as_view(), name='device_config'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5930d6b2d3..e299357d15 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -8,6 +8,7 @@ from django.urls import reverse from django.utils.html import escape from django.utils.safestring import mark_safe +from django.utils.translation import gettext as _ from django.views.generic import View from circuits.models import Circuit, CircuitTermination @@ -19,7 +20,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.permissions import get_permission_for_model from utilities.utils import count_related -from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin +from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view from virtualization.models import VirtualMachine from . import filtersets, forms, tables from .choices import DeviceFaceChoices @@ -47,7 +48,7 @@ def get_children(self, request, parent): def get_extra_context(self, request, instance): return { - 'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}", + 'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '')}", } @@ -60,10 +61,11 @@ def get_children(self, request, parent): return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent) def get_extra_context(self, request, instance): - context = super().get_extra_context(request, instance) - context['return_url'] = reverse(self.viewname, kwargs={'pk': instance.pk}) - - return context + model_name = self.child_model._meta.verbose_name_plural + return { + 'active_tab': f"{model_name.replace(' ', '').replace('template', '')}", + 'return_url': reverse(self.viewname, kwargs={'pk': instance.pk}), + } class ModuleTypeComponentsView(DeviceComponentsView): @@ -75,10 +77,11 @@ def get_children(self, request, parent): return self.child_model.objects.restrict(request.user, 'view').filter(module_type=parent) def get_extra_context(self, request, instance): - context = super().get_extra_context(request, instance) - context['return_url'] = reverse(self.viewname, kwargs={'pk': instance.pk}) - - return context + model_name = self.child_model._meta.verbose_name_plural + return { + 'active_tab': f"{model_name.replace(' ', '').replace('template', '')}", + 'return_url': reverse(self.viewname, kwargs={'pk': instance.pk}), + } class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): @@ -857,74 +860,144 @@ def get_extra_context(self, request, instance): } +@register_model_view(DeviceType, 'consoleports', path='console-ports') class DeviceTypeConsolePortsView(DeviceTypeComponentsView): child_model = ConsolePortTemplate table = tables.ConsolePortTemplateTable filterset = filtersets.ConsolePortTemplateFilterSet viewname = 'dcim:devicetype_consoleports' + tab = ViewTab( + label=_('Console Ports'), + badge=lambda obj: obj.consoleporttemplates.count(), + permission='dcim.view_consoleporttemplate', + always_display=False + ) +@register_model_view(DeviceType, 'consoleserverports', path='console-server-ports') class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView): child_model = ConsoleServerPortTemplate table = tables.ConsoleServerPortTemplateTable filterset = filtersets.ConsoleServerPortTemplateFilterSet viewname = 'dcim:devicetype_consoleserverports' + tab = ViewTab( + label=_('Console Server Ports'), + badge=lambda obj: obj.consoleserverporttemplates.count(), + permission='dcim.view_consoleserverporttemplate', + always_display=False + ) +@register_model_view(DeviceType, 'powerports', path='power-ports') class DeviceTypePowerPortsView(DeviceTypeComponentsView): child_model = PowerPortTemplate table = tables.PowerPortTemplateTable filterset = filtersets.PowerPortTemplateFilterSet viewname = 'dcim:devicetype_powerports' + tab = ViewTab( + label=_('Power Ports'), + badge=lambda obj: obj.powerporttemplates.count(), + permission='dcim.view_powerporttemplate', + always_display=False + ) +@register_model_view(DeviceType, 'poweroutlets', path='power-outlets') class DeviceTypePowerOutletsView(DeviceTypeComponentsView): child_model = PowerOutletTemplate table = tables.PowerOutletTemplateTable filterset = filtersets.PowerOutletTemplateFilterSet viewname = 'dcim:devicetype_poweroutlets' + tab = ViewTab( + label=_('Power Outlets'), + badge=lambda obj: obj.poweroutlettemplates.count(), + permission='dcim.view_poweroutlettemplate', + always_display=False + ) +@register_model_view(DeviceType, 'interfaces') class DeviceTypeInterfacesView(DeviceTypeComponentsView): child_model = InterfaceTemplate table = tables.InterfaceTemplateTable filterset = filtersets.InterfaceTemplateFilterSet viewname = 'dcim:devicetype_interfaces' + tab = ViewTab( + label=_('Interfaces'), + badge=lambda obj: obj.interfacetemplates.count(), + permission='dcim.view_interfacetemplate', + always_display=False + ) +@register_model_view(DeviceType, 'frontports', path='front-ports') class DeviceTypeFrontPortsView(DeviceTypeComponentsView): child_model = FrontPortTemplate table = tables.FrontPortTemplateTable filterset = filtersets.FrontPortTemplateFilterSet viewname = 'dcim:devicetype_frontports' + tab = ViewTab( + label=_('Front Ports'), + badge=lambda obj: obj.frontporttemplates.count(), + permission='dcim.view_frontporttemplate', + always_display=False + ) +@register_model_view(DeviceType, 'rearports', path='rear-ports') class DeviceTypeRearPortsView(DeviceTypeComponentsView): child_model = RearPortTemplate table = tables.RearPortTemplateTable filterset = filtersets.RearPortTemplateFilterSet viewname = 'dcim:devicetype_rearports' + tab = ViewTab( + label=_('Rear Ports'), + badge=lambda obj: obj.rearporttemplates.count(), + permission='dcim.view_rearporttemplate', + always_display=False + ) +@register_model_view(DeviceType, 'modulebays', path='module-bays') class DeviceTypeModuleBaysView(DeviceTypeComponentsView): child_model = ModuleBayTemplate table = tables.ModuleBayTemplateTable filterset = filtersets.ModuleBayTemplateFilterSet viewname = 'dcim:devicetype_modulebays' + tab = ViewTab( + label=_('Module Bays'), + badge=lambda obj: obj.modulebaytemplates.count(), + permission='dcim.view_modulebaytemplate', + always_display=False + ) +@register_model_view(DeviceType, 'devicebays', path='device-bays') class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): child_model = DeviceBayTemplate table = tables.DeviceBayTemplateTable filterset = filtersets.DeviceBayTemplateFilterSet viewname = 'dcim:devicetype_devicebays' + tab = ViewTab( + label=_('Device Bays'), + badge=lambda obj: obj.devicebaytemplates.count(), + permission='dcim.view_devicebaytemplate', + always_display=False + ) +@register_model_view(DeviceType, 'inventoryitems', path='inventory-items') class DeviceTypeInventoryItemsView(DeviceTypeComponentsView): child_model = InventoryItemTemplate table = tables.InventoryItemTemplateTable filterset = filtersets.InventoryItemTemplateFilterSet viewname = 'dcim:devicetype_inventoryitems' + tab = ViewTab( + label=_('Inventory Items'), + badge=lambda obj: obj.inventoryitemtemplates.count(), + permission='dcim.view_invenotryitemtemplate', + always_display=False + ) class DeviceTypeEditView(generic.ObjectEditView): @@ -1011,53 +1084,102 @@ def get_extra_context(self, request, instance): } +@register_model_view(ModuleType, 'consoleports', path='console-ports') class ModuleTypeConsolePortsView(ModuleTypeComponentsView): child_model = ConsolePortTemplate table = tables.ConsolePortTemplateTable filterset = filtersets.ConsolePortTemplateFilterSet viewname = 'dcim:moduletype_consoleports' + tab = ViewTab( + label=_('Console Ports'), + badge=lambda obj: obj.consoleporttemplates.count(), + permission='dcim.view_consoleporttemplate', + always_display=False + ) +@register_model_view(ModuleType, 'consoleserverports', path='console-server-ports') class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView): child_model = ConsoleServerPortTemplate table = tables.ConsoleServerPortTemplateTable filterset = filtersets.ConsoleServerPortTemplateFilterSet viewname = 'dcim:moduletype_consoleserverports' + tab = ViewTab( + label=_('Console Server Ports'), + badge=lambda obj: obj.consoleserverporttemplates.count(), + permission='dcim.view_consoleserverporttemplate', + always_display=False + ) +@register_model_view(ModuleType, 'powerports', path='power-ports') class ModuleTypePowerPortsView(ModuleTypeComponentsView): child_model = PowerPortTemplate table = tables.PowerPortTemplateTable filterset = filtersets.PowerPortTemplateFilterSet viewname = 'dcim:moduletype_powerports' + tab = ViewTab( + label=_('Power Ports'), + badge=lambda obj: obj.powerporttemplates.count(), + permission='dcim.view_powerporttemplate', + always_display=False + ) +@register_model_view(ModuleType, 'poweroutlets', path='power-outlets') class ModuleTypePowerOutletsView(ModuleTypeComponentsView): child_model = PowerOutletTemplate table = tables.PowerOutletTemplateTable filterset = filtersets.PowerOutletTemplateFilterSet viewname = 'dcim:moduletype_poweroutlets' + tab = ViewTab( + label=_('Power Outlets'), + badge=lambda obj: obj.poweroutlettemplates.count(), + permission='dcim.view_poweroutlettemplate', + always_display=False + ) +@register_model_view(ModuleType, 'interfaces') class ModuleTypeInterfacesView(ModuleTypeComponentsView): child_model = InterfaceTemplate table = tables.InterfaceTemplateTable filterset = filtersets.InterfaceTemplateFilterSet viewname = 'dcim:moduletype_interfaces' + tab = ViewTab( + label=_('Interfaces'), + badge=lambda obj: obj.interfacetemplates.count(), + permission='dcim.view_interfacetemplate', + always_display=False + ) +@register_model_view(ModuleType, 'frontports', path='front-ports') class ModuleTypeFrontPortsView(ModuleTypeComponentsView): child_model = FrontPortTemplate table = tables.FrontPortTemplateTable filterset = filtersets.FrontPortTemplateFilterSet viewname = 'dcim:moduletype_frontports' + tab = ViewTab( + label=_('Front Ports'), + badge=lambda obj: obj.frontporttemplates.count(), + permission='dcim.view_frontporttemplate', + always_display=False + ) +@register_model_view(ModuleType, 'rearports', path='rear-ports') class ModuleTypeRearPortsView(ModuleTypeComponentsView): child_model = RearPortTemplate table = tables.RearPortTemplateTable filterset = filtersets.RearPortTemplateFilterSet viewname = 'dcim:moduletype_rearports' + tab = ViewTab( + label=_('Rear Ports'), + badge=lambda obj: obj.rearporttemplates.count(), + permission='dcim.view_rearporttemplate', + always_display=False + ) class ModuleTypeEditView(generic.ObjectEditView): @@ -1620,39 +1742,74 @@ def get_extra_context(self, request, instance): } +@register_model_view(Device, 'consoleports', path='console-ports') class DeviceConsolePortsView(DeviceComponentsView): child_model = ConsolePort table = tables.DeviceConsolePortTable filterset = filtersets.ConsolePortFilterSet template_name = 'dcim/device/consoleports.html' + tab = ViewTab( + label=_('Console Ports'), + badge=lambda obj: obj.consoleports.count(), + permission='dcim.view_consoleport', + always_display=False + ) +@register_model_view(Device, 'consoleserverports', path='console-server-ports') class DeviceConsoleServerPortsView(DeviceComponentsView): child_model = ConsoleServerPort table = tables.DeviceConsoleServerPortTable filterset = filtersets.ConsoleServerPortFilterSet template_name = 'dcim/device/consoleserverports.html' + tab = ViewTab( + label=_('Console Server Ports'), + badge=lambda obj: obj.consoleserverports.count(), + permission='dcim.view_consoleserverport', + always_display=False + ) +@register_model_view(Device, 'powerports', path='power-ports') class DevicePowerPortsView(DeviceComponentsView): child_model = PowerPort table = tables.DevicePowerPortTable filterset = filtersets.PowerPortFilterSet template_name = 'dcim/device/powerports.html' + tab = ViewTab( + label=_('Power Ports'), + badge=lambda obj: obj.powerports.count(), + permission='dcim.view_powerport', + always_display=False + ) +@register_model_view(Device, 'poweroutlets', path='power-outlets') class DevicePowerOutletsView(DeviceComponentsView): child_model = PowerOutlet table = tables.DevicePowerOutletTable filterset = filtersets.PowerOutletFilterSet template_name = 'dcim/device/poweroutlets.html' + tab = ViewTab( + label=_('Power Outlets'), + badge=lambda obj: obj.poweroutlets.count(), + permission='dcim.view_poweroutlet', + always_display=False + ) +@register_model_view(Device, 'interfaces') class DeviceInterfacesView(DeviceComponentsView): child_model = Interface table = tables.DeviceInterfaceTable filterset = filtersets.InterfaceFilterSet template_name = 'dcim/device/interfaces.html' + tab = ViewTab( + label=_('Interfaces'), + badge=lambda obj: obj.interfaces.count(), + permission='dcim.view_interface', + always_display=False + ) def get_children(self, request, parent): return parent.vc_interfaces().restrict(request.user, 'view').prefetch_related( @@ -1661,39 +1818,74 @@ def get_children(self, request, parent): ) +@register_model_view(Device, 'frontports', path='front-ports') class DeviceFrontPortsView(DeviceComponentsView): child_model = FrontPort table = tables.DeviceFrontPortTable filterset = filtersets.FrontPortFilterSet template_name = 'dcim/device/frontports.html' + tab = ViewTab( + label=_('Front Ports'), + badge=lambda obj: obj.frontports.count(), + permission='dcim.view_frontport', + always_display=False + ) +@register_model_view(Device, 'rearports', path='rear-ports') class DeviceRearPortsView(DeviceComponentsView): child_model = RearPort table = tables.DeviceRearPortTable filterset = filtersets.RearPortFilterSet template_name = 'dcim/device/rearports.html' + tab = ViewTab( + label=_('Rear Ports'), + badge=lambda obj: obj.rearports.count(), + permission='dcim.view_rearport', + always_display=False + ) +@register_model_view(Device, 'modulebays', path='module-bays') class DeviceModuleBaysView(DeviceComponentsView): child_model = ModuleBay table = tables.DeviceModuleBayTable filterset = filtersets.ModuleBayFilterSet template_name = 'dcim/device/modulebays.html' + tab = ViewTab( + label=_('Module Bays'), + badge=lambda obj: obj.modulebays.count(), + permission='dcim.view_modulebay', + always_display=False + ) +@register_model_view(Device, 'devicebays', path='device-bays') class DeviceDeviceBaysView(DeviceComponentsView): child_model = DeviceBay table = tables.DeviceDeviceBayTable filterset = filtersets.DeviceBayFilterSet template_name = 'dcim/device/devicebays.html' + tab = ViewTab( + label=_('Device Bays'), + badge=lambda obj: obj.devicebays.count(), + permission='dcim.view_devicebay', + always_display=False + ) +@register_model_view(Device, 'inventory') class DeviceInventoryView(DeviceComponentsView): child_model = InventoryItem table = tables.DeviceInventoryItemTable filterset = filtersets.InventoryItemFilterSet template_name = 'dcim/device/inventory.html' + tab = ViewTab( + label=_('Inventory Items'), + badge=lambda obj: obj.inventoryitems.count(), + permission='dcim.view_inventoryitem', + always_display=False + ) class DeviceStatusView(generic.ObjectView): @@ -1736,9 +1928,14 @@ def get_extra_context(self, request, instance): } +@register_model_view(Device, 'configcontext', path='config-context') class DeviceConfigContextView(ObjectConfigContextView): queryset = Device.objects.annotate_config_context_data() base_template = 'dcim/device/base.html' + tab = ViewTab( + label=_('Config Context'), + permission='extras.view_configcontext' + ) class DeviceEditView(generic.ObjectEditView): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index d8a015bb03..f95b3fb64a 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -352,7 +352,7 @@ def get_extra_context(self, request, instance): 'source_contexts': source_contexts, 'format': format, 'base_template': self.base_template, - 'active_tab': 'config-context', + 'active_tab': 'configcontext', } diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 76ea2934b7..c7b60045b0 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -57,7 +57,6 @@ path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), path('aggregates//', views.AggregateView.as_view(), name='aggregate'), - path('aggregates//prefixes/', views.AggregatePrefixesView.as_view(), name='aggregate_prefixes'), path('aggregates//edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), path('aggregates//delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), path('aggregates//', include(get_model_urls('ipam', 'aggregate'))), @@ -82,9 +81,6 @@ path('prefixes//', views.PrefixView.as_view(), name='prefix'), path('prefixes//edit/', views.PrefixEditView.as_view(), name='prefix_edit'), path('prefixes//delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), - path('prefixes//prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), - path('prefixes//ip-ranges/', views.PrefixIPRangesView.as_view(), name='prefix_ipranges'), - path('prefixes//ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), path('prefixes//', include(get_model_urls('ipam', 'prefix'))), # IP ranges @@ -96,7 +92,6 @@ path('ip-ranges//', views.IPRangeView.as_view(), name='iprange'), path('ip-ranges//edit/', views.IPRangeEditView.as_view(), name='iprange_edit'), path('ip-ranges//delete/', views.IPRangeDeleteView.as_view(), name='iprange_delete'), - path('ip-ranges//ip-addresses/', views.IPRangeIPAddressesView.as_view(), name='iprange_ipaddresses'), path('ip-ranges//', include(get_model_urls('ipam', 'iprange'))), # IP addresses diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 04d07e3566..f705664b3c 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -3,6 +3,7 @@ from django.db.models.expressions import RawSQL from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.utils.translation import gettext as _ from circuits.models import Provider, Circuit from circuits.tables import ProviderTable @@ -11,6 +12,7 @@ from dcim.tables import SiteTable from netbox.views import generic from utilities.utils import count_related +from utilities.views import ViewTab, register_model_view from virtualization.filtersets import VMInterfaceFilterSet from virtualization.models import VMInterface, VirtualMachine from . import filtersets, forms, tables @@ -289,12 +291,18 @@ class AggregateView(generic.ObjectView): queryset = Aggregate.objects.all() +@register_model_view(Aggregate, 'prefixes') class AggregatePrefixesView(generic.ObjectChildrenView): queryset = Aggregate.objects.all() child_model = Prefix table = tables.PrefixTable filterset = filtersets.PrefixFilterSet template_name = 'ipam/aggregate/prefixes.html' + tab = ViewTab( + label=_('Prefixes'), + badge=lambda x: x.get_child_prefixes().count(), + permission='ipam.view_prefix' + ) def get_children(self, request, parent): return Prefix.objects.restrict(request.user, 'view').filter( @@ -466,12 +474,18 @@ def get_extra_context(self, request, instance): } +@register_model_view(Prefix, 'prefixes') class PrefixPrefixesView(generic.ObjectChildrenView): queryset = Prefix.objects.all() child_model = Prefix table = tables.PrefixTable filterset = filtersets.PrefixFilterSet template_name = 'ipam/prefix/prefixes.html' + tab = ViewTab( + label=_('Child Prefixes'), + badge=lambda x: x.get_child_prefixes().count(), + permission='ipam.view_prefix' + ) def get_children(self, request, parent): return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related( @@ -495,12 +509,18 @@ def get_extra_context(self, request, instance): } +@register_model_view(Prefix, 'ipranges', path='ip-ranges') class PrefixIPRangesView(generic.ObjectChildrenView): queryset = Prefix.objects.all() child_model = IPRange table = tables.IPRangeTable filterset = filtersets.IPRangeFilterSet template_name = 'ipam/prefix/ip_ranges.html' + tab = ViewTab( + label=_('Child Ranges'), + badge=lambda x: x.get_child_ranges().count(), + permission='ipam.view_iprange' + ) def get_children(self, request, parent): return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related( @@ -510,17 +530,23 @@ def get_children(self, request, parent): def get_extra_context(self, request, instance): return { 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", - 'active_tab': 'ip-ranges', + 'active_tab': 'ipranges', 'first_available_ip': instance.get_first_available_ip(), } +@register_model_view(Prefix, 'ipaddresses', path='ip-addresses') class PrefixIPAddressesView(generic.ObjectChildrenView): queryset = Prefix.objects.all() child_model = IPAddress table = tables.IPAddressTable filterset = filtersets.IPAddressFilterSet template_name = 'ipam/prefix/ip_addresses.html' + tab = ViewTab( + label=_('IP Addresses'), + badge=lambda x: x.get_child_ips().count(), + permission='ipam.view_ipaddress' + ) def get_children(self, request, parent): return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group') @@ -533,7 +559,7 @@ def prep_table_data(self, request, queryset, parent): def get_extra_context(self, request, instance): return { 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", - 'active_tab': 'ip-addresses', + 'active_tab': 'ipaddresses', 'first_available_ip': instance.get_first_available_ip(), } @@ -581,19 +607,25 @@ class IPRangeView(generic.ObjectView): queryset = IPRange.objects.all() +@register_model_view(IPRange, 'ipaddresses', path='ip-addresses') class IPRangeIPAddressesView(generic.ObjectChildrenView): queryset = IPRange.objects.all() child_model = IPAddress table = tables.IPAddressTable filterset = filtersets.IPAddressFilterSet template_name = 'ipam/iprange/ip_addresses.html' + tab = ViewTab( + label=_('IP Addresses'), + badge=lambda x: x.get_child_ips().count(), + permission='ipam.view_ipaddress' + ) def get_children(self, request, parent): return parent.get_child_ips().restrict(request.user, 'view') def get_extra_context(self, request, instance): return { - 'active_tab': 'ip-addresses', + 'active_tab': 'ipaddresses', } diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 5325cbcd1e..f59e72c145 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -13,7 +13,7 @@ from netbox.signals import post_clean from utilities.json import CustomFieldJSONEncoder from utilities.utils import serialize_object -from utilities.views import ViewTab, register_model_view +from utilities.views import register_model_view __all__ = ( 'ChangeLoggingMixin', @@ -299,13 +299,11 @@ def _register_features(sender, **kwargs): register_model_view( sender, 'journal', - 'netbox.views.generic.ObjectJournalView', kwargs={'model': sender} - ) + )('netbox.views.generic.ObjectJournalView') if issubclass(sender, ChangeLoggingMixin): register_model_view( sender, 'changelog', - 'netbox.views.generic.ObjectChangeLogView', kwargs={'model': sender} - ) + )('netbox.views.generic.ObjectChangeLogView') diff --git a/netbox/templates/dcim/device/base.html b/netbox/templates/dcim/device/base.html index ea67154b1e..161e41256d 100644 --- a/netbox/templates/dcim/device/base.html +++ b/netbox/templates/dcim/device/base.html @@ -56,87 +56,6 @@ {% endblock %} {% block extra_tabs %} - {% with tab_name='device-bays' devicebay_count=object.devicebays.count %} - {% if active_tab == tab_name or devicebay_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='module-bays' modulebay_count=object.modulebays.count %} - {% if active_tab == tab_name or modulebay_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='interfaces' interface_count=object.interfaces_count %} - {% if active_tab == tab_name or interface_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='front-ports' frontport_count=object.frontports.count %} - {% if active_tab == tab_name or frontport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='rear-ports' rearport_count=object.rearports.count %} - {% if active_tab == tab_name or rearport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='console-ports' consoleport_count=object.consoleports.count %} - {% if active_tab == tab_name or consoleport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='console-server-ports' consoleserverport_count=object.consoleserverports.count %} - {% if active_tab == tab_name or consoleserverport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='power-ports' powerport_count=object.powerports.count %} - {% if active_tab == tab_name or powerport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='power-outlets' poweroutlet_count=object.poweroutlets.count %} - {% if active_tab == tab_name or poweroutlet_count %} - - {% endif %} - {% endwith %} - - - {% with tab_name='inventory-items' inventoryitem_count=object.inventoryitems.count %} - {% if active_tab == tab_name or inventoryitem_count %} - - {% endif %} - {% endwith %} - {% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %} {# NAPALM-enabled tabs #} {% endif %} - - {% if perms.extras.view_configcontext %} - - {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/devicetype/base.html b/netbox/templates/dcim/devicetype/base.html index 83ee1f41ee..916952dfb9 100644 --- a/netbox/templates/dcim/devicetype/base.html +++ b/netbox/templates/dcim/devicetype/base.html @@ -51,85 +51,3 @@ {% endif %} {% endblock %} - -{% block extra_tabs %} - {% with tab_name='device-bay-templates' devicebay_count=object.devicebaytemplates.count %} - {% if active_tab == tab_name or devicebay_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='module-bay-templates' modulebay_count=object.modulebaytemplates.count %} - {% if active_tab == tab_name or modulebay_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='interface-templates' interface_count=object.interfacetemplates.count %} - {% if active_tab == tab_name or interface_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='front-port-templates' frontport_count=object.frontporttemplates.count %} - {% if active_tab == tab_name or frontport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='rear-port-templates' rearport_count=object.rearporttemplates.count %} - {% if active_tab == tab_name or rearport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='console-port-templates' consoleport_count=object.consoleporttemplates.count %} - {% if active_tab == tab_name or consoleport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='console-server-port-templates' consoleserverport_count=object.consoleserverporttemplates.count %} - {% if active_tab == tab_name or consoleserverport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='power-port-templates' powerport_count=object.powerporttemplates.count %} - {% if active_tab == tab_name or powerport_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='power-outlet-templates' poweroutlet_count=object.poweroutlettemplates.count %} - {% if active_tab == tab_name or poweroutlet_count %} - - {% endif %} - {% endwith %} - - {% with tab_name='inventory-item-templates' inventoryitem_count=object.inventoryitemtemplates.count %} - {% if active_tab == tab_name or inventoryitem_count %} - - {% endif %} - {% endwith %} -{% endblock %} diff --git a/netbox/templates/dcim/moduletype/base.html b/netbox/templates/dcim/moduletype/base.html index f5713efc37..148effec24 100644 --- a/netbox/templates/dcim/moduletype/base.html +++ b/netbox/templates/dcim/moduletype/base.html @@ -42,61 +42,3 @@ {% endif %} {% endblock %} - -{% block extra_tabs %} - {% with interface_count=object.interfacetemplates.count %} - {% if interface_count %} - - {% endif %} - {% endwith %} - - {% with frontport_count=object.frontporttemplates.count %} - {% if frontport_count %} - - {% endif %} - {% endwith %} - - {% with rearport_count=object.rearporttemplates.count %} - {% if rearport_count %} - - {% endif %} - {% endwith %} - - {% with consoleport_count=object.consoleporttemplates.count %} - {% if consoleport_count %} - - {% endif %} - {% endwith %} - - {% with consoleserverport_count=object.consoleserverporttemplates.count %} - {% if consoleserverport_count %} - - {% endif %} - {% endwith %} - - {% with powerport_count=object.powerporttemplates.count %} - {% if powerport_count %} - - {% endif %} - {% endwith %} - - {% with poweroutlet_count=object.poweroutlettemplates.count %} - {% if poweroutlet_count %} - - {% endif %} - {% endwith %} -{% endblock %} diff --git a/netbox/templates/ipam/aggregate/base.html b/netbox/templates/ipam/aggregate/base.html index c69661a65d..968c4a041b 100644 --- a/netbox/templates/ipam/aggregate/base.html +++ b/netbox/templates/ipam/aggregate/base.html @@ -6,13 +6,3 @@ {{ block.super }} {% endblock %} - -{% block extra_tabs %} - {% if perms.ipam.view_prefix %} - - {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/iprange/base.html b/netbox/templates/ipam/iprange/base.html index 30e8582644..e97db85571 100644 --- a/netbox/templates/ipam/iprange/base.html +++ b/netbox/templates/ipam/iprange/base.html @@ -8,13 +8,3 @@ {% endif %} {% endblock %} - -{% block extra_tabs %} - {% if perms.ipam.view_ipaddress %} - - {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/prefix/base.html b/netbox/templates/ipam/prefix/base.html index b543e37ac2..7ac3070140 100644 --- a/netbox/templates/ipam/prefix/base.html +++ b/netbox/templates/ipam/prefix/base.html @@ -8,21 +8,3 @@ {% endif %} {% endblock %} - -{% block extra_tabs %} - - - -{% endblock %} diff --git a/netbox/templates/virtualization/cluster/base.html b/netbox/templates/virtualization/cluster/base.html index 69b55ec6b4..eb9eefe0ef 100644 --- a/netbox/templates/virtualization/cluster/base.html +++ b/netbox/templates/virtualization/cluster/base.html @@ -24,20 +24,3 @@ {% endif %} {% endblock %} - -{% block extra_tabs %} - {% with virtualmachine_count=object.virtual_machines.count %} - - {% endwith %} - {% with device_count=object.devices.count %} - - {% endwith %} -{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine/base.html b/netbox/templates/virtualization/virtualmachine/base.html index 946467e31f..995c16fb00 100644 --- a/netbox/templates/virtualization/virtualmachine/base.html +++ b/netbox/templates/virtualization/virtualmachine/base.html @@ -21,18 +21,3 @@ {% endif %} {% endblock %} - -{% block extra_tabs %} - {% with interface_count=object.interfaces.count %} - {% if interface_count %} - - {% endif %} - {% endwith %} - {% if perms.extras.view_configcontext %} - - {% endif %} -{% endblock %} diff --git a/netbox/utilities/templatetags/tabs.py b/netbox/utilities/templatetags/tabs.py index 13b4a5f632..e0ab49589d 100644 --- a/netbox/utilities/templatetags/tabs.py +++ b/netbox/utilities/templatetags/tabs.py @@ -1,6 +1,6 @@ from django import template -from django.core.exceptions import ImproperlyConfigured from django.urls import reverse +from django.utils.module_loading import import_string from extras.registry import registry @@ -26,23 +26,27 @@ def model_view_tabs(context, instance): 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'])): + 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 # 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'] + if tab.badge and callable(tab.badge): + badge_value = tab.badge(instance) else: - badge_value = None + badge_value = tab.badge + + if not tab.always_display and not badge_value: + continue tabs.append({ - 'name': view['name'], - 'url': reverse(f"{app_label}:{model_name}_{view['name']}", args=[instance.pk]), - 'label': view['tab_label'], + 'name': config['name'], + 'url': reverse(f"{app_label}:{model_name}_{config['name']}", args=[instance.pk]), + 'label': tab.label, 'badge_value': badge_value, - 'is_active': context.get('active_tab') == view['name'], + 'is_active': context.get('active_tab') == config['name'], }) return { diff --git a/netbox/utilities/urls.py b/netbox/utilities/urls.py index 2db8bc91ff..9ba2a65e61 100644 --- a/netbox/utilities/urls.py +++ b/netbox/utilities/urls.py @@ -30,9 +30,10 @@ def get_model_urls(app_label, model_name): view_ = config['view'] if issubclass(view_, View): view_ = view_.as_view() + # Create a path to the view paths.append( - path(f"{config['name']}/", view_, name=f"{model_name}_{config['name']}", kwargs=config['kwargs']) + path(f"{config['path']}/", view_, name=f"{model_name}_{config['name']}", kwargs=config['kwargs']) ) return paths diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1200112bea..5a357111a4 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -142,24 +142,39 @@ def __init__(self, label, badge=None, permission=None, always_display=True): self.always_display = always_display -def register_model_view(model, name, view, kwargs=None): +def register_model_view(model, name, path=None, kwargs=None): """ - Register a subview for a core model. + 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//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 name to register when creating a URL path - view: A class-based or function view, or the dotted path to it (e.g. 'myplugin.views.FooView') - kwargs: A dictionary of keyword arguments to send to the view (optional) + 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) """ - app_label = model._meta.app_label - model_name = model._meta.model_name + 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 {}, + }) - if model_name not in registry['views'][app_label]: - registry['views'][app_label][model_name] = [] + return cls - registry['views'][app_label][model_name].append({ - 'name': name, - 'view': view, - 'kwargs': kwargs or {}, - }) + return _wrapper diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index 8968414bc6..31914bc3b0 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -35,8 +35,6 @@ path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), path('clusters//', views.ClusterView.as_view(), name='cluster'), - path('clusters//devices/', views.ClusterDevicesView.as_view(), name='cluster_devices'), - path('clusters//virtual-machines/', views.ClusterVirtualMachinesView.as_view(), name='cluster_virtualmachines'), path('clusters//edit/', views.ClusterEditView.as_view(), name='cluster_edit'), path('clusters//delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), path('clusters//devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), @@ -50,10 +48,8 @@ path('virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), path('virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), path('virtual-machines//', views.VirtualMachineView.as_view(), name='virtualmachine'), - path('virtual-machines//interfaces/', views.VirtualMachineInterfacesView.as_view(), name='virtualmachine_interfaces'), path('virtual-machines//edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), path('virtual-machines//delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), - path('virtual-machines//config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), path('virtual-machines//', include(get_model_urls('virtualization', 'virtualmachine'))), # VM interfaces diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 611725d62e..3289c0b567 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -3,6 +3,7 @@ from django.db.models import Prefetch from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.utils.translation import gettext as _ from dcim.filtersets import DeviceFilterSet from dcim.models import Device @@ -12,6 +13,7 @@ from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable from netbox.views import generic from utilities.utils import count_related +from utilities.views import ViewTab, register_model_view from . import filtersets, forms, tables from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -161,28 +163,40 @@ class ClusterView(generic.ObjectView): queryset = Cluster.objects.all() +@register_model_view(Cluster, 'virtualmachines', path='virtual-machines') class ClusterVirtualMachinesView(generic.ObjectChildrenView): queryset = Cluster.objects.all() child_model = VirtualMachine table = tables.VirtualMachineTable filterset = filtersets.VirtualMachineFilterSet template_name = 'virtualization/cluster/virtual_machines.html' + tab = ViewTab( + label=_('Virtual Machines'), + badge=lambda obj: obj.virtual_machines.count(), + permission='virtualization.view_virtualmachine' + ) def get_children(self, request, parent): return VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=parent) def get_extra_context(self, request, instance): return { - 'active_tab': 'virtual-machines', + 'active_tab': 'virtualmachines', } +@register_model_view(Cluster, 'devices') class ClusterDevicesView(generic.ObjectChildrenView): queryset = Cluster.objects.all() child_model = Device table = DeviceTable filterset = DeviceFilterSet template_name = 'virtualization/cluster/devices.html' + tab = ViewTab( + label=_('Devices'), + badge=lambda obj: obj.devices.count(), + permission='virtualization.view_virtualmachine' + ) def get_children(self, request, parent): return Device.objects.restrict(request.user, 'view').filter(cluster=parent) @@ -344,12 +358,18 @@ def get_extra_context(self, request, instance): } +@register_model_view(VirtualMachine, 'interfaces') class VirtualMachineInterfacesView(generic.ObjectChildrenView): queryset = VirtualMachine.objects.all() child_model = VMInterface table = tables.VirtualMachineVMInterfaceTable filterset = filtersets.VMInterfaceFilterSet template_name = 'virtualization/virtualmachine/interfaces.html' + tab = ViewTab( + label=_('Interfaces'), + badge=lambda obj: obj.interfaces.count(), + permission='virtualization.view_vminterface' + ) def get_children(self, request, parent): return parent.interfaces.restrict(request.user, 'view').prefetch_related( @@ -363,9 +383,14 @@ def get_extra_context(self, request, instance): } +@register_model_view(VirtualMachine, 'configcontext', path='config-context') class VirtualMachineConfigContextView(ObjectConfigContextView): queryset = VirtualMachine.objects.annotate_config_context_data() base_template = 'virtualization/virtualmachine.html' + tab = ViewTab( + label=_('Config Context'), + permission='extras.view_configcontext' + ) class VirtualMachineEditView(generic.ObjectEditView): From 5e1a0733e4b439137e2e671b9b7404375db70809 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 7 Oct 2022 12:14:19 -0400 Subject: [PATCH 5/7] Replace active_tab context for object views --- netbox/dcim/views.py | 9 ------ netbox/extras/views.py | 1 - netbox/ipam/urls.py | 2 -- netbox/ipam/views.py | 31 ++++++++------------ netbox/netbox/views/generic/base.py | 1 + netbox/netbox/views/generic/feature_views.py | 4 +-- netbox/netbox/views/generic/object_views.py | 3 +- netbox/templates/generic/object.html | 2 +- netbox/templates/ipam/vlan/base.html | 24 --------------- netbox/utilities/templatetags/tabs.py | 6 ++-- netbox/virtualization/views.py | 15 ---------- 11 files changed, 22 insertions(+), 76 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index e299357d15..d5aed58975 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -46,11 +46,6 @@ class DeviceComponentsView(generic.ObjectChildrenView): def get_children(self, request, parent): return self.child_model.objects.restrict(request.user, 'view').filter(device=parent) - def get_extra_context(self, request, instance): - return { - 'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '')}", - } - class DeviceTypeComponentsView(DeviceComponentsView): queryset = DeviceType.objects.all() @@ -61,9 +56,7 @@ def get_children(self, request, parent): return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent) def get_extra_context(self, request, instance): - model_name = self.child_model._meta.verbose_name_plural return { - 'active_tab': f"{model_name.replace(' ', '').replace('template', '')}", 'return_url': reverse(self.viewname, kwargs={'pk': instance.pk}), } @@ -77,9 +70,7 @@ def get_children(self, request, parent): return self.child_model.objects.restrict(request.user, 'view').filter(module_type=parent) def get_extra_context(self, request, instance): - model_name = self.child_model._meta.verbose_name_plural return { - 'active_tab': f"{model_name.replace(' ', '').replace('template', '')}", 'return_url': reverse(self.viewname, kwargs={'pk': instance.pk}), } diff --git a/netbox/extras/views.py b/netbox/extras/views.py index f95b3fb64a..e48fd672bb 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -352,7 +352,6 @@ def get_extra_context(self, request, instance): 'source_contexts': source_contexts, 'format': format, 'base_template': self.base_template, - 'active_tab': 'configcontext', } diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index c7b60045b0..d5594eeb92 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -141,8 +141,6 @@ path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), path('vlans//', views.VLANView.as_view(), name='vlan'), - path('vlans//interfaces/', views.VLANInterfacesView.as_view(), name='vlan_interfaces'), - path('vlans//vm-interfaces/', views.VLANVMInterfacesView.as_view(), name='vlan_vminterfaces'), path('vlans//edit/', views.VLANEditView.as_view(), name='vlan_edit'), path('vlans//delete/', views.VLANDeleteView.as_view(), name='vlan_delete'), path('vlans//', include(get_model_urls('ipam', 'vlan'))), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index f705664b3c..fba577f029 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -319,7 +319,6 @@ def prep_table_data(self, request, queryset, parent): def get_extra_context(self, request, instance): return { 'bulk_querystring': f'within={instance.prefix}', - 'active_tab': 'prefixes', 'first_available_prefix': instance.get_first_available_prefix(), 'show_available': bool(request.GET.get('show_available', 'true') == 'true'), 'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'), @@ -502,7 +501,6 @@ def prep_table_data(self, request, queryset, parent): def get_extra_context(self, request, instance): return { 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&within={instance.prefix}", - 'active_tab': 'prefixes', 'first_available_prefix': instance.get_first_available_prefix(), 'show_available': bool(request.GET.get('show_available', 'true') == 'true'), 'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'), @@ -530,7 +528,6 @@ def get_children(self, request, parent): def get_extra_context(self, request, instance): return { 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", - 'active_tab': 'ipranges', 'first_available_ip': instance.get_first_available_ip(), } @@ -559,7 +556,6 @@ def prep_table_data(self, request, queryset, parent): def get_extra_context(self, request, instance): return { 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", - 'active_tab': 'ipaddresses', 'first_available_ip': instance.get_first_available_ip(), } @@ -623,11 +619,6 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView): def get_children(self, request, parent): return parent.get_child_ips().restrict(request.user, 'view') - def get_extra_context(self, request, instance): - return { - 'active_tab': 'ipaddresses', - } - class IPRangeEditView(generic.ObjectEditView): queryset = IPRange.objects.all() @@ -1032,37 +1023,39 @@ def get_extra_context(self, request, instance): } +@register_model_view(VLAN, 'interfaces') class VLANInterfacesView(generic.ObjectChildrenView): queryset = VLAN.objects.all() child_model = Interface table = tables.VLANDevicesTable filterset = InterfaceFilterSet template_name = 'ipam/vlan/interfaces.html' + tab = ViewTab( + label=_('Device Interfaces'), + badge=lambda x: x.get_interfaces().count(), + permission='dcim.view_interface' + ) def get_children(self, request, parent): return parent.get_interfaces().restrict(request.user, 'view') - def get_extra_context(self, request, instance): - return { - 'active_tab': 'interfaces', - } - +@register_model_view(VLAN, 'vminterfaces', path='vm-interfaces') class VLANVMInterfacesView(generic.ObjectChildrenView): queryset = VLAN.objects.all() child_model = VMInterface table = tables.VLANVirtualMachinesTable filterset = VMInterfaceFilterSet template_name = 'ipam/vlan/vminterfaces.html' + tab = ViewTab( + label=_('VM Interfaces'), + badge=lambda x: x.get_vminterfaces().count(), + permission='virtualization.view_vminterface' + ) def get_children(self, request, parent): return parent.get_vminterfaces().restrict(request.user, 'view') - def get_extra_context(self, request, instance): - return { - 'active_tab': 'vminterfaces', - } - class VLANEditView(generic.ObjectEditView): queryset = VLAN.objects.all() diff --git a/netbox/netbox/views/generic/base.py b/netbox/netbox/views/generic/base.py index 3ad3bcf679..3a85df6180 100644 --- a/netbox/netbox/views/generic/base.py +++ b/netbox/netbox/views/generic/base.py @@ -14,6 +14,7 @@ class BaseObjectView(ObjectPermissionRequiredMixin, View): """ queryset = None template_name = None + tab = None def get_object(self, **kwargs): """ diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 963fad1964..ce5b29eb2e 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -62,7 +62,7 @@ def get(self, request, model, **kwargs): 'object': obj, 'table': objectchanges_table, 'base_template': self.base_template, - 'active_tab': 'changelog', + 'tab': self.tab, }) @@ -122,5 +122,5 @@ def get(self, request, model, **kwargs): 'form': form, 'table': journalentry_table, 'base_template': self.base_template, - 'active_tab': 'journal', + 'tab': self.tab, }) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index a56a832b6e..941eee72e6 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -5,7 +5,6 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.db.models import ProtectedError -from django.forms.widgets import HiddenInput from django.shortcuts import redirect, render from django.urls import reverse from django.utils.html import escape @@ -67,6 +66,7 @@ def get(self, request, **kwargs): return render(request, self.get_template_name(), { 'object': instance, + 'tab': self.tab, **self.get_extra_context(request, instance), }) @@ -141,6 +141,7 @@ def get(self, request, *args, **kwargs): 'child_model': self.child_model, 'table': table, 'actions': actions, + 'tab': self.tab, **self.get_extra_context(request, instance), }) diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index 2c3c76329d..023726a30e 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -81,7 +81,7 @@