1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Wrap model detail views with register_model_view()

This commit is contained in:
jeremystretch
2022-10-07 11:36:14 -04:00
parent 4c999daacd
commit bfe26b46a6
19 changed files with 320 additions and 383 deletions

View File

@ -105,16 +105,6 @@ urlpatterns = [
path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'), path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
path('device-types/<int:pk>/console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'),
path('device-types/<int:pk>/console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'),
path('device-types/<int:pk>/power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'),
path('device-types/<int:pk>/power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'),
path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
path('device-types/<int:pk>/module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'),
path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
path('device-types/<int:pk>/inventory-items/', views.DeviceTypeInventoryItemsView.as_view(), name='devicetype_inventoryitems'),
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
path('device-types/<int:pk>/', include(get_model_urls('dcim', 'devicetype'))), path('device-types/<int:pk>/', include(get_model_urls('dcim', 'devicetype'))),
@ -126,13 +116,6 @@ urlpatterns = [
path('module-types/edit/', views.ModuleTypeBulkEditView.as_view(), name='moduletype_bulk_edit'), path('module-types/edit/', views.ModuleTypeBulkEditView.as_view(), name='moduletype_bulk_edit'),
path('module-types/delete/', views.ModuleTypeBulkDeleteView.as_view(), name='moduletype_bulk_delete'), path('module-types/delete/', views.ModuleTypeBulkDeleteView.as_view(), name='moduletype_bulk_delete'),
path('module-types/<int:pk>/', views.ModuleTypeView.as_view(), name='moduletype'), path('module-types/<int:pk>/', views.ModuleTypeView.as_view(), name='moduletype'),
path('module-types/<int:pk>/console-ports/', views.ModuleTypeConsolePortsView.as_view(), name='moduletype_consoleports'),
path('module-types/<int:pk>/console-server-ports/', views.ModuleTypeConsoleServerPortsView.as_view(), name='moduletype_consoleserverports'),
path('module-types/<int:pk>/power-ports/', views.ModuleTypePowerPortsView.as_view(), name='moduletype_powerports'),
path('module-types/<int:pk>/power-outlets/', views.ModuleTypePowerOutletsView.as_view(), name='moduletype_poweroutlets'),
path('module-types/<int:pk>/interfaces/', views.ModuleTypeInterfacesView.as_view(), name='moduletype_interfaces'),
path('module-types/<int:pk>/front-ports/', views.ModuleTypeFrontPortsView.as_view(), name='moduletype_frontports'),
path('module-types/<int:pk>/rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'),
path('module-types/<int:pk>/edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'), path('module-types/<int:pk>/edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'),
path('module-types/<int:pk>/delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'), path('module-types/<int:pk>/delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'),
path('module-types/<int:pk>/', include(get_model_urls('dcim', 'moduletype'))), path('module-types/<int:pk>/', include(get_model_urls('dcim', 'moduletype'))),
@ -250,17 +233,7 @@ urlpatterns = [
path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'), path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'), path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'), path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
path('devices/<int:pk>/console-ports/', views.DeviceConsolePortsView.as_view(), name='device_consoleports'),
path('devices/<int:pk>/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>/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>/status/', views.DeviceStatusView.as_view(), name='device_status'), 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>/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>/config/', views.DeviceConfigView.as_view(), name='device_config'),

View File

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

View File

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

View File

@ -57,7 +57,6 @@ urlpatterns = [
path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'), path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
path('aggregates/<int:pk>/prefixes/', views.AggregatePrefixesView.as_view(), name='aggregate_prefixes'),
path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
path('aggregates/<int:pk>/', include(get_model_urls('ipam', 'aggregate'))), path('aggregates/<int:pk>/', include(get_model_urls('ipam', 'aggregate'))),
@ -82,9 +81,6 @@ urlpatterns = [
path('prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'), path('prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'),
path('prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'), path('prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
path('prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), path('prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'),
path('prefixes/<int:pk>/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'))), path('prefixes/<int:pk>/', include(get_model_urls('ipam', 'prefix'))),
# IP ranges # IP ranges
@ -96,7 +92,6 @@ urlpatterns = [
path('ip-ranges/<int:pk>/', views.IPRangeView.as_view(), name='iprange'), path('ip-ranges/<int:pk>/', views.IPRangeView.as_view(), name='iprange'),
path('ip-ranges/<int:pk>/edit/', views.IPRangeEditView.as_view(), name='iprange_edit'), path('ip-ranges/<int:pk>/edit/', views.IPRangeEditView.as_view(), name='iprange_edit'),
path('ip-ranges/<int:pk>/delete/', views.IPRangeDeleteView.as_view(), name='iprange_delete'), path('ip-ranges/<int:pk>/delete/', views.IPRangeDeleteView.as_view(), name='iprange_delete'),
path('ip-ranges/<int:pk>/ip-addresses/', views.IPRangeIPAddressesView.as_view(), name='iprange_ipaddresses'),
path('ip-ranges/<int:pk>/', include(get_model_urls('ipam', 'iprange'))), path('ip-ranges/<int:pk>/', include(get_model_urls('ipam', 'iprange'))),
# IP addresses # IP addresses

View File

@ -3,6 +3,7 @@ from django.db.models import Prefetch
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _
from circuits.models import Provider, Circuit from circuits.models import Provider, Circuit
from circuits.tables import ProviderTable from circuits.tables import ProviderTable
@ -11,6 +12,7 @@ from dcim.models import Interface, Site, Device
from dcim.tables import SiteTable from dcim.tables import SiteTable
from netbox.views import generic from netbox.views import generic
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface, VirtualMachine from virtualization.models import VMInterface, VirtualMachine
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -289,12 +291,18 @@ class AggregateView(generic.ObjectView):
queryset = Aggregate.objects.all() queryset = Aggregate.objects.all()
@register_model_view(Aggregate, 'prefixes')
class AggregatePrefixesView(generic.ObjectChildrenView): class AggregatePrefixesView(generic.ObjectChildrenView):
queryset = Aggregate.objects.all() queryset = Aggregate.objects.all()
child_model = Prefix child_model = Prefix
table = tables.PrefixTable table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet filterset = filtersets.PrefixFilterSet
template_name = 'ipam/aggregate/prefixes.html' template_name = 'ipam/aggregate/prefixes.html'
tab = ViewTab(
label=_('Prefixes'),
badge=lambda x: x.get_child_prefixes().count(),
permission='ipam.view_prefix'
)
def get_children(self, request, parent): def get_children(self, request, parent):
return Prefix.objects.restrict(request.user, 'view').filter( return Prefix.objects.restrict(request.user, 'view').filter(
@ -466,12 +474,18 @@ class PrefixView(generic.ObjectView):
} }
@register_model_view(Prefix, 'prefixes')
class PrefixPrefixesView(generic.ObjectChildrenView): class PrefixPrefixesView(generic.ObjectChildrenView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
child_model = Prefix child_model = Prefix
table = tables.PrefixTable table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet filterset = filtersets.PrefixFilterSet
template_name = 'ipam/prefix/prefixes.html' template_name = 'ipam/prefix/prefixes.html'
tab = ViewTab(
label=_('Child Prefixes'),
badge=lambda x: x.get_child_prefixes().count(),
permission='ipam.view_prefix'
)
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related( return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
@ -495,12 +509,18 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
} }
@register_model_view(Prefix, 'ipranges', path='ip-ranges')
class PrefixIPRangesView(generic.ObjectChildrenView): class PrefixIPRangesView(generic.ObjectChildrenView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
child_model = IPRange child_model = IPRange
table = tables.IPRangeTable table = tables.IPRangeTable
filterset = filtersets.IPRangeFilterSet filterset = filtersets.IPRangeFilterSet
template_name = 'ipam/prefix/ip_ranges.html' template_name = 'ipam/prefix/ip_ranges.html'
tab = ViewTab(
label=_('Child Ranges'),
badge=lambda x: x.get_child_ranges().count(),
permission='ipam.view_iprange'
)
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related( return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
@ -510,17 +530,23 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
'active_tab': 'ip-ranges', 'active_tab': 'ipranges',
'first_available_ip': instance.get_first_available_ip(), 'first_available_ip': instance.get_first_available_ip(),
} }
@register_model_view(Prefix, 'ipaddresses', path='ip-addresses')
class PrefixIPAddressesView(generic.ObjectChildrenView): class PrefixIPAddressesView(generic.ObjectChildrenView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
child_model = IPAddress child_model = IPAddress
table = tables.IPAddressTable table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet filterset = filtersets.IPAddressFilterSet
template_name = 'ipam/prefix/ip_addresses.html' template_name = 'ipam/prefix/ip_addresses.html'
tab = ViewTab(
label=_('IP Addresses'),
badge=lambda x: x.get_child_ips().count(),
permission='ipam.view_ipaddress'
)
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group') return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
@ -533,7 +559,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
'active_tab': 'ip-addresses', 'active_tab': 'ipaddresses',
'first_available_ip': instance.get_first_available_ip(), 'first_available_ip': instance.get_first_available_ip(),
} }
@ -581,19 +607,25 @@ class IPRangeView(generic.ObjectView):
queryset = IPRange.objects.all() queryset = IPRange.objects.all()
@register_model_view(IPRange, 'ipaddresses', path='ip-addresses')
class IPRangeIPAddressesView(generic.ObjectChildrenView): class IPRangeIPAddressesView(generic.ObjectChildrenView):
queryset = IPRange.objects.all() queryset = IPRange.objects.all()
child_model = IPAddress child_model = IPAddress
table = tables.IPAddressTable table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet filterset = filtersets.IPAddressFilterSet
template_name = 'ipam/iprange/ip_addresses.html' template_name = 'ipam/iprange/ip_addresses.html'
tab = ViewTab(
label=_('IP Addresses'),
badge=lambda x: x.get_child_ips().count(),
permission='ipam.view_ipaddress'
)
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view') return parent.get_child_ips().restrict(request.user, 'view')
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
'active_tab': 'ip-addresses', 'active_tab': 'ipaddresses',
} }

View File

@ -13,7 +13,7 @@ from extras.utils import is_taggable, register_features
from netbox.signals import post_clean from netbox.signals import post_clean
from utilities.json import CustomFieldJSONEncoder from utilities.json import CustomFieldJSONEncoder
from utilities.utils import serialize_object from utilities.utils import serialize_object
from utilities.views import ViewTab, register_model_view from utilities.views import register_model_view
__all__ = ( __all__ = (
'ChangeLoggingMixin', 'ChangeLoggingMixin',
@ -299,13 +299,11 @@ def _register_features(sender, **kwargs):
register_model_view( register_model_view(
sender, sender,
'journal', 'journal',
'netbox.views.generic.ObjectJournalView',
kwargs={'model': sender} kwargs={'model': sender}
) )('netbox.views.generic.ObjectJournalView')
if issubclass(sender, ChangeLoggingMixin): if issubclass(sender, ChangeLoggingMixin):
register_model_view( register_model_view(
sender, sender,
'changelog', 'changelog',
'netbox.views.generic.ObjectChangeLogView',
kwargs={'model': sender} kwargs={'model': sender}
) )('netbox.views.generic.ObjectChangeLogView')

View File

@ -56,87 +56,6 @@
{% endblock %} {% endblock %}
{% block extra_tabs %} {% 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 %} {% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %}
{# NAPALM-enabled tabs #} {# NAPALM-enabled tabs #}
<li role="presentation" class="nav-item"> <li role="presentation" class="nav-item">
@ -155,12 +74,4 @@
</a> </a>
</li> </li>
{% endif %} {% 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 %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
from django import template from django import template
from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse from django.urls import reverse
from django.utils.module_loading import import_string
from extras.registry import registry from extras.registry import registry
@ -26,23 +26,27 @@ def model_view_tabs(context, instance):
views = [] views = []
# Compile a list of tabs to be displayed in the UI # Compile a list of tabs to be displayed in the UI
for view in views: for config in views:
if view['tab_label'] and (not view['tab_permission'] or user.has_perm(view['tab_permission'])): view = import_string(config['view']) if type(config['view']) is str else config['view']
if tab := getattr(view, 'tab', None):
if tab.permission and not user.has_perm(tab.permission):
continue
# Determine the value of the tab's badge (if any) # Determine the value of the tab's badge (if any)
if view['tab_badge'] and callable(view['tab_badge']): if tab.badge and callable(tab.badge):
badge_value = view['tab_badge'](instance) badge_value = tab.badge(instance)
elif view['tab_badge']:
badge_value = view['tab_badge']
else: else:
badge_value = None badge_value = tab.badge
if not tab.always_display and not badge_value:
continue
tabs.append({ tabs.append({
'name': view['name'], 'name': config['name'],
'url': reverse(f"{app_label}:{model_name}_{view['name']}", args=[instance.pk]), 'url': reverse(f"{app_label}:{model_name}_{config['name']}", args=[instance.pk]),
'label': view['tab_label'], 'label': tab.label,
'badge_value': badge_value, 'badge_value': badge_value,
'is_active': context.get('active_tab') == view['name'], 'is_active': context.get('active_tab') == config['name'],
}) })
return { return {

View File

@ -30,9 +30,10 @@ def get_model_urls(app_label, model_name):
view_ = config['view'] view_ = config['view']
if issubclass(view_, View): if issubclass(view_, View):
view_ = view_.as_view() view_ = view_.as_view()
# Create a path to the view # Create a path to the view
paths.append( paths.append(
path(f"{config['name']}/", view_, name=f"{model_name}_{config['name']}", kwargs=config['kwargs']) path(f"{config['path']}/", view_, name=f"{model_name}_{config['name']}", kwargs=config['kwargs'])
) )
return paths return paths

View File

@ -142,24 +142,39 @@ class ViewTab:
self.always_display = always_display self.always_display = always_display
def register_model_view(model, name, view, kwargs=None): def register_model_view(model, name, path=None, kwargs=None):
""" """
Register a subview for a core model. This decorator can be used to "attach" a view to any model in NetBox. This is typically used to inject
additional tabs within a model's detail view. For example, to add a custom tab to NetBox's dcim.Site model:
@netbox_model_view(Site, 'myview', path='my-custom-view')
class MyView(ObjectView):
...
This will automatically create a URL path for MyView at `/dcim/sites/<id>/my-custom-view/` which can be
resolved using the view name `dcim:site_myview'.
Args: Args:
model: The Django model class with which this view will be associated model: The Django model class with which this view will be associated.
name: The name to register when creating a URL path name: The string used to form the view's name for URL resolution (e.g. via `reverse()`). This will be appended
view: A class-based or function view, or the dotted path to it (e.g. 'myplugin.views.FooView') to the name of the base view for the model using an underscore.
kwargs: A dictionary of keyword arguments to send to the view (optional) path: The URL path by which the view can be reached (optional). If not provided, `name` will be used.
kwargs: A dictionary of keyword arguments for the view to include when registering its URL path (optional)
""" """
app_label = model._meta.app_label def _wrapper(cls):
model_name = model._meta.model_name app_label = model._meta.app_label
model_name = model._meta.model_name
if model_name not in registry['views'][app_label]: if model_name not in registry['views'][app_label]:
registry['views'][app_label][model_name] = [] registry['views'][app_label][model_name] = []
registry['views'][app_label][model_name].append({ registry['views'][app_label][model_name].append({
'name': name, 'name': name,
'view': view, 'view': cls,
'kwargs': kwargs or {}, 'path': path or name,
}) 'kwargs': kwargs or {},
})
return cls
return _wrapper

View File

@ -35,8 +35,6 @@ urlpatterns = [
path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'), path('clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'), path('clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
path('clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'), path('clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'),
path('clusters/<int:pk>/devices/', views.ClusterDevicesView.as_view(), name='cluster_devices'),
path('clusters/<int:pk>/virtual-machines/', views.ClusterVirtualMachinesView.as_view(), name='cluster_virtualmachines'),
path('clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'), path('clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'),
path('clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'), path('clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'),
path('clusters/<int:pk>/devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'), path('clusters/<int:pk>/devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
@ -50,10 +48,8 @@ urlpatterns = [
path('virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'), path('virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'),
path('virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'), path('virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'),
path('virtual-machines/<int:pk>/', views.VirtualMachineView.as_view(), name='virtualmachine'), path('virtual-machines/<int:pk>/', views.VirtualMachineView.as_view(), name='virtualmachine'),
path('virtual-machines/<int:pk>/interfaces/', views.VirtualMachineInterfacesView.as_view(), name='virtualmachine_interfaces'),
path('virtual-machines/<int:pk>/edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), path('virtual-machines/<int:pk>/edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
path('virtual-machines/<int:pk>/delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), path('virtual-machines/<int:pk>/delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
path('virtual-machines/<int:pk>/config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'),
path('virtual-machines/<int:pk>/', include(get_model_urls('virtualization', 'virtualmachine'))), path('virtual-machines/<int:pk>/', include(get_model_urls('virtualization', 'virtualmachine'))),
# VM interfaces # VM interfaces

View File

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