Merge pull request #10597 from netbox-community/9072-plugin-view-tabs

#9072: Custom model view tabs for plugins
This commit is contained in:
Jeremy Stretch 2022-10-07 15:22:04 -04:00 committed by GitHub
commit a3dbf4023c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 659 additions and 637 deletions

View File

@ -148,6 +148,32 @@ These views are provided to enable or enhance certain NetBox model features, suc
## Extending Core Views ## Extending Core Views
### Additional Tabs
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`:
```python
from dcim.models import Site
from myplugin.models import Stuff
from netbox.views import generic
from utilities.views import ViewTab, register_model_view
@register_model_view(Site, 'mview', path='some-other-stuff')
class MyView(generic.ObjectView):
...
tab = ViewTab(
label='Other Stuff',
badge=lambda obj: Stuff.objects.filter(site=obj).count(),
permission='myplugin.view_stuff'
)
```
::: utilities.views.register_model_view
::: utilities.views.ViewTab
### Extra Template Content
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available: Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
* `left_page()` - Inject content on the left side of the page * `left_page()` - Inject content on the left side of the page

View File

@ -27,6 +27,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
### Plugins API ### Plugins API
* [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus * [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
* [#9072](https://github.com/netbox-community/netbox/issues/9072) - Enable registration of tabbed plugin views for core NetBox models
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter * [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin * [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin

View File

@ -1,9 +1,9 @@
from django.urls import path from django.urls import include, path
from dcim.views import PathTraceView from dcim.views import PathTraceView
from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from utilities.urls import get_model_urls
from . import views from . import views
from .models import * from .models import CircuitTermination
app_name = 'circuits' app_name = 'circuits'
urlpatterns = [ urlpatterns = [
@ -17,8 +17,7 @@ urlpatterns = [
path('providers/<int:pk>/', views.ProviderView.as_view(), name='provider'), path('providers/<int:pk>/', views.ProviderView.as_view(), name='provider'),
path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
path('providers/<int:pk>/journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
# Provider networks # Provider networks
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'), path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
@ -29,8 +28,7 @@ urlpatterns = [
path('provider-networks/<int:pk>/', views.ProviderNetworkView.as_view(), name='providernetwork'), path('provider-networks/<int:pk>/', views.ProviderNetworkView.as_view(), name='providernetwork'),
path('provider-networks/<int:pk>/edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'), path('provider-networks/<int:pk>/edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'),
path('provider-networks/<int:pk>/delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'), path('provider-networks/<int:pk>/delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'),
path('provider-networks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}), path('provider-networks/<int:pk>/', include(get_model_urls('circuits', 'providernetwork'))),
path('provider-networks/<int:pk>/journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}),
# Circuit types # Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
@ -41,7 +39,7 @@ urlpatterns = [
path('circuit-types/<int:pk>/', views.CircuitTypeView.as_view(), name='circuittype'), path('circuit-types/<int:pk>/', views.CircuitTypeView.as_view(), name='circuittype'),
path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'), path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), path('circuit-types/<int:pk>/', include(get_model_urls('circuits', 'circuittype'))),
# Circuits # Circuits
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'), path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
@ -52,9 +50,8 @@ urlpatterns = [
path('circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'), path('circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'), path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
path('circuits/<int:pk>/journal/', ObjectJournalView.as_view(), name='circuit_journal', kwargs={'model': Circuit}),
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'), path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
# Circuit terminations # Circuit terminations
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'), path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),

View File

@ -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 . import views
from .models import * from .models import (
ConsolePort, ConsoleServerPort, FrontPort, Interface, PowerFeed, PowerPort, PowerOutlet, RearPort,
)
app_name = 'dcim' app_name = 'dcim'
urlpatterns = [ urlpatterns = [
@ -16,7 +18,7 @@ urlpatterns = [
path('regions/<int:pk>/', views.RegionView.as_view(), name='region'), path('regions/<int:pk>/', views.RegionView.as_view(), name='region'),
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'), path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'), path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), path('regions/<int:pk>/', include(get_model_urls('dcim', 'region'))),
# Site groups # Site groups
path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'), path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'),
@ -27,7 +29,7 @@ urlpatterns = [
path('site-groups/<int:pk>/', views.SiteGroupView.as_view(), name='sitegroup'), path('site-groups/<int:pk>/', views.SiteGroupView.as_view(), name='sitegroup'),
path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'), path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'), path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
path('site-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}), path('site-groups/<int:pk>/', include(get_model_urls('dcim', 'sitegroup'))),
# Sites # Sites
path('sites/', views.SiteListView.as_view(), name='site_list'), path('sites/', views.SiteListView.as_view(), name='site_list'),
@ -38,8 +40,7 @@ urlpatterns = [
path('sites/<int:pk>/', views.SiteView.as_view(), name='site'), path('sites/<int:pk>/', views.SiteView.as_view(), name='site'),
path('sites/<int:pk>/edit/', views.SiteEditView.as_view(), name='site_edit'), path('sites/<int:pk>/edit/', views.SiteEditView.as_view(), name='site_edit'),
path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'), path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), path('sites/<int:pk>/', include(get_model_urls('dcim', 'site'))),
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
# Locations # Locations
path('locations/', views.LocationListView.as_view(), name='location_list'), path('locations/', views.LocationListView.as_view(), name='location_list'),
@ -50,7 +51,7 @@ urlpatterns = [
path('locations/<int:pk>/', views.LocationView.as_view(), name='location'), path('locations/<int:pk>/', views.LocationView.as_view(), name='location'),
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'), path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'), path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}), path('locations/<int:pk>/', include(get_model_urls('dcim', 'location'))),
# Rack roles # Rack roles
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
@ -61,7 +62,7 @@ urlpatterns = [
path('rack-roles/<int:pk>/', views.RackRoleView.as_view(), name='rackrole'), path('rack-roles/<int:pk>/', views.RackRoleView.as_view(), name='rackrole'),
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'), path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), path('rack-roles/<int:pk>/', include(get_model_urls('dcim', 'rackrole'))),
# Rack reservations # Rack reservations
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
@ -72,8 +73,7 @@ urlpatterns = [
path('rack-reservations/<int:pk>/', views.RackReservationView.as_view(), name='rackreservation'), path('rack-reservations/<int:pk>/', views.RackReservationView.as_view(), name='rackreservation'),
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), path('rack-reservations/<int:pk>/', include(get_model_urls('dcim', 'rackreservation'))),
path('rack-reservations/<int:pk>/journal/', ObjectJournalView.as_view(), name='rackreservation_journal', kwargs={'model': RackReservation}),
# Racks # Racks
path('racks/', views.RackListView.as_view(), name='rack_list'), path('racks/', views.RackListView.as_view(), name='rack_list'),
@ -85,8 +85,7 @@ urlpatterns = [
path('racks/<int:pk>/', views.RackView.as_view(), name='rack'), path('racks/<int:pk>/', views.RackView.as_view(), name='rack'),
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'), path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'), path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), path('racks/<int:pk>/', include(get_model_urls('dcim', 'rack'))),
path('racks/<int:pk>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
# Manufacturers # Manufacturers
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
@ -97,7 +96,7 @@ urlpatterns = [
path('manufacturers/<int:pk>/', views.ManufacturerView.as_view(), name='manufacturer'), path('manufacturers/<int:pk>/', views.ManufacturerView.as_view(), name='manufacturer'),
path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'), path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
path('manufacturers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), path('manufacturers/<int:pk>/', include(get_model_urls('dcim', 'manufacturer'))),
# Device types # Device types
path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
@ -106,20 +105,9 @@ urlpatterns = [
path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), 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/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'), path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
path('device-types/<int:pk>/console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'),
path('device-types/<int:pk>/console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'),
path('device-types/<int:pk>/power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'),
path('device-types/<int:pk>/power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'),
path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
path('device-types/<int:pk>/module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'),
path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
path('device-types/<int:pk>/inventory-items/', views.DeviceTypeInventoryItemsView.as_view(), name='devicetype_inventoryitems'),
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), path('device-types/<int:pk>/', include(get_model_urls('dcim', 'devicetype'))),
path('device-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
# Module types # Module types
path('module-types/', views.ModuleTypeListView.as_view(), name='moduletype_list'), path('module-types/', views.ModuleTypeListView.as_view(), name='moduletype_list'),
@ -128,17 +116,9 @@ urlpatterns = [
path('module-types/edit/', views.ModuleTypeBulkEditView.as_view(), name='moduletype_bulk_edit'), 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/delete/', views.ModuleTypeBulkDeleteView.as_view(), name='moduletype_bulk_delete'),
path('module-types/<int:pk>/', views.ModuleTypeView.as_view(), name='moduletype'), path('module-types/<int:pk>/', views.ModuleTypeView.as_view(), name='moduletype'),
path('module-types/<int:pk>/console-ports/', views.ModuleTypeConsolePortsView.as_view(), name='moduletype_consoleports'),
path('module-types/<int:pk>/console-server-ports/', views.ModuleTypeConsoleServerPortsView.as_view(), name='moduletype_consoleserverports'),
path('module-types/<int:pk>/power-ports/', views.ModuleTypePowerPortsView.as_view(), name='moduletype_powerports'),
path('module-types/<int:pk>/power-outlets/', views.ModuleTypePowerOutletsView.as_view(), name='moduletype_poweroutlets'),
path('module-types/<int:pk>/interfaces/', views.ModuleTypeInterfacesView.as_view(), name='moduletype_interfaces'),
path('module-types/<int:pk>/front-ports/', views.ModuleTypeFrontPortsView.as_view(), name='moduletype_frontports'),
path('module-types/<int:pk>/rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'),
path('module-types/<int:pk>/edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'), path('module-types/<int:pk>/edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'),
path('module-types/<int:pk>/delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'), path('module-types/<int:pk>/delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'),
path('module-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='moduletype_changelog', kwargs={'model': ModuleType}), path('module-types/<int:pk>/', include(get_model_urls('dcim', 'moduletype'))),
path('module-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='moduletype_journal', kwargs={'model': ModuleType}),
# Console port templates # Console port templates
path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'), path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
@ -229,7 +209,7 @@ urlpatterns = [
path('device-roles/<int:pk>/', views.DeviceRoleView.as_view(), name='devicerole'), path('device-roles/<int:pk>/', views.DeviceRoleView.as_view(), name='devicerole'),
path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'), path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
path('device-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), path('device-roles/<int:pk>/', include(get_model_urls('dcim', 'devicerole'))),
# Platforms # Platforms
path('platforms/', views.PlatformListView.as_view(), name='platform_list'), path('platforms/', views.PlatformListView.as_view(), name='platform_list'),
@ -240,7 +220,7 @@ urlpatterns = [
path('platforms/<int:pk>/', views.PlatformView.as_view(), name='platform'), path('platforms/<int:pk>/', views.PlatformView.as_view(), name='platform'),
path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'), path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'), path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
path('platforms/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), path('platforms/<int:pk>/', include(get_model_urls('dcim', 'platform'))),
# Devices # Devices
path('devices/', views.DeviceListView.as_view(), name='device_list'), path('devices/', views.DeviceListView.as_view(), name='device_list'),
@ -253,22 +233,7 @@ urlpatterns = [
path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'), path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'), path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'), path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
path('devices/<int:pk>/console-ports/', views.DeviceConsolePortsView.as_view(), name='device_consoleports'), path('devices/<int:pk>/', include(get_model_urls('dcim', 'device'))),
path('devices/<int:pk>/console-server-ports/', views.DeviceConsoleServerPortsView.as_view(), name='device_consoleserverports'),
path('devices/<int:pk>/power-ports/', views.DevicePowerPortsView.as_view(), name='device_powerports'),
path('devices/<int:pk>/power-outlets/', views.DevicePowerOutletsView.as_view(), name='device_poweroutlets'),
path('devices/<int:pk>/interfaces/', views.DeviceInterfacesView.as_view(), name='device_interfaces'),
path('devices/<int:pk>/front-ports/', views.DeviceFrontPortsView.as_view(), name='device_frontports'),
path('devices/<int:pk>/rear-ports/', views.DeviceRearPortsView.as_view(), name='device_rearports'),
path('devices/<int:pk>/module-bays/', views.DeviceModuleBaysView.as_view(), name='device_modulebays'),
path('devices/<int:pk>/device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'),
path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
path('devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
path('devices/<int:pk>/journal/', ObjectJournalView.as_view(), name='device_journal', kwargs={'model': Device}),
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
# Modules # Modules
path('modules/', views.ModuleListView.as_view(), name='module_list'), path('modules/', views.ModuleListView.as_view(), name='module_list'),
@ -279,8 +244,7 @@ urlpatterns = [
path('modules/<int:pk>/', views.ModuleView.as_view(), name='module'), path('modules/<int:pk>/', views.ModuleView.as_view(), name='module'),
path('modules/<int:pk>/edit/', views.ModuleEditView.as_view(), name='module_edit'), path('modules/<int:pk>/edit/', views.ModuleEditView.as_view(), name='module_edit'),
path('modules/<int:pk>/delete/', views.ModuleDeleteView.as_view(), name='module_delete'), path('modules/<int:pk>/delete/', views.ModuleDeleteView.as_view(), name='module_delete'),
path('modules/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='module_changelog', kwargs={'model': Module}), path('modules/<int:pk>/', include(get_model_urls('dcim', 'module'))),
path('modules/<int:pk>/journal/', ObjectJournalView.as_view(), name='module_journal', kwargs={'model': Module}),
# Console ports # Console ports
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
@ -293,8 +257,8 @@ urlpatterns = [
path('console-ports/<int:pk>/', views.ConsolePortView.as_view(), name='consoleport'), path('console-ports/<int:pk>/', views.ConsolePortView.as_view(), name='consoleport'),
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
path('console-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path('console-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path('console-ports/<int:pk>/', include(get_model_urls('dcim', 'consoleport'))),
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
# Console server ports # Console server ports
@ -308,8 +272,8 @@ urlpatterns = [
path('console-server-ports/<int:pk>/', views.ConsoleServerPortView.as_view(), name='consoleserverport'), path('console-server-ports/<int:pk>/', views.ConsoleServerPortView.as_view(), name='consoleserverport'),
path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
path('console-server-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path('console-server-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path('console-server-ports/<int:pk>/', include(get_model_urls('dcim', 'consoleserverport'))),
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
# Power ports # Power ports
@ -323,8 +287,8 @@ urlpatterns = [
path('power-ports/<int:pk>/', views.PowerPortView.as_view(), name='powerport'), path('power-ports/<int:pk>/', views.PowerPortView.as_view(), name='powerport'),
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
path('power-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path('power-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path('power-ports/<int:pk>/', include(get_model_urls('dcim', 'powerport'))),
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
# Power outlets # Power outlets
@ -338,8 +302,8 @@ urlpatterns = [
path('power-outlets/<int:pk>/', views.PowerOutletView.as_view(), name='poweroutlet'), path('power-outlets/<int:pk>/', views.PowerOutletView.as_view(), name='poweroutlet'),
path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
path('power-outlets/<int:pk>/trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path('power-outlets/<int:pk>/trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path('power-outlets/<int:pk>/', include(get_model_urls('dcim', 'poweroutlet'))),
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
# Interfaces # Interfaces
@ -353,8 +317,8 @@ urlpatterns = [
path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'), path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
path('interfaces/<int:pk>/trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path('interfaces/<int:pk>/trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path('interfaces/<int:pk>/', include(get_model_urls('dcim', 'interface'))),
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
# Front ports # Front ports
@ -368,8 +332,8 @@ urlpatterns = [
path('front-ports/<int:pk>/', views.FrontPortView.as_view(), name='frontport'), path('front-ports/<int:pk>/', views.FrontPortView.as_view(), name='frontport'),
path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
path('front-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path('front-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path('front-ports/<int:pk>/', include(get_model_urls('dcim', 'frontport'))),
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
# Rear ports # Rear ports
@ -383,8 +347,8 @@ urlpatterns = [
path('rear-ports/<int:pk>/', views.RearPortView.as_view(), name='rearport'), path('rear-ports/<int:pk>/', views.RearPortView.as_view(), name='rearport'),
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
path('rear-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path('rear-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path('rear-ports/<int:pk>/', include(get_model_urls('dcim', 'rearport'))),
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Module bays # Module bays
@ -397,7 +361,7 @@ urlpatterns = [
path('module-bays/<int:pk>/', views.ModuleBayView.as_view(), name='modulebay'), path('module-bays/<int:pk>/', views.ModuleBayView.as_view(), name='modulebay'),
path('module-bays/<int:pk>/edit/', views.ModuleBayEditView.as_view(), name='modulebay_edit'), path('module-bays/<int:pk>/edit/', views.ModuleBayEditView.as_view(), name='modulebay_edit'),
path('module-bays/<int:pk>/delete/', views.ModuleBayDeleteView.as_view(), name='modulebay_delete'), path('module-bays/<int:pk>/delete/', views.ModuleBayDeleteView.as_view(), name='modulebay_delete'),
path('module-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='modulebay_changelog', kwargs={'model': ModuleBay}), path('module-bays/<int:pk>/', include(get_model_urls('dcim', 'modulebay'))),
path('devices/module-bays/add/', views.DeviceBulkAddModuleBayView.as_view(), name='device_bulk_add_modulebay'), path('devices/module-bays/add/', views.DeviceBulkAddModuleBayView.as_view(), name='device_bulk_add_modulebay'),
# Device bays # Device bays
@ -410,9 +374,9 @@ urlpatterns = [
path('device-bays/<int:pk>/', views.DeviceBayView.as_view(), name='devicebay'), path('device-bays/<int:pk>/', views.DeviceBayView.as_view(), name='devicebay'),
path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path('device-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicebay_changelog', kwargs={'model': DeviceBay}),
path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path('device-bays/<int:pk>/', include(get_model_urls('dcim', 'devicebay'))),
path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
# Inventory items # Inventory items
@ -425,10 +389,10 @@ urlpatterns = [
path('inventory-items/<int:pk>/', views.InventoryItemView.as_view(), name='inventoryitem'), path('inventory-items/<int:pk>/', views.InventoryItemView.as_view(), name='inventoryitem'),
path('inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), path('inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
path('inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), path('inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
path('inventory-items/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}), path('inventory-items/<int:pk>/', include(get_model_urls('dcim', 'inventoryitem'))),
path('devices/inventory-items/add/', views.DeviceBulkAddInventoryItemView.as_view(), name='device_bulk_add_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/', views.InventoryItemRoleListView.as_view(), name='inventoryitemrole_list'),
path('inventory-item-roles/add/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_add'), path('inventory-item-roles/add/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_add'),
path('inventory-item-roles/import/', views.InventoryItemRoleBulkImportView.as_view(), name='inventoryitemrole_import'), path('inventory-item-roles/import/', views.InventoryItemRoleBulkImportView.as_view(), name='inventoryitemrole_import'),
@ -437,7 +401,7 @@ urlpatterns = [
path('inventory-item-roles/<int:pk>/', views.InventoryItemRoleView.as_view(), name='inventoryitemrole'), path('inventory-item-roles/<int:pk>/', views.InventoryItemRoleView.as_view(), name='inventoryitemrole'),
path('inventory-item-roles/<int:pk>/edit/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_edit'), path('inventory-item-roles/<int:pk>/edit/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_edit'),
path('inventory-item-roles/<int:pk>/delete/', views.InventoryItemRoleDeleteView.as_view(), name='inventoryitemrole_delete'), path('inventory-item-roles/<int:pk>/delete/', views.InventoryItemRoleDeleteView.as_view(), name='inventoryitemrole_delete'),
path('inventory-item-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitemrole_changelog', kwargs={'model': InventoryItemRole}), path('inventory-item-roles/<int:pk>/', include(get_model_urls('dcim', 'inventoryitemrole'))),
# Cables # Cables
path('cables/', views.CableListView.as_view(), name='cable_list'), path('cables/', views.CableListView.as_view(), name='cable_list'),
@ -448,8 +412,7 @@ urlpatterns = [
path('cables/<int:pk>/', views.CableView.as_view(), name='cable'), path('cables/<int:pk>/', views.CableView.as_view(), name='cable'),
path('cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'), path('cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'), path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
path('cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), path('cables/<int:pk>/', include(get_model_urls('dcim', 'cable'))),
path('cables/<int:pk>/journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}),
# Console/power/interface connections (read-only) # Console/power/interface connections (read-only)
path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
@ -465,9 +428,8 @@ urlpatterns = [
path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'), path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'),
path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
path('virtual-chassis/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualchassis_journal', kwargs={'model': VirtualChassis}),
path('virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), path('virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
path('virtual-chassis/<int:pk>/', include(get_model_urls('dcim', 'virtualchassis'))),
path('virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), path('virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
# Power panels # Power panels
@ -479,8 +441,7 @@ urlpatterns = [
path('power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'), path('power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'),
path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
path('power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), path('power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
path('power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), path('power-panels/<int:pk>/', include(get_model_urls('dcim', 'powerpanel'))),
path('power-panels/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerpanel_journal', kwargs={'model': PowerPanel}),
# Power feeds # Power feeds
path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
@ -493,7 +454,6 @@ urlpatterns = [
path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}), path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), path('power-feeds/<int:pk>/', include(get_model_urls('dcim', 'powerfeed'))),
path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}),
] ]

View File

@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.views.generic import View from django.views.generic import View
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
@ -19,7 +20,7 @@ from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.utils import count_related 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 virtualization.models import VirtualMachine
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import DeviceFaceChoices from .choices import DeviceFaceChoices
@ -45,11 +46,6 @@ class DeviceComponentsView(generic.ObjectChildrenView):
def get_children(self, request, parent): def get_children(self, request, parent):
return self.child_model.objects.restrict(request.user, 'view').filter(device=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): class DeviceTypeComponentsView(DeviceComponentsView):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
@ -60,10 +56,9 @@ class DeviceTypeComponentsView(DeviceComponentsView):
return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent) return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
context = super().get_extra_context(request, instance) return {
context['return_url'] = reverse(self.viewname, kwargs={'pk': instance.pk}) 'return_url': reverse(self.viewname, kwargs={'pk': instance.pk}),
}
return context
class ModuleTypeComponentsView(DeviceComponentsView): class ModuleTypeComponentsView(DeviceComponentsView):
@ -75,10 +70,9 @@ class ModuleTypeComponentsView(DeviceComponentsView):
return self.child_model.objects.restrict(request.user, 'view').filter(module_type=parent) return self.child_model.objects.restrict(request.user, 'view').filter(module_type=parent)
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
context = super().get_extra_context(request, instance) return {
context['return_url'] = reverse(self.viewname, kwargs={'pk': instance.pk}) 'return_url': reverse(self.viewname, kwargs={'pk': instance.pk}),
}
return context
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
@ -857,74 +851,134 @@ class DeviceTypeView(generic.ObjectView):
} }
@register_model_view(DeviceType, 'consoleports', path='console-ports')
class DeviceTypeConsolePortsView(DeviceTypeComponentsView): class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
child_model = ConsolePortTemplate child_model = ConsolePortTemplate
table = tables.ConsolePortTemplateTable table = tables.ConsolePortTemplateTable
filterset = filtersets.ConsolePortTemplateFilterSet filterset = filtersets.ConsolePortTemplateFilterSet
viewname = 'dcim:devicetype_consoleports' viewname = 'dcim:devicetype_consoleports'
tab = ViewTab(
label=_('Console Ports'),
badge=lambda obj: obj.consoleporttemplates.count(),
permission='dcim.view_consoleporttemplate'
)
@register_model_view(DeviceType, 'consoleserverports', path='console-server-ports')
class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView): class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
child_model = ConsoleServerPortTemplate child_model = ConsoleServerPortTemplate
table = tables.ConsoleServerPortTemplateTable table = tables.ConsoleServerPortTemplateTable
filterset = filtersets.ConsoleServerPortTemplateFilterSet filterset = filtersets.ConsoleServerPortTemplateFilterSet
viewname = 'dcim:devicetype_consoleserverports' viewname = 'dcim:devicetype_consoleserverports'
tab = ViewTab(
label=_('Console Server Ports'),
badge=lambda obj: obj.consoleserverporttemplates.count(),
permission='dcim.view_consoleserverporttemplate'
)
@register_model_view(DeviceType, 'powerports', path='power-ports')
class DeviceTypePowerPortsView(DeviceTypeComponentsView): class DeviceTypePowerPortsView(DeviceTypeComponentsView):
child_model = PowerPortTemplate child_model = PowerPortTemplate
table = tables.PowerPortTemplateTable table = tables.PowerPortTemplateTable
filterset = filtersets.PowerPortTemplateFilterSet filterset = filtersets.PowerPortTemplateFilterSet
viewname = 'dcim:devicetype_powerports' viewname = 'dcim:devicetype_powerports'
tab = ViewTab(
label=_('Power Ports'),
badge=lambda obj: obj.powerporttemplates.count(),
permission='dcim.view_powerporttemplate'
)
@register_model_view(DeviceType, 'poweroutlets', path='power-outlets')
class DeviceTypePowerOutletsView(DeviceTypeComponentsView): class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
child_model = PowerOutletTemplate child_model = PowerOutletTemplate
table = tables.PowerOutletTemplateTable table = tables.PowerOutletTemplateTable
filterset = filtersets.PowerOutletTemplateFilterSet filterset = filtersets.PowerOutletTemplateFilterSet
viewname = 'dcim:devicetype_poweroutlets' viewname = 'dcim:devicetype_poweroutlets'
tab = ViewTab(
label=_('Power Outlets'),
badge=lambda obj: obj.poweroutlettemplates.count(),
permission='dcim.view_poweroutlettemplate'
)
@register_model_view(DeviceType, 'interfaces')
class DeviceTypeInterfacesView(DeviceTypeComponentsView): class DeviceTypeInterfacesView(DeviceTypeComponentsView):
child_model = InterfaceTemplate child_model = InterfaceTemplate
table = tables.InterfaceTemplateTable table = tables.InterfaceTemplateTable
filterset = filtersets.InterfaceTemplateFilterSet filterset = filtersets.InterfaceTemplateFilterSet
viewname = 'dcim:devicetype_interfaces' viewname = 'dcim:devicetype_interfaces'
tab = ViewTab(
label=_('Interfaces'),
badge=lambda obj: obj.interfacetemplates.count(),
permission='dcim.view_interfacetemplate'
)
@register_model_view(DeviceType, 'frontports', path='front-ports')
class DeviceTypeFrontPortsView(DeviceTypeComponentsView): class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
child_model = FrontPortTemplate child_model = FrontPortTemplate
table = tables.FrontPortTemplateTable table = tables.FrontPortTemplateTable
filterset = filtersets.FrontPortTemplateFilterSet filterset = filtersets.FrontPortTemplateFilterSet
viewname = 'dcim:devicetype_frontports' viewname = 'dcim:devicetype_frontports'
tab = ViewTab(
label=_('Front Ports'),
badge=lambda obj: obj.frontporttemplates.count(),
permission='dcim.view_frontporttemplate'
)
@register_model_view(DeviceType, 'rearports', path='rear-ports')
class DeviceTypeRearPortsView(DeviceTypeComponentsView): class DeviceTypeRearPortsView(DeviceTypeComponentsView):
child_model = RearPortTemplate child_model = RearPortTemplate
table = tables.RearPortTemplateTable table = tables.RearPortTemplateTable
filterset = filtersets.RearPortTemplateFilterSet filterset = filtersets.RearPortTemplateFilterSet
viewname = 'dcim:devicetype_rearports' viewname = 'dcim:devicetype_rearports'
tab = ViewTab(
label=_('Rear Ports'),
badge=lambda obj: obj.rearporttemplates.count(),
permission='dcim.view_rearporttemplate'
)
@register_model_view(DeviceType, 'modulebays', path='module-bays')
class DeviceTypeModuleBaysView(DeviceTypeComponentsView): class DeviceTypeModuleBaysView(DeviceTypeComponentsView):
child_model = ModuleBayTemplate child_model = ModuleBayTemplate
table = tables.ModuleBayTemplateTable table = tables.ModuleBayTemplateTable
filterset = filtersets.ModuleBayTemplateFilterSet filterset = filtersets.ModuleBayTemplateFilterSet
viewname = 'dcim:devicetype_modulebays' viewname = 'dcim:devicetype_modulebays'
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.modulebaytemplates.count(),
permission='dcim.view_modulebaytemplate'
)
@register_model_view(DeviceType, 'devicebays', path='device-bays')
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
child_model = DeviceBayTemplate child_model = DeviceBayTemplate
table = tables.DeviceBayTemplateTable table = tables.DeviceBayTemplateTable
filterset = filtersets.DeviceBayTemplateFilterSet filterset = filtersets.DeviceBayTemplateFilterSet
viewname = 'dcim:devicetype_devicebays' viewname = 'dcim:devicetype_devicebays'
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.devicebaytemplates.count(),
permission='dcim.view_devicebaytemplate'
)
@register_model_view(DeviceType, 'inventoryitems', path='inventory-items')
class DeviceTypeInventoryItemsView(DeviceTypeComponentsView): class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
child_model = InventoryItemTemplate child_model = InventoryItemTemplate
table = tables.InventoryItemTemplateTable table = tables.InventoryItemTemplateTable
filterset = filtersets.InventoryItemTemplateFilterSet filterset = filtersets.InventoryItemTemplateFilterSet
viewname = 'dcim:devicetype_inventoryitems' viewname = 'dcim:devicetype_inventoryitems'
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventoryitemtemplates.count(),
permission='dcim.view_invenotryitemtemplate'
)
class DeviceTypeEditView(generic.ObjectEditView): class DeviceTypeEditView(generic.ObjectEditView):
@ -1011,53 +1065,95 @@ class ModuleTypeView(generic.ObjectView):
} }
@register_model_view(ModuleType, 'consoleports', path='console-ports')
class ModuleTypeConsolePortsView(ModuleTypeComponentsView): class ModuleTypeConsolePortsView(ModuleTypeComponentsView):
child_model = ConsolePortTemplate child_model = ConsolePortTemplate
table = tables.ConsolePortTemplateTable table = tables.ConsolePortTemplateTable
filterset = filtersets.ConsolePortTemplateFilterSet filterset = filtersets.ConsolePortTemplateFilterSet
viewname = 'dcim:moduletype_consoleports' viewname = 'dcim:moduletype_consoleports'
tab = ViewTab(
label=_('Console Ports'),
badge=lambda obj: obj.consoleporttemplates.count(),
permission='dcim.view_consoleporttemplate'
)
@register_model_view(ModuleType, 'consoleserverports', path='console-server-ports')
class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView): class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView):
child_model = ConsoleServerPortTemplate child_model = ConsoleServerPortTemplate
table = tables.ConsoleServerPortTemplateTable table = tables.ConsoleServerPortTemplateTable
filterset = filtersets.ConsoleServerPortTemplateFilterSet filterset = filtersets.ConsoleServerPortTemplateFilterSet
viewname = 'dcim:moduletype_consoleserverports' viewname = 'dcim:moduletype_consoleserverports'
tab = ViewTab(
label=_('Console Server Ports'),
badge=lambda obj: obj.consoleserverporttemplates.count(),
permission='dcim.view_consoleserverporttemplate'
)
@register_model_view(ModuleType, 'powerports', path='power-ports')
class ModuleTypePowerPortsView(ModuleTypeComponentsView): class ModuleTypePowerPortsView(ModuleTypeComponentsView):
child_model = PowerPortTemplate child_model = PowerPortTemplate
table = tables.PowerPortTemplateTable table = tables.PowerPortTemplateTable
filterset = filtersets.PowerPortTemplateFilterSet filterset = filtersets.PowerPortTemplateFilterSet
viewname = 'dcim:moduletype_powerports' viewname = 'dcim:moduletype_powerports'
tab = ViewTab(
label=_('Power Ports'),
badge=lambda obj: obj.powerporttemplates.count(),
permission='dcim.view_powerporttemplate'
)
@register_model_view(ModuleType, 'poweroutlets', path='power-outlets')
class ModuleTypePowerOutletsView(ModuleTypeComponentsView): class ModuleTypePowerOutletsView(ModuleTypeComponentsView):
child_model = PowerOutletTemplate child_model = PowerOutletTemplate
table = tables.PowerOutletTemplateTable table = tables.PowerOutletTemplateTable
filterset = filtersets.PowerOutletTemplateFilterSet filterset = filtersets.PowerOutletTemplateFilterSet
viewname = 'dcim:moduletype_poweroutlets' viewname = 'dcim:moduletype_poweroutlets'
tab = ViewTab(
label=_('Power Outlets'),
badge=lambda obj: obj.poweroutlettemplates.count(),
permission='dcim.view_poweroutlettemplate'
)
@register_model_view(ModuleType, 'interfaces')
class ModuleTypeInterfacesView(ModuleTypeComponentsView): class ModuleTypeInterfacesView(ModuleTypeComponentsView):
child_model = InterfaceTemplate child_model = InterfaceTemplate
table = tables.InterfaceTemplateTable table = tables.InterfaceTemplateTable
filterset = filtersets.InterfaceTemplateFilterSet filterset = filtersets.InterfaceTemplateFilterSet
viewname = 'dcim:moduletype_interfaces' viewname = 'dcim:moduletype_interfaces'
tab = ViewTab(
label=_('Interfaces'),
badge=lambda obj: obj.interfacetemplates.count(),
permission='dcim.view_interfacetemplate'
)
@register_model_view(ModuleType, 'frontports', path='front-ports')
class ModuleTypeFrontPortsView(ModuleTypeComponentsView): class ModuleTypeFrontPortsView(ModuleTypeComponentsView):
child_model = FrontPortTemplate child_model = FrontPortTemplate
table = tables.FrontPortTemplateTable table = tables.FrontPortTemplateTable
filterset = filtersets.FrontPortTemplateFilterSet filterset = filtersets.FrontPortTemplateFilterSet
viewname = 'dcim:moduletype_frontports' viewname = 'dcim:moduletype_frontports'
tab = ViewTab(
label=_('Front Ports'),
badge=lambda obj: obj.frontporttemplates.count(),
permission='dcim.view_frontporttemplate'
)
@register_model_view(ModuleType, 'rearports', path='rear-ports')
class ModuleTypeRearPortsView(ModuleTypeComponentsView): class ModuleTypeRearPortsView(ModuleTypeComponentsView):
child_model = RearPortTemplate child_model = RearPortTemplate
table = tables.RearPortTemplateTable table = tables.RearPortTemplateTable
filterset = filtersets.RearPortTemplateFilterSet filterset = filtersets.RearPortTemplateFilterSet
viewname = 'dcim:moduletype_rearports' viewname = 'dcim:moduletype_rearports'
tab = ViewTab(
label=_('Rear Ports'),
badge=lambda obj: obj.rearporttemplates.count(),
permission='dcim.view_rearporttemplate'
)
class ModuleTypeEditView(generic.ObjectEditView): class ModuleTypeEditView(generic.ObjectEditView):
@ -1620,39 +1716,69 @@ class DeviceView(generic.ObjectView):
} }
@register_model_view(Device, 'consoleports', path='console-ports')
class DeviceConsolePortsView(DeviceComponentsView): class DeviceConsolePortsView(DeviceComponentsView):
child_model = ConsolePort child_model = ConsolePort
table = tables.DeviceConsolePortTable table = tables.DeviceConsolePortTable
filterset = filtersets.ConsolePortFilterSet filterset = filtersets.ConsolePortFilterSet
template_name = 'dcim/device/consoleports.html' template_name = 'dcim/device/consoleports.html'
tab = ViewTab(
label=_('Console Ports'),
badge=lambda obj: obj.consoleports.count(),
permission='dcim.view_consoleport'
)
@register_model_view(Device, 'consoleserverports', path='console-server-ports')
class DeviceConsoleServerPortsView(DeviceComponentsView): class DeviceConsoleServerPortsView(DeviceComponentsView):
child_model = ConsoleServerPort child_model = ConsoleServerPort
table = tables.DeviceConsoleServerPortTable table = tables.DeviceConsoleServerPortTable
filterset = filtersets.ConsoleServerPortFilterSet filterset = filtersets.ConsoleServerPortFilterSet
template_name = 'dcim/device/consoleserverports.html' template_name = 'dcim/device/consoleserverports.html'
tab = ViewTab(
label=_('Console Server Ports'),
badge=lambda obj: obj.consoleserverports.count(),
permission='dcim.view_consoleserverport'
)
@register_model_view(Device, 'powerports', path='power-ports')
class DevicePowerPortsView(DeviceComponentsView): class DevicePowerPortsView(DeviceComponentsView):
child_model = PowerPort child_model = PowerPort
table = tables.DevicePowerPortTable table = tables.DevicePowerPortTable
filterset = filtersets.PowerPortFilterSet filterset = filtersets.PowerPortFilterSet
template_name = 'dcim/device/powerports.html' template_name = 'dcim/device/powerports.html'
tab = ViewTab(
label=_('Power Ports'),
badge=lambda obj: obj.powerports.count(),
permission='dcim.view_powerport'
)
@register_model_view(Device, 'poweroutlets', path='power-outlets')
class DevicePowerOutletsView(DeviceComponentsView): class DevicePowerOutletsView(DeviceComponentsView):
child_model = PowerOutlet child_model = PowerOutlet
table = tables.DevicePowerOutletTable table = tables.DevicePowerOutletTable
filterset = filtersets.PowerOutletFilterSet filterset = filtersets.PowerOutletFilterSet
template_name = 'dcim/device/poweroutlets.html' template_name = 'dcim/device/poweroutlets.html'
tab = ViewTab(
label=_('Power Outlets'),
badge=lambda obj: obj.poweroutlets.count(),
permission='dcim.view_poweroutlet'
)
@register_model_view(Device, 'interfaces')
class DeviceInterfacesView(DeviceComponentsView): class DeviceInterfacesView(DeviceComponentsView):
child_model = Interface child_model = Interface
table = tables.DeviceInterfaceTable table = tables.DeviceInterfaceTable
filterset = filtersets.InterfaceFilterSet filterset = filtersets.InterfaceFilterSet
template_name = 'dcim/device/interfaces.html' template_name = 'dcim/device/interfaces.html'
tab = ViewTab(
label=_('Interfaces'),
badge=lambda obj: obj.interfaces.count(),
permission='dcim.view_interface'
)
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.vc_interfaces().restrict(request.user, 'view').prefetch_related( return parent.vc_interfaces().restrict(request.user, 'view').prefetch_related(
@ -1661,84 +1787,79 @@ class DeviceInterfacesView(DeviceComponentsView):
) )
@register_model_view(Device, 'frontports', path='front-ports')
class DeviceFrontPortsView(DeviceComponentsView): class DeviceFrontPortsView(DeviceComponentsView):
child_model = FrontPort child_model = FrontPort
table = tables.DeviceFrontPortTable table = tables.DeviceFrontPortTable
filterset = filtersets.FrontPortFilterSet filterset = filtersets.FrontPortFilterSet
template_name = 'dcim/device/frontports.html' template_name = 'dcim/device/frontports.html'
tab = ViewTab(
label=_('Front Ports'),
badge=lambda obj: obj.frontports.count(),
permission='dcim.view_frontport'
)
@register_model_view(Device, 'rearports', path='rear-ports')
class DeviceRearPortsView(DeviceComponentsView): class DeviceRearPortsView(DeviceComponentsView):
child_model = RearPort child_model = RearPort
table = tables.DeviceRearPortTable table = tables.DeviceRearPortTable
filterset = filtersets.RearPortFilterSet filterset = filtersets.RearPortFilterSet
template_name = 'dcim/device/rearports.html' template_name = 'dcim/device/rearports.html'
tab = ViewTab(
label=_('Rear Ports'),
badge=lambda obj: obj.rearports.count(),
permission='dcim.view_rearport'
)
@register_model_view(Device, 'modulebays', path='module-bays')
class DeviceModuleBaysView(DeviceComponentsView): class DeviceModuleBaysView(DeviceComponentsView):
child_model = ModuleBay child_model = ModuleBay
table = tables.DeviceModuleBayTable table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet filterset = filtersets.ModuleBayFilterSet
template_name = 'dcim/device/modulebays.html' template_name = 'dcim/device/modulebays.html'
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.modulebays.count(),
permission='dcim.view_modulebay'
)
@register_model_view(Device, 'devicebays', path='device-bays')
class DeviceDeviceBaysView(DeviceComponentsView): class DeviceDeviceBaysView(DeviceComponentsView):
child_model = DeviceBay child_model = DeviceBay
table = tables.DeviceDeviceBayTable table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet filterset = filtersets.DeviceBayFilterSet
template_name = 'dcim/device/devicebays.html' template_name = 'dcim/device/devicebays.html'
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.devicebays.count(),
permission='dcim.view_devicebay'
)
@register_model_view(Device, 'inventory')
class DeviceInventoryView(DeviceComponentsView): class DeviceInventoryView(DeviceComponentsView):
child_model = InventoryItem child_model = InventoryItem
table = tables.DeviceInventoryItemTable table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet filterset = filtersets.InventoryItemFilterSet
template_name = 'dcim/device/inventory.html' template_name = 'dcim/device/inventory.html'
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventoryitems.count(),
permission='dcim.view_inventoryitem'
)
class DeviceStatusView(generic.ObjectView): @register_model_view(Device, 'configcontext', path='config-context')
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all()
template_name = 'dcim/device/status.html'
def get_extra_context(self, request, instance):
return {
'active_tab': 'status',
}
class DeviceLLDPNeighborsView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all()
template_name = 'dcim/device/lldp_neighbors.html'
def get_extra_context(self, request, instance):
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
'_path'
).exclude(
type__in=NONCONNECTABLE_IFACE_TYPES
)
return {
'interfaces': interfaces,
'active_tab': 'lldp-neighbors',
}
class DeviceConfigView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all()
template_name = 'dcim/device/config.html'
def get_extra_context(self, request, instance):
return {
'active_tab': 'config',
}
class DeviceConfigContextView(ObjectConfigContextView): class DeviceConfigContextView(ObjectConfigContextView):
queryset = Device.objects.annotate_config_context_data() queryset = Device.objects.annotate_config_context_data()
base_template = 'dcim/device/base.html' base_template = 'dcim/device/base.html'
tab = ViewTab(
label=_('Config Context'),
permission='extras.view_configcontext'
)
class DeviceEditView(generic.ObjectEditView): class DeviceEditView(generic.ObjectEditView):
@ -1796,7 +1917,68 @@ class DeviceBulkRenameView(generic.BulkRenameView):
# #
# Devices # Device NAPALM views
#
class NAPALMViewTab(ViewTab):
def render(self, instance):
# Display NAPALM tabs only for devices which meet certain requirements
if not (
instance.status == 'active' and
instance.primary_ip and
instance.platform.napalm_driver
):
return None
return super().render(instance)
@register_model_view(Device, 'status')
class DeviceStatusView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all()
template_name = 'dcim/device/status.html'
tab = NAPALMViewTab(
label=_('Status'),
permission='dcim.napalm_read_device',
)
@register_model_view(Device, 'lldp_neighbors', path='lldp-neighbors')
class DeviceLLDPNeighborsView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all()
template_name = 'dcim/device/lldp_neighbors.html'
tab = NAPALMViewTab(
label=_('LLDP Neighbors'),
permission='dcim.napalm_read_device',
)
def get_extra_context(self, request, instance):
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
'_path'
).exclude(
type__in=NONCONNECTABLE_IFACE_TYPES
)
return {
'interfaces': interfaces,
}
@register_model_view(Device, 'config')
class DeviceConfigView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all()
template_name = 'dcim/device/config.html'
tab = NAPALMViewTab(
label=_('Config'),
permission='dcim.napalm_read_device',
)
#
# Modules
# #
class ModuleListView(generic.ObjectListView): class ModuleListView(generic.ObjectListView):

View File

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

View File

@ -1,6 +1,8 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.views.generic import View from django.views.generic import View
from dcim.models import Site
from utilities.views import register_model_view
from .models import DummyModel from .models import DummyModel
@ -9,3 +11,10 @@ class DummyModelsView(View):
def get(self, request): def get(self, request):
instance_count = DummyModel.objects.count() instance_count = DummyModel.objects.count()
return HttpResponse(f"Instances: {instance_count}") return HttpResponse(f"Instances: {instance_count}")
@register_model_view(Site, 'extra', path='other-stuff')
class ExtraCoreModelView(View):
def get(self, request, pk):
return HttpResponse("Success!")

View File

@ -59,6 +59,17 @@ class PluginTest(TestCase):
response = client.get(url) response = client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_registered_views(self):
# Test URL resolution
url = reverse('dcim:site_extra', kwargs={'pk': 1})
self.assertEqual(url, '/dcim/sites/1/other-stuff/')
# Test GET request
client = Client()
response = client.get(url)
self.assertEqual(response.status_code, 200)
def test_menu(self): def test_menu(self):
""" """
Check menu registration. Check menu registration.

View File

@ -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 extras import views
from netbox.views.generic import ObjectChangeLogView from utilities.urls import get_model_urls
app_name = 'extras' app_name = 'extras'
@ -16,8 +16,7 @@ urlpatterns = [
path('custom-fields/<int:pk>/', views.CustomFieldView.as_view(), name='customfield'), path('custom-fields/<int:pk>/', views.CustomFieldView.as_view(), name='customfield'),
path('custom-fields/<int:pk>/edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'), path('custom-fields/<int:pk>/edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'),
path('custom-fields/<int:pk>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'), path('custom-fields/<int:pk>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'),
path('custom-fields/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='customfield_changelog', path('custom-fields/<int:pk>/', include(get_model_urls('extras', 'customfield'))),
kwargs={'model': models.CustomField}),
# Custom links # Custom links
path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'), path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'),
@ -28,8 +27,7 @@ urlpatterns = [
path('custom-links/<int:pk>/', views.CustomLinkView.as_view(), name='customlink'), path('custom-links/<int:pk>/', views.CustomLinkView.as_view(), name='customlink'),
path('custom-links/<int:pk>/edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'), path('custom-links/<int:pk>/edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'),
path('custom-links/<int:pk>/delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'), path('custom-links/<int:pk>/delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'),
path('custom-links/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='customlink_changelog', path('custom-links/<int:pk>/', include(get_model_urls('extras', 'customlink'))),
kwargs={'model': models.CustomLink}),
# Export templates # Export templates
path('export-templates/', views.ExportTemplateListView.as_view(), name='exporttemplate_list'), path('export-templates/', views.ExportTemplateListView.as_view(), name='exporttemplate_list'),
@ -40,8 +38,7 @@ urlpatterns = [
path('export-templates/<int:pk>/', views.ExportTemplateView.as_view(), name='exporttemplate'), path('export-templates/<int:pk>/', views.ExportTemplateView.as_view(), name='exporttemplate'),
path('export-templates/<int:pk>/edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'), path('export-templates/<int:pk>/edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'),
path('export-templates/<int:pk>/delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'), path('export-templates/<int:pk>/delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'),
path('export-templates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='exporttemplate_changelog', path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
kwargs={'model': models.ExportTemplate}),
# Webhooks # Webhooks
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'), path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
@ -52,8 +49,7 @@ urlpatterns = [
path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'), path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'),
path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'), path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'),
path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'), path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'),
path('webhooks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='webhook_changelog', path('webhooks/<int:pk>/', include(get_model_urls('extras', 'webhook'))),
kwargs={'model': models.Webhook}),
# Tags # Tags
path('tags/', views.TagListView.as_view(), name='tag_list'), path('tags/', views.TagListView.as_view(), name='tag_list'),
@ -64,8 +60,7 @@ urlpatterns = [
path('tags/<int:pk>/', views.TagView.as_view(), name='tag'), path('tags/<int:pk>/', views.TagView.as_view(), name='tag'),
path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'), path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'), path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path('tags/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tag_changelog', path('tags/<int:pk>/', include(get_model_urls('extras', 'tag'))),
kwargs={'model': models.Tag}),
# Config contexts # Config contexts
path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'), path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
@ -75,8 +70,7 @@ urlpatterns = [
path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'), path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
path('config-contexts/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='configcontext_changelog', path('config-contexts/<int:pk>/', include(get_model_urls('extras', 'configcontext'))),
kwargs={'model': models.ConfigContext}),
# Image attachments # Image attachments
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'), path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
@ -91,8 +85,7 @@ urlpatterns = [
path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'), path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'),
path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'), path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'),
path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'), path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
path('journal-entries/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='journalentry_changelog', path('journal-entries/<int:pk>/', include(get_model_urls('extras', 'journalentry'))),
kwargs={'model': models.JournalEntry}),
# Change logging # Change logging
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),

View File

@ -352,7 +352,6 @@ class ObjectConfigContextView(generic.ObjectView):
'source_contexts': source_contexts, 'source_contexts': source_contexts,
'format': format, 'format': format,
'base_template': self.base_template, 'base_template': self.base_template,
'active_tab': 'config-context',
} }

View File

@ -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 . import views
from .models import *
app_name = 'ipam' app_name = 'ipam'
urlpatterns = [ urlpatterns = [
@ -16,8 +15,7 @@ urlpatterns = [
path('asns/<int:pk>/', views.ASNView.as_view(), name='asn'), path('asns/<int:pk>/', views.ASNView.as_view(), name='asn'),
path('asns/<int:pk>/edit/', views.ASNEditView.as_view(), name='asn_edit'), path('asns/<int:pk>/edit/', views.ASNEditView.as_view(), name='asn_edit'),
path('asns/<int:pk>/delete/', views.ASNDeleteView.as_view(), name='asn_delete'), path('asns/<int:pk>/delete/', views.ASNDeleteView.as_view(), name='asn_delete'),
path('asns/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='asn_changelog', kwargs={'model': ASN}), path('asns/<int:pk>/', include(get_model_urls('ipam', 'asn'))),
path('asns/<int:pk>/journal/', ObjectJournalView.as_view(), name='asn_journal', kwargs={'model': ASN}),
# VRFs # VRFs
path('vrfs/', views.VRFListView.as_view(), name='vrf_list'), path('vrfs/', views.VRFListView.as_view(), name='vrf_list'),
@ -28,8 +26,7 @@ urlpatterns = [
path('vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'), path('vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'),
path('vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'), path('vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'),
path('vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'), path('vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'),
path('vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), path('vrfs/<int:pk>/', include(get_model_urls('ipam', 'vrf'))),
path('vrfs/<int:pk>/journal/', ObjectJournalView.as_view(), name='vrf_journal', kwargs={'model': VRF}),
# Route targets # Route targets
path('route-targets/', views.RouteTargetListView.as_view(), name='routetarget_list'), path('route-targets/', views.RouteTargetListView.as_view(), name='routetarget_list'),
@ -40,8 +37,7 @@ urlpatterns = [
path('route-targets/<int:pk>/', views.RouteTargetView.as_view(), name='routetarget'), path('route-targets/<int:pk>/', views.RouteTargetView.as_view(), name='routetarget'),
path('route-targets/<int:pk>/edit/', views.RouteTargetEditView.as_view(), name='routetarget_edit'), path('route-targets/<int:pk>/edit/', views.RouteTargetEditView.as_view(), name='routetarget_edit'),
path('route-targets/<int:pk>/delete/', views.RouteTargetDeleteView.as_view(), name='routetarget_delete'), path('route-targets/<int:pk>/delete/', views.RouteTargetDeleteView.as_view(), name='routetarget_delete'),
path('route-targets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='routetarget_changelog', kwargs={'model': RouteTarget}), path('route-targets/<int:pk>/', include(get_model_urls('ipam', 'routetarget'))),
path('route-targets/<int:pk>/journal/', ObjectJournalView.as_view(), name='routetarget_journal', kwargs={'model': RouteTarget}),
# RIRs # RIRs
path('rirs/', views.RIRListView.as_view(), name='rir_list'), path('rirs/', views.RIRListView.as_view(), name='rir_list'),
@ -52,7 +48,7 @@ urlpatterns = [
path('rirs/<int:pk>/', views.RIRView.as_view(), name='rir'), path('rirs/<int:pk>/', views.RIRView.as_view(), name='rir'),
path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'), path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'),
path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'), path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
path('rirs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), path('rirs/<int:pk>/', include(get_model_urls('ipam', 'rir'))),
# Aggregates # Aggregates
path('aggregates/', views.AggregateListView.as_view(), name='aggregate_list'), path('aggregates/', views.AggregateListView.as_view(), name='aggregate_list'),
@ -61,11 +57,9 @@ urlpatterns = [
path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'), path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
path('aggregates/<int:pk>/prefixes/', views.AggregatePrefixesView.as_view(), name='aggregate_prefixes'),
path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
path('aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), path('aggregates/<int:pk>/', include(get_model_urls('ipam', 'aggregate'))),
path('aggregates/<int:pk>/journal/', ObjectJournalView.as_view(), name='aggregate_journal', kwargs={'model': Aggregate}),
# Roles # Roles
path('roles/', views.RoleListView.as_view(), name='role_list'), path('roles/', views.RoleListView.as_view(), name='role_list'),
@ -76,7 +70,7 @@ urlpatterns = [
path('roles/<int:pk>/', views.RoleView.as_view(), name='role'), path('roles/<int:pk>/', views.RoleView.as_view(), name='role'),
path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'), path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'),
path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'), path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
path('roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), path('roles/<int:pk>/', include(get_model_urls('ipam', 'role'))),
# Prefixes # Prefixes
path('prefixes/', views.PrefixListView.as_view(), name='prefix_list'), path('prefixes/', views.PrefixListView.as_view(), name='prefix_list'),
@ -87,11 +81,7 @@ urlpatterns = [
path('prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'), path('prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'),
path('prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'), path('prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
path('prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), path('prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'),
path('prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), path('prefixes/<int:pk>/', include(get_model_urls('ipam', 'prefix'))),
path('prefixes/<int:pk>/journal/', ObjectJournalView.as_view(), name='prefix_journal', kwargs={'model': Prefix}),
path('prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
path('prefixes/<int:pk>/ip-ranges/', views.PrefixIPRangesView.as_view(), name='prefix_ipranges'),
path('prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP ranges # IP ranges
path('ip-ranges/', views.IPRangeListView.as_view(), name='iprange_list'), path('ip-ranges/', views.IPRangeListView.as_view(), name='iprange_list'),
@ -102,9 +92,7 @@ urlpatterns = [
path('ip-ranges/<int:pk>/', views.IPRangeView.as_view(), name='iprange'), path('ip-ranges/<int:pk>/', views.IPRangeView.as_view(), name='iprange'),
path('ip-ranges/<int:pk>/edit/', views.IPRangeEditView.as_view(), name='iprange_edit'), path('ip-ranges/<int:pk>/edit/', views.IPRangeEditView.as_view(), name='iprange_edit'),
path('ip-ranges/<int:pk>/delete/', views.IPRangeDeleteView.as_view(), name='iprange_delete'), path('ip-ranges/<int:pk>/delete/', views.IPRangeDeleteView.as_view(), name='iprange_delete'),
path('ip-ranges/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='iprange_changelog', kwargs={'model': IPRange}), path('ip-ranges/<int:pk>/', include(get_model_urls('ipam', 'iprange'))),
path('ip-ranges/<int:pk>/journal/', ObjectJournalView.as_view(), name='iprange_journal', kwargs={'model': IPRange}),
path('ip-ranges/<int:pk>/ip-addresses/', views.IPRangeIPAddressesView.as_view(), name='iprange_ipaddresses'),
# IP addresses # IP addresses
path('ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'), path('ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'),
@ -113,12 +101,11 @@ urlpatterns = [
path('ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), 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/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
path('ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), path('ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
path('ip-addresses/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
path('ip-addresses/<int:pk>/journal/', ObjectJournalView.as_view(), name='ipaddress_journal', kwargs={'model': IPAddress}),
path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
path('ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'), path('ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'),
path('ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'), path('ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
path('ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), path('ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
path('ip-addresses/<int:pk>/', include(get_model_urls('ipam', 'ipaddress'))),
# FHRP groups # FHRP groups
path('fhrp-groups/', views.FHRPGroupListView.as_view(), name='fhrpgroup_list'), path('fhrp-groups/', views.FHRPGroupListView.as_view(), name='fhrpgroup_list'),
@ -129,8 +116,7 @@ urlpatterns = [
path('fhrp-groups/<int:pk>/', views.FHRPGroupView.as_view(), name='fhrpgroup'), path('fhrp-groups/<int:pk>/', views.FHRPGroupView.as_view(), name='fhrpgroup'),
path('fhrp-groups/<int:pk>/edit/', views.FHRPGroupEditView.as_view(), name='fhrpgroup_edit'), path('fhrp-groups/<int:pk>/edit/', views.FHRPGroupEditView.as_view(), name='fhrpgroup_edit'),
path('fhrp-groups/<int:pk>/delete/', views.FHRPGroupDeleteView.as_view(), name='fhrpgroup_delete'), path('fhrp-groups/<int:pk>/delete/', views.FHRPGroupDeleteView.as_view(), name='fhrpgroup_delete'),
path('fhrp-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='fhrpgroup_changelog', kwargs={'model': FHRPGroup}), path('fhrp-groups/<int:pk>/', include(get_model_urls('ipam', 'fhrpgroup'))),
path('fhrp-groups/<int:pk>/journal/', ObjectJournalView.as_view(), name='fhrpgroup_journal', kwargs={'model': FHRPGroup}),
# FHRP group assignments # FHRP group assignments
path('fhrp-group-assignments/add/', views.FHRPGroupAssignmentEditView.as_view(), name='fhrpgroupassignment_add'), path('fhrp-group-assignments/add/', views.FHRPGroupAssignmentEditView.as_view(), name='fhrpgroupassignment_add'),
@ -146,7 +132,7 @@ urlpatterns = [
path('vlan-groups/<int:pk>/', views.VLANGroupView.as_view(), name='vlangroup'), path('vlan-groups/<int:pk>/', views.VLANGroupView.as_view(), name='vlangroup'),
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'), path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), path('vlan-groups/<int:pk>/', include(get_model_urls('ipam', 'vlangroup'))),
# VLANs # VLANs
path('vlans/', views.VLANListView.as_view(), name='vlan_list'), path('vlans/', views.VLANListView.as_view(), name='vlan_list'),
@ -155,12 +141,9 @@ urlpatterns = [
path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'), path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
path('vlans/<int:pk>/interfaces/', views.VLANInterfacesView.as_view(), name='vlan_interfaces'),
path('vlans/<int:pk>/vm-interfaces/', views.VLANVMInterfacesView.as_view(), name='vlan_vminterfaces'),
path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'), path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'), path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), path('vlans/<int:pk>/', include(get_model_urls('ipam', 'vlan'))),
path('vlans/<int:pk>/journal/', ObjectJournalView.as_view(), name='vlan_journal', kwargs={'model': VLAN}),
# Service templates # Service templates
path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'), path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'),
@ -171,8 +154,7 @@ urlpatterns = [
path('service-templates/<int:pk>/', views.ServiceTemplateView.as_view(), name='servicetemplate'), path('service-templates/<int:pk>/', views.ServiceTemplateView.as_view(), name='servicetemplate'),
path('service-templates/<int:pk>/edit/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_edit'), path('service-templates/<int:pk>/edit/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_edit'),
path('service-templates/<int:pk>/delete/', views.ServiceTemplateDeleteView.as_view(), name='servicetemplate_delete'), path('service-templates/<int:pk>/delete/', views.ServiceTemplateDeleteView.as_view(), name='servicetemplate_delete'),
path('service-templates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='servicetemplate_changelog', kwargs={'model': ServiceTemplate}), path('service-templates/<int:pk>/', include(get_model_urls('ipam', 'servicetemplate'))),
path('service-templates/<int:pk>/journal/', ObjectJournalView.as_view(), name='servicetemplate_journal', kwargs={'model': ServiceTemplate}),
# Services # Services
path('services/', views.ServiceListView.as_view(), name='service_list'), path('services/', views.ServiceListView.as_view(), name='service_list'),
@ -183,8 +165,7 @@ urlpatterns = [
path('services/<int:pk>/', views.ServiceView.as_view(), name='service'), path('services/<int:pk>/', views.ServiceView.as_view(), name='service'),
path('services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'), path('services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'),
path('services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'), path('services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'),
path('services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), path('services/<int:pk>/', include(get_model_urls('ipam', 'service'))),
path('services/<int:pk>/journal/', ObjectJournalView.as_view(), name='service_journal', kwargs={'model': Service}),
# L2VPN # L2VPN
path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'), path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
@ -195,9 +176,9 @@ urlpatterns = [
path('l2vpns/<int:pk>/', views.L2VPNView.as_view(), name='l2vpn'), path('l2vpns/<int:pk>/', views.L2VPNView.as_view(), name='l2vpn'),
path('l2vpns/<int:pk>/edit/', views.L2VPNEditView.as_view(), name='l2vpn_edit'), path('l2vpns/<int:pk>/edit/', views.L2VPNEditView.as_view(), name='l2vpn_edit'),
path('l2vpns/<int:pk>/delete/', views.L2VPNDeleteView.as_view(), name='l2vpn_delete'), path('l2vpns/<int:pk>/delete/', views.L2VPNDeleteView.as_view(), name='l2vpn_delete'),
path('l2vpns/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='l2vpn_changelog', kwargs={'model': L2VPN}), path('l2vpns/<int:pk>/', include(get_model_urls('ipam', 'l2vpn'))),
path('l2vpns/<int:pk>/journal/', ObjectJournalView.as_view(), name='l2vpn_journal', kwargs={'model': L2VPN}),
# L2VPN terminations
path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'), path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'),
path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'), path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'),
path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'), path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'),
@ -206,6 +187,5 @@ urlpatterns = [
path('l2vpn-terminations/<int:pk>/', views.L2VPNTerminationView.as_view(), name='l2vpntermination'), path('l2vpn-terminations/<int:pk>/', views.L2VPNTerminationView.as_view(), name='l2vpntermination'),
path('l2vpn-terminations/<int:pk>/edit/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_edit'), path('l2vpn-terminations/<int:pk>/edit/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_edit'),
path('l2vpn-terminations/<int:pk>/delete/', views.L2VPNTerminationDeleteView.as_view(), name='l2vpntermination_delete'), path('l2vpn-terminations/<int:pk>/delete/', views.L2VPNTerminationDeleteView.as_view(), name='l2vpntermination_delete'),
path('l2vpn-terminations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='l2vpntermination_changelog', kwargs={'model': L2VPNTermination}), path('l2vpn-terminations/<int:pk>/', include(get_model_urls('ipam', 'l2vpntermination'))),
path('l2vpn-terminations/<int:pk>/journal/', ObjectJournalView.as_view(), name='l2vpntermination_journal', kwargs={'model': L2VPNTermination}),
] ]

View File

@ -3,6 +3,7 @@ from django.db.models import Prefetch
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _
from circuits.models import Provider, Circuit from circuits.models import Provider, Circuit
from circuits.tables import ProviderTable from circuits.tables import ProviderTable
@ -11,6 +12,7 @@ from dcim.models import Interface, Site, Device
from dcim.tables import SiteTable from dcim.tables import SiteTable
from netbox.views import generic from netbox.views import generic
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface, VirtualMachine from virtualization.models import VMInterface, VirtualMachine
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -289,12 +291,18 @@ class AggregateView(generic.ObjectView):
queryset = Aggregate.objects.all() queryset = Aggregate.objects.all()
@register_model_view(Aggregate, 'prefixes')
class AggregatePrefixesView(generic.ObjectChildrenView): class AggregatePrefixesView(generic.ObjectChildrenView):
queryset = Aggregate.objects.all() queryset = Aggregate.objects.all()
child_model = Prefix child_model = Prefix
table = tables.PrefixTable table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet filterset = filtersets.PrefixFilterSet
template_name = 'ipam/aggregate/prefixes.html' 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): def get_children(self, request, parent):
return Prefix.objects.restrict(request.user, 'view').filter( return Prefix.objects.restrict(request.user, 'view').filter(
@ -311,7 +319,6 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
'bulk_querystring': f'within={instance.prefix}', 'bulk_querystring': f'within={instance.prefix}',
'active_tab': 'prefixes',
'first_available_prefix': instance.get_first_available_prefix(), 'first_available_prefix': instance.get_first_available_prefix(),
'show_available': bool(request.GET.get('show_available', 'true') == 'true'), 'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'), 'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
@ -466,12 +473,18 @@ class PrefixView(generic.ObjectView):
} }
@register_model_view(Prefix, 'prefixes')
class PrefixPrefixesView(generic.ObjectChildrenView): class PrefixPrefixesView(generic.ObjectChildrenView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
child_model = Prefix child_model = Prefix
table = tables.PrefixTable table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet filterset = filtersets.PrefixFilterSet
template_name = 'ipam/prefix/prefixes.html' 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): def get_children(self, request, parent):
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related( return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
@ -488,19 +501,24 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&within={instance.prefix}", '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(), 'first_available_prefix': instance.get_first_available_prefix(),
'show_available': bool(request.GET.get('show_available', 'true') == 'true'), 'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'), 'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
} }
@register_model_view(Prefix, 'ipranges', path='ip-ranges')
class PrefixIPRangesView(generic.ObjectChildrenView): class PrefixIPRangesView(generic.ObjectChildrenView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
child_model = IPRange child_model = IPRange
table = tables.IPRangeTable table = tables.IPRangeTable
filterset = filtersets.IPRangeFilterSet filterset = filtersets.IPRangeFilterSet
template_name = 'ipam/prefix/ip_ranges.html' 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): def get_children(self, request, parent):
return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related( return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
@ -510,17 +528,22 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
'active_tab': 'ip-ranges',
'first_available_ip': instance.get_first_available_ip(), 'first_available_ip': instance.get_first_available_ip(),
} }
@register_model_view(Prefix, 'ipaddresses', path='ip-addresses')
class PrefixIPAddressesView(generic.ObjectChildrenView): class PrefixIPAddressesView(generic.ObjectChildrenView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
child_model = IPAddress child_model = IPAddress
table = tables.IPAddressTable table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet filterset = filtersets.IPAddressFilterSet
template_name = 'ipam/prefix/ip_addresses.html' 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): def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group') return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
@ -533,7 +556,6 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
'active_tab': 'ip-addresses',
'first_available_ip': instance.get_first_available_ip(), 'first_available_ip': instance.get_first_available_ip(),
} }
@ -581,21 +603,22 @@ class IPRangeView(generic.ObjectView):
queryset = IPRange.objects.all() queryset = IPRange.objects.all()
@register_model_view(IPRange, 'ipaddresses', path='ip-addresses')
class IPRangeIPAddressesView(generic.ObjectChildrenView): class IPRangeIPAddressesView(generic.ObjectChildrenView):
queryset = IPRange.objects.all() queryset = IPRange.objects.all()
child_model = IPAddress child_model = IPAddress
table = tables.IPAddressTable table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet filterset = filtersets.IPAddressFilterSet
template_name = 'ipam/iprange/ip_addresses.html' 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): def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view') return parent.get_child_ips().restrict(request.user, 'view')
def get_extra_context(self, request, instance):
return {
'active_tab': 'ip-addresses',
}
class IPRangeEditView(generic.ObjectEditView): class IPRangeEditView(generic.ObjectEditView):
queryset = IPRange.objects.all() queryset = IPRange.objects.all()
@ -1000,37 +1023,39 @@ class VLANView(generic.ObjectView):
} }
@register_model_view(VLAN, 'interfaces')
class VLANInterfacesView(generic.ObjectChildrenView): class VLANInterfacesView(generic.ObjectChildrenView):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()
child_model = Interface child_model = Interface
table = tables.VLANDevicesTable table = tables.VLANDevicesTable
filterset = InterfaceFilterSet filterset = InterfaceFilterSet
template_name = 'ipam/vlan/interfaces.html' 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): def get_children(self, request, parent):
return parent.get_interfaces().restrict(request.user, 'view') 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): class VLANVMInterfacesView(generic.ObjectChildrenView):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()
child_model = VMInterface child_model = VMInterface
table = tables.VLANVirtualMachinesTable table = tables.VLANVirtualMachinesTable
filterset = VMInterfaceFilterSet filterset = VMInterfaceFilterSet
template_name = 'ipam/vlan/vminterfaces.html' 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): def get_children(self, request, parent):
return parent.get_vminterfaces().restrict(request.user, 'view') return parent.get_vminterfaces().restrict(request.user, 'view')
def get_extra_context(self, request, instance):
return {
'active_tab': 'vminterfaces',
}
class VLANEditView(generic.ObjectEditView): class VLANEditView(generic.ObjectEditView):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()

View File

@ -13,6 +13,7 @@ from extras.utils import is_taggable, register_features
from netbox.signals import post_clean from netbox.signals import post_clean
from utilities.json import CustomFieldJSONEncoder from utilities.json import CustomFieldJSONEncoder
from utilities.utils import serialize_object from utilities.utils import serialize_object
from utilities.views import register_model_view
__all__ = ( __all__ = (
'ChangeLoggingMixin', 'ChangeLoggingMixin',
@ -292,3 +293,17 @@ def _register_features(sender, **kwargs):
feature for feature, cls in FEATURES_MAP if issubclass(sender, cls) feature for feature, cls in FEATURES_MAP if issubclass(sender, cls)
} }
register_features(sender, features) register_features(sender, features)
# Feature view registration
if issubclass(sender, JournalingMixin):
register_model_view(
sender,
'journal',
kwargs={'model': sender}
)('netbox.views.generic.ObjectJournalView')
if issubclass(sender, ChangeLoggingMixin):
register_model_view(
sender,
'changelog',
kwargs={'model': sender}
)('netbox.views.generic.ObjectChangeLogView')

View File

@ -1,10 +1,12 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext as _
from django.views.generic import View from django.views.generic import View
from extras import forms, tables from extras import forms, tables
from extras.models import * from extras.models import *
from utilities.views import ViewTab
__all__ = ( __all__ = (
'ObjectChangeLogView', '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: The name of the template to extend. If not provided, "{app}/{model}.html" will be used.
""" """
base_template = None base_template = None
tab = ViewTab(
label=_('Changelog'),
permission='extras.view_objectchange'
)
def get(self, request, model, **kwargs): def get(self, request, model, **kwargs):
@ -56,7 +62,7 @@ class ObjectChangeLogView(View):
'object': obj, 'object': obj,
'table': objectchanges_table, 'table': objectchanges_table,
'base_template': self.base_template, 'base_template': self.base_template,
'active_tab': 'changelog', 'tab': self.tab,
}) })
@ -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: The name of the template to extend. If not provided, "{app}/{model}.html" will be used.
""" """
base_template = None base_template = None
tab = ViewTab(
label=_('Journal'),
badge=lambda obj: obj.journal_entries.count(),
permission='extras.view_journalentry'
)
def get(self, request, model, **kwargs): def get(self, request, model, **kwargs):
@ -111,5 +122,5 @@ class ObjectJournalView(View):
'form': form, 'form': form,
'table': journalentry_table, 'table': journalentry_table,
'base_template': self.base_template, 'base_template': self.base_template,
'active_tab': 'journal', 'tab': self.tab,
}) })

View File

@ -5,7 +5,6 @@ from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction from django.db import transaction
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.forms.widgets import HiddenInput
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
@ -38,7 +37,12 @@ class ObjectView(BaseObjectView):
Retrieve a single object for display. Retrieve a single object for display.
Note: If `template_name` is not specified, it will be determined automatically based on the queryset model. Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
Attributes:
tab: A ViewTab instance for the view
""" """
tab = None
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view') return get_permission_for_model(self.queryset.model, 'view')
@ -67,6 +71,7 @@ class ObjectView(BaseObjectView):
return render(request, self.get_template_name(), { return render(request, self.get_template_name(), {
'object': instance, 'object': instance,
'tab': self.tab,
**self.get_extra_context(request, instance), **self.get_extra_context(request, instance),
}) })
@ -141,6 +146,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
'child_model': self.child_model, 'child_model': self.child_model,
'table': table, 'table': table,
'actions': actions, 'actions': actions,
'tab': self.tab,
**self.get_extra_context(request, instance), **self.get_extra_context(request, instance),
}) })

View File

@ -54,113 +54,3 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_tabs %}
{% with tab_name='device-bays' devicebay_count=object.devicebays.count %}
{% if active_tab == tab_name or devicebay_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='module-bays' modulebay_count=object.modulebays.count %}
{% if active_tab == tab_name or modulebay_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='interfaces' interface_count=object.interfaces_count %}
{% if active_tab == tab_name or interface_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='front-ports' frontport_count=object.frontports.count %}
{% if active_tab == tab_name or frontport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='rear-ports' rearport_count=object.rearports.count %}
{% if active_tab == tab_name or rearport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='console-ports' consoleport_count=object.consoleports.count %}
{% if active_tab == tab_name or consoleport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='console-server-ports' consoleserverport_count=object.consoleserverports.count %}
{% if active_tab == tab_name or consoleserverport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='power-ports' powerport_count=object.powerports.count %}
{% if active_tab == tab_name or powerport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='power-outlets' poweroutlet_count=object.poweroutlets.count %}
{% if active_tab == tab_name or poweroutlet_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='inventory-items' inventoryitem_count=object.inventoryitems.count %}
{% if active_tab == tab_name or inventoryitem_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_inventory' pk=object.pk %}">Inventory {% badge inventoryitem_count %}</a>
</li>
{% endif %}
{% endwith %}
{% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %}
{# NAPALM-enabled tabs #}
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'status' %} active{% endif %}" href="{% url 'dcim:device_status' pk=object.pk %}">
Status
</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'lldp-neighbors' %} active{% endif %}" href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">
LLDP Neighbors
</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'config' %} active{% endif %}" href="{% url 'dcim:device_config' pk=object.pk %}">
Configuration
</a>
</li>
{% endif %}
{% if perms.extras.view_configcontext %}
<li role="presentation" class="nav-item">
<a href="{% url 'dcim:device_configcontext' pk=object.pk %}" class="nav-link{% if active_tab == 'config-context' %} active{% endif %}">
Config Context
</a>
</li>
{% endif %}
{% endblock %}

View File

@ -51,85 +51,3 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_tabs %}
{% with tab_name='device-bay-templates' devicebay_count=object.devicebaytemplates.count %}
{% if active_tab == tab_name or devicebay_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='module-bay-templates' modulebay_count=object.modulebaytemplates.count %}
{% if active_tab == tab_name or modulebay_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='interface-templates' interface_count=object.interfacetemplates.count %}
{% if active_tab == tab_name or interface_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='front-port-templates' frontport_count=object.frontporttemplates.count %}
{% if active_tab == tab_name or frontport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='rear-port-templates' rearport_count=object.rearporttemplates.count %}
{% if active_tab == tab_name or rearport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='console-port-templates' consoleport_count=object.consoleporttemplates.count %}
{% if active_tab == tab_name or consoleport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='console-server-port-templates' consoleserverport_count=object.consoleserverporttemplates.count %}
{% if active_tab == tab_name or consoleserverport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='power-port-templates' powerport_count=object.powerporttemplates.count %}
{% if active_tab == tab_name or powerport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='power-outlet-templates' poweroutlet_count=object.poweroutlettemplates.count %}
{% if active_tab == tab_name or poweroutlet_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with tab_name='inventory-item-templates' inventoryitem_count=object.inventoryitemtemplates.count %}
{% if active_tab == tab_name or inventoryitem_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_inventoryitems' pk=object.pk %}">Inventory Items {% badge inventoryitem_count %}</a>
</li>
{% endif %}
{% endwith %}
{% endblock %}

View File

@ -42,61 +42,3 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_tabs %}
{% with interface_count=object.interfacetemplates.count %}
{% if interface_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'interface-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with frontport_count=object.frontporttemplates.count %}
{% if frontport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'front-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with rearport_count=object.rearporttemplates.count %}
{% if rearport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'rear-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with consoleport_count=object.consoleporttemplates.count %}
{% if consoleport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'console-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with consoleserverport_count=object.consoleserverporttemplates.count %}
{% if consoleserverport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'console-server-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with powerport_count=object.powerporttemplates.count %}
{% if powerport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'power-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with poweroutlet_count=object.poweroutlettemplates.count %}
{% if poweroutlet_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'power-outlet-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
</li>
{% endif %}
{% endwith %}
{% endblock %}

View File

@ -4,6 +4,7 @@
{% load helpers %} {% load helpers %}
{% load perms %} {% load perms %}
{% load plugins %} {% load plugins %}
{% load tabs %}
{% comment %} {% comment %}
Blocks: Blocks:
@ -80,37 +81,14 @@ Context:
<ul class="nav nav-tabs px-3"> <ul class="nav nav-tabs px-3">
{# Primary tab #} {# Primary tab #}
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a> <a class="nav-link{% if not tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
</li> </li>
{# Include any additional tabs #} {# Include any extra tabs passed by the view #}
{% block extra_tabs %}{% endblock %} {% block extra_tabs %}{% endblock %}
{# Object journal #} {# Include tabs for registered model views #}
{% if perms.extras.view_journalentry %} {% model_view_tabs object %}
{% with journal_viewname=object|viewname:'journal' %}
{% url journal_viewname pk=object.pk as journal_url %}
{% if journal_url %}
<li role="presentation" class="nav-item">
<a href="{{ journal_url }}" class="nav-link{% if active_tab == 'journal'%} active{% endif %}">
Journal {% badge object.journal_entries.count %}
</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
{# Object changelog #}
{% if perms.extras.view_objectchange %}
{% with changelog_viewname=object|viewname:'changelog' %}
{% url changelog_viewname pk=object.pk as changelog_url %}
{% if changelog_url %}
<li role="presentation" class="nav-item">
<a href="{{ changelog_url }}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
</ul> </ul>
{% endblock tabs %} {% endblock tabs %}

View File

@ -6,13 +6,3 @@
{{ block.super }} {{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.rir.pk }}">{{ object.rir }}</a></li> <li class="breadcrumb-item"><a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.rir.pk }}">{{ object.rir }}</a></li>
{% endblock %} {% endblock %}
{% block extra_tabs %}
{% if perms.ipam.view_prefix %}
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'prefixes' %} active{% endif %}" href="{% url 'ipam:aggregate_prefixes' pk=object.pk %}">
Prefixes {% badge object.get_child_prefixes.count %}
</a>
</li>
{% endif %}
{% endblock %}

View File

@ -8,13 +8,3 @@
<li class="breadcrumb-item"><a href="{% url 'ipam:iprange_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li> <li class="breadcrumb-item"><a href="{% url 'ipam:iprange_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_tabs %}
{% if perms.ipam.view_ipaddress %}
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'ip-addresses' %} active{% endif %}" href="{% url 'ipam:iprange_ipaddresses' pk=object.pk %}">
IP Addresses {% badge object.get_child_ips.count %}
</a>
</li>
{% endif %}
{% endblock %}

View File

@ -8,21 +8,3 @@
<li class="breadcrumb-item"><a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li> <li class="breadcrumb-item"><a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_tabs %}
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'prefixes' %} active{% endif %}" href="{% url 'ipam:prefix_prefixes' pk=object.pk %}">
Child Prefixes {% badge object.get_child_prefixes.count %}
</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'ip-ranges' %} active{% endif %}" href="{% url 'ipam:prefix_ipranges' pk=object.pk %}">
Child Ranges {% badge object.get_child_ranges.count %}
</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'ip-addresses' %} active{% endif %}" href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">
IP Addresses {% badge object.get_child_ips.count %}
</a>
</li>
{% endblock %}

View File

@ -13,27 +13,3 @@
<li class="breadcrumb-item"><a href="{% url 'ipam:vlan_list' %}?group_id={{ object.group.pk }}">{{ object.group }}</a></li> <li class="breadcrumb-item"><a href="{% url 'ipam:vlan_list' %}?group_id={{ object.group.pk }}">{{ object.group }}</a></li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{% url 'ipam:vlan' pk=object.pk %}">VLAN</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link{% if active_tab == 'interfaces' %} active{% endif %}" href="{% url 'ipam:vlan_interfaces' pk=object.pk %}">Device Interfaces {% badge object.get_interfaces.count %}</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link{% if active_tab == 'vminterfaces' %} active{% endif %}" href="{% url 'ipam:vlan_vminterfaces' pk=object.pk %}">VM Interfaces {% badge object.get_vminterfaces.count %}</a>
</li>
{% if perms.extras.view_journalentry %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if active_tab == 'journal' %} active{% endif %}" href="{% url 'ipam:vlan_journal' pk=object.pk %}">Journal</a>
</li>
{% endif %}
{% if perms.extras.view_objectchange %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if active_tab == 'changelog' %} active{% endif %}" href="{% url 'ipam:vlan_changelog' pk=object.pk %}">Change Log</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -24,20 +24,3 @@
</a> </a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_tabs %}
{% with virtualmachine_count=object.virtual_machines.count %}
<li role="presentation" class="nav-item">
<a href="{% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="nav-link{% if active_tab == 'virtual-machines' %} active{% endif %}">
Virtual Machines {% badge virtualmachine_count %}
</a>
</li>
{% endwith %}
{% with device_count=object.devices.count %}
<li role="presentation" class="nav-item">
<a href="{% url 'virtualization:cluster_devices' pk=object.pk %}" class="nav-link{% if active_tab == 'devices' %} active{% endif %}">
Devices {% badge device_count %}
</a>
</li>
{% endwith %}
{% endblock %}

View File

@ -21,18 +21,3 @@
</a> </a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_tabs %}
{% with interface_count=object.interfaces.count %}
{% if interface_count %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if active_tab == 'interfaces' %} active{% endif %}" href="{% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
</li>
{% endif %}
{% endwith %}
{% if perms.extras.view_configcontext %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if active_tab == 'config-context' %} active{% endif %}" href="{% url 'virtualization:virtualmachine_configcontext' pk=object.pk %}">Config Context</a>
</li>
{% endif %}
{% endblock %}

View File

@ -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 . import views
from .models import *
app_name = 'tenancy' app_name = 'tenancy'
urlpatterns = [ urlpatterns = [
@ -16,7 +15,7 @@ urlpatterns = [
path('tenant-groups/<int:pk>/', views.TenantGroupView.as_view(), name='tenantgroup'), path('tenant-groups/<int:pk>/', views.TenantGroupView.as_view(), name='tenantgroup'),
path('tenant-groups/<int:pk>/edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), path('tenant-groups/<int:pk>/edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
path('tenant-groups/<int:pk>/delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'), path('tenant-groups/<int:pk>/delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'),
path('tenant-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}), path('tenant-groups/<int:pk>/', include(get_model_urls('tenancy', 'tenantgroup'))),
# Tenants # Tenants
path('tenants/', views.TenantListView.as_view(), name='tenant_list'), path('tenants/', views.TenantListView.as_view(), name='tenant_list'),
@ -27,8 +26,7 @@ urlpatterns = [
path('tenants/<int:pk>/', views.TenantView.as_view(), name='tenant'), path('tenants/<int:pk>/', views.TenantView.as_view(), name='tenant'),
path('tenants/<int:pk>/edit/', views.TenantEditView.as_view(), name='tenant_edit'), path('tenants/<int:pk>/edit/', views.TenantEditView.as_view(), name='tenant_edit'),
path('tenants/<int:pk>/delete/', views.TenantDeleteView.as_view(), name='tenant_delete'), path('tenants/<int:pk>/delete/', views.TenantDeleteView.as_view(), name='tenant_delete'),
path('tenants/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), path('tenants/<int:pk>/', include(get_model_urls('tenancy', 'tenant'))),
path('tenants/<int:pk>/journal/', ObjectJournalView.as_view(), name='tenant_journal', kwargs={'model': Tenant}),
# Contact groups # Contact groups
path('contact-groups/', views.ContactGroupListView.as_view(), name='contactgroup_list'), path('contact-groups/', views.ContactGroupListView.as_view(), name='contactgroup_list'),
@ -39,7 +37,7 @@ urlpatterns = [
path('contact-groups/<int:pk>/', views.ContactGroupView.as_view(), name='contactgroup'), path('contact-groups/<int:pk>/', views.ContactGroupView.as_view(), name='contactgroup'),
path('contact-groups/<int:pk>/edit/', views.ContactGroupEditView.as_view(), name='contactgroup_edit'), path('contact-groups/<int:pk>/edit/', views.ContactGroupEditView.as_view(), name='contactgroup_edit'),
path('contact-groups/<int:pk>/delete/', views.ContactGroupDeleteView.as_view(), name='contactgroup_delete'), path('contact-groups/<int:pk>/delete/', views.ContactGroupDeleteView.as_view(), name='contactgroup_delete'),
path('contact-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='contactgroup_changelog', kwargs={'model': ContactGroup}), path('contact-groups/<int:pk>/', include(get_model_urls('tenancy', 'contactgroup'))),
# Contact roles # Contact roles
path('contact-roles/', views.ContactRoleListView.as_view(), name='contactrole_list'), path('contact-roles/', views.ContactRoleListView.as_view(), name='contactrole_list'),
@ -50,7 +48,7 @@ urlpatterns = [
path('contact-roles/<int:pk>/', views.ContactRoleView.as_view(), name='contactrole'), path('contact-roles/<int:pk>/', views.ContactRoleView.as_view(), name='contactrole'),
path('contact-roles/<int:pk>/edit/', views.ContactRoleEditView.as_view(), name='contactrole_edit'), path('contact-roles/<int:pk>/edit/', views.ContactRoleEditView.as_view(), name='contactrole_edit'),
path('contact-roles/<int:pk>/delete/', views.ContactRoleDeleteView.as_view(), name='contactrole_delete'), path('contact-roles/<int:pk>/delete/', views.ContactRoleDeleteView.as_view(), name='contactrole_delete'),
path('contact-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='contactrole_changelog', kwargs={'model': ContactRole}), path('contact-roles/<int:pk>/', include(get_model_urls('tenancy', 'contactrole'))),
# Contacts # Contacts
path('contacts/', views.ContactListView.as_view(), name='contact_list'), path('contacts/', views.ContactListView.as_view(), name='contact_list'),
@ -61,8 +59,7 @@ urlpatterns = [
path('contacts/<int:pk>/', views.ContactView.as_view(), name='contact'), path('contacts/<int:pk>/', views.ContactView.as_view(), name='contact'),
path('contacts/<int:pk>/edit/', views.ContactEditView.as_view(), name='contact_edit'), path('contacts/<int:pk>/edit/', views.ContactEditView.as_view(), name='contact_edit'),
path('contacts/<int:pk>/delete/', views.ContactDeleteView.as_view(), name='contact_delete'), path('contacts/<int:pk>/delete/', views.ContactDeleteView.as_view(), name='contact_delete'),
path('contacts/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='contact_changelog', kwargs={'model': Contact}), path('contacts/<int:pk>/', include(get_model_urls('tenancy', 'contact'))),
path('contacts/<int:pk>/journal/', ObjectJournalView.as_view(), name='contact_journal', kwargs={'model': Contact}),
# Contact assignments # Contact assignments
path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'), path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'),

View File

@ -0,0 +1,7 @@
{% for tab in tabs %}
<li role="presentation" class="nav-item">
<a href="{{ tab.url }}" class="nav-link{% if tab.is_active %} active{% endif %}">
{{ tab.label }} {% badge tab.badge %}
</a>
</li>
{% endfor %}

View File

@ -0,0 +1,48 @@
from django import template
from django.urls import reverse
from django.utils.module_loading import import_string
from extras.registry import registry
register = template.Library()
#
# Object detail view tabs
#
@register.inclusion_tag('tabs/model_view_tabs.html', takes_context=True)
def model_view_tabs(context, instance):
app_label = instance._meta.app_label
model_name = instance._meta.model_name
user = context['request'].user
tabs = []
# Retrieve registered views for this model
try:
views = registry['views'][app_label][model_name]
except KeyError:
# No views have been registered for this model
views = []
# Compile a list of tabs to be displayed in the UI
for config in views:
view = import_string(config['view']) if type(config['view']) is str else config['view']
if tab := getattr(view, 'tab', None):
if tab.permission and not user.has_perm(tab.permission):
continue
if attrs := tab.render(instance):
viewname = f"{app_label}:{model_name}_{config['name']}"
active_tab = context.get('tab')
tabs.append({
'name': config['name'],
'url': reverse(viewname, args=[instance.pk]),
'label': attrs['label'],
'badge': attrs['badge'],
'is_active': active_tab and active_tab == tab,
})
return {
'tabs': tabs,
}

39
netbox/utilities/urls.py Normal file
View File

@ -0,0 +1,39 @@
from django.urls import path
from django.utils.module_loading import import_string
from django.views.generic import View
from extras.registry import registry
def get_model_urls(app_label, model_name):
"""
Return a list of URL paths for detail views registered to the given model.
Args:
app_label: App/plugin name
model_name: Model name
"""
paths = []
# Retrieve registered views for this model
try:
views = registry['views'][app_label][model_name]
except KeyError:
# No views have been registered for this model
views = []
for config in views:
# Import the view class or function
if type(config['view']) is str:
view_ = import_string(config['view'])
else:
view_ = config['view']
if issubclass(view_, View):
view_ = view_.as_view()
# Create a path to the view
paths.append(
path(f"{config['path']}/", view_, name=f"{model_name}_{config['name']}", kwargs=config['kwargs'])
)
return paths

View File

@ -3,8 +3,17 @@ from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse from django.urls import reverse
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
from extras.registry import registry
from .permissions import resolve_permission from .permissions import resolve_permission
__all__ = (
'ContentTypePermissionRequiredMixin',
'GetReturnURLMixin',
'ObjectPermissionRequiredMixin',
'ViewTab',
'register_model_view',
)
# #
# View Mixins # View Mixins
@ -122,3 +131,75 @@ class GetReturnURLMixin:
# If all else fails, return home. Ideally this should never happen. # If all else fails, return home. Ideally this should never happen.
return reverse('home') return reverse('home')
class ViewTab:
"""
ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for
a particular object.
Args:
label: Human-friendly text
badge: A static value or callable to display alongside the label (optional). If a callable is used, it must accept a single
argument representing the object being viewed.
permission: The permission required to display the tab (optional).
"""
def __init__(self, label, badge=None, permission=None):
self.label = label
self.badge = badge
self.permission = permission
def render(self, instance):
"""Return the attributes needed to render a tab in HTML."""
badge_value = self._get_badge_value(instance)
if self.badge and not badge_value:
return None
return {
'label': self.label,
'badge': badge_value,
}
def _get_badge_value(self, instance):
if not self.badge:
return None
if callable(self.badge):
return self.badge(instance)
return self.badge
def register_model_view(model, name, path=None, kwargs=None):
"""
This decorator can be used to "attach" a view to any model in NetBox. This is typically used to inject
additional tabs within a model's detail view. For example, to add a custom tab to NetBox's dcim.Site model:
@netbox_model_view(Site, 'myview', path='my-custom-view')
class MyView(ObjectView):
...
This will automatically create a URL path for MyView at `/dcim/sites/<id>/my-custom-view/` which can be
resolved using the view name `dcim:site_myview'.
Args:
model: The Django model class with which this view will be associated.
name: The string used to form the view's name for URL resolution (e.g. via `reverse()`). This will be appended
to the name of the base view for the model using an underscore.
path: The URL path by which the view can be reached (optional). If not provided, `name` will be used.
kwargs: A dictionary of keyword arguments for the view to include when registering its URL path (optional).
"""
def _wrapper(cls):
app_label = model._meta.app_label
model_name = model._meta.model_name
if model_name not in registry['views'][app_label]:
registry['views'][app_label][model_name] = []
registry['views'][app_label][model_name].append({
'name': name,
'view': cls,
'path': path or name,
'kwargs': kwargs or {},
})
return cls
return _wrapper

View File

@ -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 . import views
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
app_name = 'virtualization' app_name = 'virtualization'
urlpatterns = [ urlpatterns = [
@ -16,7 +15,7 @@ urlpatterns = [
path('cluster-types/<int:pk>/', views.ClusterTypeView.as_view(), name='clustertype'), path('cluster-types/<int:pk>/', views.ClusterTypeView.as_view(), name='clustertype'),
path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'), path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'),
path('cluster-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), path('cluster-types/<int:pk>/', include(get_model_urls('virtualization', 'clustertype'))),
# Cluster groups # Cluster groups
path('cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'), path('cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
@ -27,7 +26,7 @@ urlpatterns = [
path('cluster-groups/<int:pk>/', views.ClusterGroupView.as_view(), name='clustergroup'), path('cluster-groups/<int:pk>/', views.ClusterGroupView.as_view(), name='clustergroup'),
path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'), path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'),
path('cluster-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), path('cluster-groups/<int:pk>/', include(get_model_urls('virtualization', 'clustergroup'))),
# Clusters # Clusters
path('clusters/', views.ClusterListView.as_view(), name='cluster_list'), path('clusters/', views.ClusterListView.as_view(), name='cluster_list'),
@ -36,14 +35,11 @@ urlpatterns = [
path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
path('clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'), path('clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'),
path('clusters/<int:pk>/devices/', views.ClusterDevicesView.as_view(), name='cluster_devices'),
path('clusters/<int:pk>/virtual-machines/', views.ClusterVirtualMachinesView.as_view(), name='cluster_virtualmachines'),
path('clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'), path('clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'),
path('clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), path('clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'),
path('clusters/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
path('clusters/<int:pk>/journal/', ObjectJournalView.as_view(), name='cluster_journal', kwargs={'model': Cluster}),
path('clusters/<int:pk>/devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), path('clusters/<int:pk>/devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
path('clusters/<int:pk>/devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'), path('clusters/<int:pk>/devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
path('clusters/<int:pk>/', include(get_model_urls('virtualization', 'cluster'))),
# Virtual machines # Virtual machines
path('virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'), path('virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'),
@ -52,12 +48,9 @@ urlpatterns = [
path('virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), 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/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'),
path('virtual-machines/<int:pk>/', views.VirtualMachineView.as_view(), name='virtualmachine'), path('virtual-machines/<int:pk>/', views.VirtualMachineView.as_view(), name='virtualmachine'),
path('virtual-machines/<int:pk>/interfaces/', views.VirtualMachineInterfacesView.as_view(), name='virtualmachine_interfaces'),
path('virtual-machines/<int:pk>/edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), path('virtual-machines/<int:pk>/edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
path('virtual-machines/<int:pk>/delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), path('virtual-machines/<int:pk>/delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
path('virtual-machines/<int:pk>/config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), path('virtual-machines/<int:pk>/', include(get_model_urls('virtualization', 'virtualmachine'))),
path('virtual-machines/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}),
path('virtual-machines/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualmachine_journal', kwargs={'model': VirtualMachine}),
# VM interfaces # VM interfaces
path('interfaces/', views.VMInterfaceListView.as_view(), name='vminterface_list'), path('interfaces/', views.VMInterfaceListView.as_view(), name='vminterface_list'),
@ -69,7 +62,7 @@ urlpatterns = [
path('interfaces/<int:pk>/', views.VMInterfaceView.as_view(), name='vminterface'), path('interfaces/<int:pk>/', views.VMInterfaceView.as_view(), name='vminterface'),
path('interfaces/<int:pk>/edit/', views.VMInterfaceEditView.as_view(), name='vminterface_edit'), path('interfaces/<int:pk>/edit/', views.VMInterfaceEditView.as_view(), name='vminterface_edit'),
path('interfaces/<int:pk>/delete/', views.VMInterfaceDeleteView.as_view(), name='vminterface_delete'), path('interfaces/<int:pk>/delete/', views.VMInterfaceDeleteView.as_view(), name='vminterface_delete'),
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vminterface_changelog', kwargs={'model': VMInterface}), path('interfaces/<int:pk>/', include(get_model_urls('virtualization', 'vminterface'))),
path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_vminterface'), path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_vminterface'),
] ]

View File

@ -3,6 +3,7 @@ from django.db import transaction
from django.db.models import Prefetch from django.db.models import Prefetch
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _
from dcim.filtersets import DeviceFilterSet from dcim.filtersets import DeviceFilterSet
from dcim.models import Device from dcim.models import Device
@ -12,6 +13,7 @@ from ipam.models import IPAddress, Service
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
from netbox.views import generic from netbox.views import generic
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -161,37 +163,39 @@ class ClusterView(generic.ObjectView):
queryset = Cluster.objects.all() queryset = Cluster.objects.all()
@register_model_view(Cluster, 'virtualmachines', path='virtual-machines')
class ClusterVirtualMachinesView(generic.ObjectChildrenView): class ClusterVirtualMachinesView(generic.ObjectChildrenView):
queryset = Cluster.objects.all() queryset = Cluster.objects.all()
child_model = VirtualMachine child_model = VirtualMachine
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
filterset = filtersets.VirtualMachineFilterSet filterset = filtersets.VirtualMachineFilterSet
template_name = 'virtualization/cluster/virtual_machines.html' 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): def get_children(self, request, parent):
return VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=parent) return VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=parent)
def get_extra_context(self, request, instance):
return {
'active_tab': 'virtual-machines',
}
@register_model_view(Cluster, 'devices')
class ClusterDevicesView(generic.ObjectChildrenView): class ClusterDevicesView(generic.ObjectChildrenView):
queryset = Cluster.objects.all() queryset = Cluster.objects.all()
child_model = Device child_model = Device
table = DeviceTable table = DeviceTable
filterset = DeviceFilterSet filterset = DeviceFilterSet
template_name = 'virtualization/cluster/devices.html' 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): def get_children(self, request, parent):
return Device.objects.restrict(request.user, 'view').filter(cluster=parent) return Device.objects.restrict(request.user, 'view').filter(cluster=parent)
def get_extra_context(self, request, instance):
return {
'active_tab': 'devices',
}
class ClusterEditView(generic.ObjectEditView): class ClusterEditView(generic.ObjectEditView):
queryset = Cluster.objects.all() queryset = Cluster.objects.all()
@ -344,12 +348,18 @@ class VirtualMachineView(generic.ObjectView):
} }
@register_model_view(VirtualMachine, 'interfaces')
class VirtualMachineInterfacesView(generic.ObjectChildrenView): class VirtualMachineInterfacesView(generic.ObjectChildrenView):
queryset = VirtualMachine.objects.all() queryset = VirtualMachine.objects.all()
child_model = VMInterface child_model = VMInterface
table = tables.VirtualMachineVMInterfaceTable table = tables.VirtualMachineVMInterfaceTable
filterset = filtersets.VMInterfaceFilterSet filterset = filtersets.VMInterfaceFilterSet
template_name = 'virtualization/virtualmachine/interfaces.html' 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): def get_children(self, request, parent):
return parent.interfaces.restrict(request.user, 'view').prefetch_related( return parent.interfaces.restrict(request.user, 'view').prefetch_related(
@ -357,15 +367,15 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
'tags', 'tags',
) )
def get_extra_context(self, request, instance):
return {
'active_tab': 'interfaces',
}
@register_model_view(VirtualMachine, 'configcontext', path='config-context')
class VirtualMachineConfigContextView(ObjectConfigContextView): class VirtualMachineConfigContextView(ObjectConfigContextView):
queryset = VirtualMachine.objects.annotate_config_context_data() queryset = VirtualMachine.objects.annotate_config_context_data()
base_template = 'virtualization/virtualmachine.html' base_template = 'virtualization/virtualmachine.html'
tab = ViewTab(
label=_('Config Context'),
permission='extras.view_configcontext'
)
class VirtualMachineEditView(generic.ObjectEditView): class VirtualMachineEditView(generic.ObjectEditView):

View File

@ -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 . import views
from .models import *
app_name = 'wireless' app_name = 'wireless'
urlpatterns = ( urlpatterns = (
@ -16,7 +15,7 @@ urlpatterns = (
path('wireless-lan-groups/<int:pk>/', views.WirelessLANGroupView.as_view(), name='wirelesslangroup'), path('wireless-lan-groups/<int:pk>/', views.WirelessLANGroupView.as_view(), name='wirelesslangroup'),
path('wireless-lan-groups/<int:pk>/edit/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_edit'), path('wireless-lan-groups/<int:pk>/edit/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_edit'),
path('wireless-lan-groups/<int:pk>/delete/', views.WirelessLANGroupDeleteView.as_view(), name='wirelesslangroup_delete'), path('wireless-lan-groups/<int:pk>/delete/', views.WirelessLANGroupDeleteView.as_view(), name='wirelesslangroup_delete'),
path('wireless-lan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslangroup_changelog', kwargs={'model': WirelessLANGroup}), path('wireless-lan-groups/<int:pk>/', include(get_model_urls('wireless', 'wirelesslangroup'))),
# Wireless LANs # Wireless LANs
path('wireless-lans/', views.WirelessLANListView.as_view(), name='wirelesslan_list'), path('wireless-lans/', views.WirelessLANListView.as_view(), name='wirelesslan_list'),
@ -27,8 +26,7 @@ urlpatterns = (
path('wireless-lans/<int:pk>/', views.WirelessLANView.as_view(), name='wirelesslan'), path('wireless-lans/<int:pk>/', views.WirelessLANView.as_view(), name='wirelesslan'),
path('wireless-lans/<int:pk>/edit/', views.WirelessLANEditView.as_view(), name='wirelesslan_edit'), path('wireless-lans/<int:pk>/edit/', views.WirelessLANEditView.as_view(), name='wirelesslan_edit'),
path('wireless-lans/<int:pk>/delete/', views.WirelessLANDeleteView.as_view(), name='wirelesslan_delete'), path('wireless-lans/<int:pk>/delete/', views.WirelessLANDeleteView.as_view(), name='wirelesslan_delete'),
path('wireless-lans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslan_changelog', kwargs={'model': WirelessLAN}), path('wireless-lans/<int:pk>/', include(get_model_urls('wireless', 'wirelesslan'))),
path('wireless-lans/<int:pk>/journal/', ObjectJournalView.as_view(), name='wirelesslan_journal', kwargs={'model': WirelessLAN}),
# Wireless links # Wireless links
path('wireless-links/', views.WirelessLinkListView.as_view(), name='wirelesslink_list'), path('wireless-links/', views.WirelessLinkListView.as_view(), name='wirelesslink_list'),
@ -39,7 +37,6 @@ urlpatterns = (
path('wireless-links/<int:pk>/', views.WirelessLinkView.as_view(), name='wirelesslink'), path('wireless-links/<int:pk>/', views.WirelessLinkView.as_view(), name='wirelesslink'),
path('wireless-links/<int:pk>/edit/', views.WirelessLinkEditView.as_view(), name='wirelesslink_edit'), path('wireless-links/<int:pk>/edit/', views.WirelessLinkEditView.as_view(), name='wirelesslink_edit'),
path('wireless-links/<int:pk>/delete/', views.WirelessLinkDeleteView.as_view(), name='wirelesslink_delete'), path('wireless-links/<int:pk>/delete/', views.WirelessLinkDeleteView.as_view(), name='wirelesslink_delete'),
path('wireless-links/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslink_changelog', kwargs={'model': WirelessLink}), path('wireless-links/<int:pk>/', include(get_model_urls('wireless', 'wirelesslink'))),
path('wireless-links/<int:pk>/journal/', ObjectJournalView.as_view(), name='wirelesslink_journal', kwargs={'model': WirelessLink}),
) )