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
### 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:
* `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
* [#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
* [#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 netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from utilities.urls import get_model_urls
from . import views
from .models import *
from .models import CircuitTermination
app_name = 'circuits'
urlpatterns = [
@ -17,8 +17,7 @@ urlpatterns = [
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>/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>/journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
# Provider networks
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}),
path('provider-networks/<int:pk>/journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}),
path('provider-networks/<int:pk>/', include(get_model_urls('circuits', 'providernetwork'))),
# Circuit types
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
path('circuit-types/<int:pk>/', include(get_model_urls('circuits', 'circuittype'))),
# Circuits
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>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
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>/', include(get_model_urls('circuits', 'circuit'))),
# Circuit terminations
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 .models import *
from .models import (
ConsolePort, ConsoleServerPort, FrontPort, Interface, PowerFeed, PowerPort, PowerOutlet, RearPort,
)
app_name = 'dcim'
urlpatterns = [
@ -16,7 +18,7 @@ urlpatterns = [
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>/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
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}),
path('site-groups/<int:pk>/', include(get_model_urls('dcim', 'sitegroup'))),
# Sites
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>/edit/', views.SiteEditView.as_view(), name='site_edit'),
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>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
path('sites/<int:pk>/', include(get_model_urls('dcim', 'site'))),
# Locations
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>/edit/', views.LocationEditView.as_view(), name='location_edit'),
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
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
path('rack-roles/<int:pk>/', include(get_model_urls('dcim', 'rackrole'))),
# Rack reservations
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
path('rack-reservations/<int:pk>/journal/', ObjectJournalView.as_view(), name='rackreservation_journal', kwargs={'model': RackReservation}),
path('rack-reservations/<int:pk>/', include(get_model_urls('dcim', 'rackreservation'))),
# Racks
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>/edit/', views.RackEditView.as_view(), name='rack_edit'),
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>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
path('racks/<int:pk>/', include(get_model_urls('dcim', 'rack'))),
# Manufacturers
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>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
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
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/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>/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>/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>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
path('device-types/<int:pk>/', include(get_model_urls('dcim', 'devicetype'))),
# Module types
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/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>/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>/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>/journal/', ObjectJournalView.as_view(), name='moduletype_journal', kwargs={'model': ModuleType}),
path('module-types/<int:pk>/', include(get_model_urls('dcim', 'moduletype'))),
# Console port templates
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
path('device-roles/<int:pk>/', include(get_model_urls('dcim', 'devicerole'))),
# Platforms
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>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
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
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>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
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>/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'),
path('devices/<int:pk>/', include(get_model_urls('dcim', 'device'))),
# Modules
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>/edit/', views.ModuleEditView.as_view(), name='module_edit'),
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>/journal/', ObjectJournalView.as_view(), name='module_journal', kwargs={'model': Module}),
path('modules/<int:pk>/', include(get_model_urls('dcim', 'module'))),
# Console ports
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>/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>/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>/', include(get_model_urls('dcim', 'consoleport'))),
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
# Console server ports
@ -308,8 +272,8 @@ urlpatterns = [
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>/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>/', include(get_model_urls('dcim', 'consoleserverport'))),
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
# Power ports
@ -323,8 +287,8 @@ urlpatterns = [
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>/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>/', include(get_model_urls('dcim', 'powerport'))),
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
# Power outlets
@ -338,8 +302,8 @@ urlpatterns = [
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>/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>/', include(get_model_urls('dcim', 'poweroutlet'))),
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
# Interfaces
@ -353,8 +317,8 @@ urlpatterns = [
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>/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>/', include(get_model_urls('dcim', 'interface'))),
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
# Front ports
@ -368,8 +332,8 @@ urlpatterns = [
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>/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>/', include(get_model_urls('dcim', 'frontport'))),
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
# Rear ports
@ -383,8 +347,8 @@ urlpatterns = [
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>/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>/', include(get_model_urls('dcim', 'rearport'))),
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Module bays
@ -397,7 +361,7 @@ urlpatterns = [
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>/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'),
# Device bays
@ -410,9 +374,9 @@ urlpatterns = [
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>/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>/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'),
# Inventory items
@ -425,10 +389,10 @@ urlpatterns = [
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>/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'),
# Device roles
# Inventory item roles
path('inventory-item-roles/', views.InventoryItemRoleListView.as_view(), name='inventoryitemrole_list'),
path('inventory-item-roles/add/', views.InventoryItemRoleEditView.as_view(), name='inventoryitemrole_add'),
path('inventory-item-roles/import/', views.InventoryItemRoleBulkImportView.as_view(), name='inventoryitemrole_import'),
@ -437,7 +401,7 @@ urlpatterns = [
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>/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
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>/edit/', views.CableEditView.as_view(), name='cable_edit'),
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>/journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}),
path('cables/<int:pk>/', include(get_model_urls('dcim', 'cable'))),
# Console/power/interface connections (read-only)
path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
@ -465,9 +428,8 @@ urlpatterns = [
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>/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>/', include(get_model_urls('dcim', 'virtualchassis'))),
path('virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
# Power panels
@ -479,8 +441,7 @@ urlpatterns = [
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>/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>/journal/', ObjectJournalView.as_view(), name='powerpanel_journal', kwargs={'model': PowerPanel}),
path('power-panels/<int:pk>/', include(get_model_urls('dcim', 'powerpanel'))),
# Power feeds
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}),
path('power-feeds/<int:pk>/', include(get_model_urls('dcim', 'powerfeed'))),
]

View File

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

View File

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

View File

@ -1,6 +1,8 @@
from django.http import HttpResponse
from django.views.generic import View
from dcim.models import Site
from utilities.views import register_model_view
from .models import DummyModel
@ -9,3 +11,10 @@ class DummyModelsView(View):
def get(self, request):
instance_count = DummyModel.objects.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)
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):
"""
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 netbox.views.generic import ObjectChangeLogView
from extras import views
from utilities.urls import get_model_urls
app_name = 'extras'
@ -16,8 +16,7 @@ urlpatterns = [
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>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'),
path('custom-fields/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='customfield_changelog',
kwargs={'model': models.CustomField}),
path('custom-fields/<int:pk>/', include(get_model_urls('extras', 'customfield'))),
# Custom links
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='customlink_changelog',
kwargs={'model': models.CustomLink}),
path('custom-links/<int:pk>/', include(get_model_urls('extras', 'customlink'))),
# Export templates
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
kwargs={'model': models.ExportTemplate}),
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
# Webhooks
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>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'),
path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'),
path('webhooks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='webhook_changelog',
kwargs={'model': models.Webhook}),
path('webhooks/<int:pk>/', include(get_model_urls('extras', 'webhook'))),
# Tags
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>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path('tags/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tag_changelog',
kwargs={'model': models.Tag}),
path('tags/<int:pk>/', include(get_model_urls('extras', 'tag'))),
# Config contexts
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='configcontext_changelog',
kwargs={'model': models.ConfigContext}),
path('config-contexts/<int:pk>/', include(get_model_urls('extras', 'configcontext'))),
# Image attachments
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='journalentry_changelog',
kwargs={'model': models.JournalEntry}),
path('journal-entries/<int:pk>/', include(get_model_urls('extras', 'journalentry'))),
# Change logging
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),

View File

@ -352,7 +352,6 @@ class ObjectConfigContextView(generic.ObjectView):
'source_contexts': source_contexts,
'format': format,
'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 .models import *
app_name = 'ipam'
urlpatterns = [
@ -16,8 +15,7 @@ urlpatterns = [
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>/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>/journal/', ObjectJournalView.as_view(), name='asn_journal', kwargs={'model': ASN}),
path('asns/<int:pk>/', include(get_model_urls('ipam', 'asn'))),
# VRFs
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>/edit/', views.VRFEditView.as_view(), name='vrf_edit'),
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>/journal/', ObjectJournalView.as_view(), name='vrf_journal', kwargs={'model': VRF}),
path('vrfs/<int:pk>/', include(get_model_urls('ipam', 'vrf'))),
# Route targets
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='routetarget_changelog', kwargs={'model': RouteTarget}),
path('route-targets/<int:pk>/journal/', ObjectJournalView.as_view(), name='routetarget_journal', kwargs={'model': RouteTarget}),
path('route-targets/<int:pk>/', include(get_model_urls('ipam', 'routetarget'))),
# RIRs
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>/edit/', views.RIREditView.as_view(), name='rir_edit'),
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
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/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
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>/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>/journal/', ObjectJournalView.as_view(), name='aggregate_journal', kwargs={'model': Aggregate}),
path('aggregates/<int:pk>/', include(get_model_urls('ipam', 'aggregate'))),
# Roles
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>/edit/', views.RoleEditView.as_view(), name='role_edit'),
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
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>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
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>/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'),
path('prefixes/<int:pk>/', include(get_model_urls('ipam', 'prefix'))),
# IP ranges
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='iprange_changelog', kwargs={'model': 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'),
path('ip-ranges/<int:pk>/', include(get_model_urls('ipam', 'iprange'))),
# IP addresses
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/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
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/<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>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
path('ip-addresses/<int:pk>/', include(get_model_urls('ipam', 'ipaddress'))),
# FHRP groups
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='fhrpgroup_changelog', kwargs={'model': FHRPGroup}),
path('fhrp-groups/<int:pk>/journal/', ObjectJournalView.as_view(), name='fhrpgroup_journal', kwargs={'model': FHRPGroup}),
path('fhrp-groups/<int:pk>/', include(get_model_urls('ipam', 'fhrpgroup'))),
# FHRP group assignments
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
path('vlan-groups/<int:pk>/', include(get_model_urls('ipam', 'vlangroup'))),
# VLANs
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/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
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>/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>/journal/', ObjectJournalView.as_view(), name='vlan_journal', kwargs={'model': VLAN}),
path('vlans/<int:pk>/', include(get_model_urls('ipam', 'vlan'))),
# Service templates
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='servicetemplate_changelog', kwargs={'model': ServiceTemplate}),
path('service-templates/<int:pk>/journal/', ObjectJournalView.as_view(), name='servicetemplate_journal', kwargs={'model': ServiceTemplate}),
path('service-templates/<int:pk>/', include(get_model_urls('ipam', 'servicetemplate'))),
# Services
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>/edit/', views.ServiceEditView.as_view(), name='service_edit'),
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>/journal/', ObjectJournalView.as_view(), name='service_journal', kwargs={'model': Service}),
path('services/<int:pk>/', include(get_model_urls('ipam', 'service'))),
# L2VPN
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>/edit/', views.L2VPNEditView.as_view(), name='l2vpn_edit'),
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>/journal/', ObjectJournalView.as_view(), name='l2vpn_journal', kwargs={'model': L2VPN}),
path('l2vpns/<int:pk>/', include(get_model_urls('ipam', 'l2vpn'))),
# L2VPN terminations
path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'),
path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'),
path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'),
@ -206,6 +187,5 @@ urlpatterns = [
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>/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>/journal/', ObjectJournalView.as_view(), name='l2vpntermination_journal', kwargs={'model': L2VPNTermination}),
path('l2vpn-terminations/<int:pk>/', include(get_model_urls('ipam', 'l2vpntermination'))),
]

View File

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

View File

@ -13,6 +13,7 @@ from extras.utils import is_taggable, register_features
from netbox.signals import post_clean
from utilities.json import CustomFieldJSONEncoder
from utilities.utils import serialize_object
from utilities.views import register_model_view
__all__ = (
'ChangeLoggingMixin',
@ -292,3 +293,17 @@ def _register_features(sender, **kwargs):
feature for feature, cls in FEATURES_MAP if issubclass(sender, cls)
}
register_features(sender, features)
# Feature view registration
if issubclass(sender, JournalingMixin):
register_model_view(
sender,
'journal',
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.db.models import Q
from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext as _
from django.views.generic import View
from extras import forms, tables
from extras.models import *
from utilities.views import ViewTab
__all__ = (
'ObjectChangeLogView',
@ -23,6 +25,10 @@ class ObjectChangeLogView(View):
base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used.
"""
base_template = None
tab = ViewTab(
label=_('Changelog'),
permission='extras.view_objectchange'
)
def get(self, request, model, **kwargs):
@ -56,7 +62,7 @@ class ObjectChangeLogView(View):
'object': obj,
'table': objectchanges_table,
'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 = None
tab = ViewTab(
label=_('Journal'),
badge=lambda obj: obj.journal_entries.count(),
permission='extras.view_journalentry'
)
def get(self, request, model, **kwargs):
@ -111,5 +122,5 @@ class ObjectJournalView(View):
'form': form,
'table': journalentry_table,
'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.db import transaction
from django.db.models import ProtectedError
from django.forms.widgets import HiddenInput
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.html import escape
@ -38,7 +37,12 @@ class ObjectView(BaseObjectView):
Retrieve a single object for display.
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):
return get_permission_for_model(self.queryset.model, 'view')
@ -67,6 +71,7 @@ class ObjectView(BaseObjectView):
return render(request, self.get_template_name(), {
'object': instance,
'tab': self.tab,
**self.get_extra_context(request, instance),
})
@ -141,6 +146,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
'child_model': self.child_model,
'table': table,
'actions': actions,
'tab': self.tab,
**self.get_extra_context(request, instance),
})

View File

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

View File

@ -6,13 +6,3 @@
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.rir.pk }}">{{ object.rir }}</a></li>
{% 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>
{% endif %}
{% 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>
{% endif %}
{% 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>
{% endif %}
{% 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>
{% endif %}
{% 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>
{% endif %}
{% 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 .models import *
app_name = 'tenancy'
urlpatterns = [
@ -16,7 +15,7 @@ urlpatterns = [
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>/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
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>/edit/', views.TenantEditView.as_view(), name='tenant_edit'),
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>/journal/', ObjectJournalView.as_view(), name='tenant_journal', kwargs={'model': Tenant}),
path('tenants/<int:pk>/', include(get_model_urls('tenancy', 'tenant'))),
# Contact groups
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='contactgroup_changelog', kwargs={'model': ContactGroup}),
path('contact-groups/<int:pk>/', include(get_model_urls('tenancy', 'contactgroup'))),
# Contact roles
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='contactrole_changelog', kwargs={'model': ContactRole}),
path('contact-roles/<int:pk>/', include(get_model_urls('tenancy', 'contactrole'))),
# Contacts
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>/edit/', views.ContactEditView.as_view(), name='contact_edit'),
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>/journal/', ObjectJournalView.as_view(), name='contact_journal', kwargs={'model': Contact}),
path('contacts/<int:pk>/', include(get_model_urls('tenancy', 'contact'))),
# Contact assignments
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.exceptions import NoReverseMatch
from extras.registry import registry
from .permissions import resolve_permission
__all__ = (
'ContentTypePermissionRequiredMixin',
'GetReturnURLMixin',
'ObjectPermissionRequiredMixin',
'ViewTab',
'register_model_view',
)
#
# View Mixins
@ -122,3 +131,75 @@ class GetReturnURLMixin:
# If all else fails, return home. Ideally this should never happen.
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 .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
app_name = 'virtualization'
urlpatterns = [
@ -16,7 +15,7 @@ urlpatterns = [
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>/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
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),
path('cluster-groups/<int:pk>/', include(get_model_urls('virtualization', 'clustergroup'))),
# Clusters
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/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
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>/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/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
path('clusters/<int:pk>/', include(get_model_urls('virtualization', 'cluster'))),
# Virtual machines
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/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>/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>/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>/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}),
path('virtual-machines/<int:pk>/', include(get_model_urls('virtualization', 'virtualmachine'))),
# VM interfaces
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>/edit/', views.VMInterfaceEditView.as_view(), name='vminterface_edit'),
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'),
]

View File

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

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 .models import *
app_name = 'wireless'
urlpatterns = (
@ -16,7 +15,7 @@ urlpatterns = (
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>/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
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslan_changelog', kwargs={'model': WirelessLAN}),
path('wireless-lans/<int:pk>/journal/', ObjectJournalView.as_view(), name='wirelesslan_journal', kwargs={'model': WirelessLAN}),
path('wireless-lans/<int:pk>/', include(get_model_urls('wireless', 'wirelesslan'))),
# Wireless links
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>/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>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslink_changelog', kwargs={'model': WirelessLink}),
path('wireless-links/<int:pk>/journal/', ObjectJournalView.as_view(), name='wirelesslink_journal', kwargs={'model': WirelessLink}),
path('wireless-links/<int:pk>/', include(get_model_urls('wireless', 'wirelesslink'))),
)