Просмотр исходного кода

Add dedicated views for organizational models

Jeremy Stretch 4 лет назад
Родитель
Сommit
b7e44a744d

+ 1 - 1
netbox/circuits/models.py

@@ -175,7 +175,7 @@ class CircuitType(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
+        return reverse('circuits:circuittype', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (

+ 1 - 0
netbox/circuits/urls.py

@@ -38,6 +38,7 @@ urlpatterns = [
     path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
     path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
     path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
     path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
     path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
     path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
+    path('circuit-types/<int:pk>/', views.CircuitTypeView.as_view(), name='circuittype'),
     path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
     path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
     path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
     path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
     path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
     path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),

+ 17 - 0
netbox/circuits/views.py

@@ -147,6 +147,23 @@ class CircuitTypeListView(generic.ObjectListView):
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
 
 
+class CircuitTypeView(generic.ObjectView):
+    queryset = CircuitType.objects.all()
+
+    def get_extra_context(self, request, instance):
+        circuits = Circuit.objects.restrict(request.user, 'view').filter(
+            type=instance
+        )
+
+        circuits_table = tables.CircuitTable(circuits)
+        circuits_table.columns.hide('type')
+        paginate_table(circuits_table, request)
+
+        return {
+            'circuits_table': circuits_table,
+        }
+
+
 class CircuitTypeEditView(generic.ObjectEditView):
 class CircuitTypeEditView(generic.ObjectEditView):
     queryset = CircuitType.objects.all()
     queryset = CircuitType.objects.all()
     model_form = forms.CircuitTypeForm
     model_form = forms.CircuitTypeForm

+ 5 - 2
netbox/dcim/models/devices.py

@@ -65,7 +65,7 @@ class Manufacturer(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
+        return reverse('dcim:manufacturer', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (
@@ -375,6 +375,9 @@ class DeviceRole(OrganizationalModel):
     def __str__(self):
     def __str__(self):
         return self.name
         return self.name
 
 
+    def get_absolute_url(self):
+        return reverse('dcim:devicerole', args=[self.pk])
+
     def to_csv(self):
     def to_csv(self):
         return (
         return (
             self.name,
             self.name,
@@ -436,7 +439,7 @@ class Platform(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
+        return reverse('dcim:platform', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (

+ 1 - 1
netbox/dcim/models/racks.py

@@ -67,7 +67,7 @@ class RackRole(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)
+        return reverse('dcim:rackrole', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (

+ 6 - 0
netbox/dcim/tables/devices.py

@@ -50,6 +50,9 @@ __all__ = (
 
 
 class DeviceRoleTable(BaseTable):
 class DeviceRoleTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
+    name = tables.Column(
+        linkify=True
+    )
     device_count = LinkedCountColumn(
     device_count = LinkedCountColumn(
         viewname='dcim:device_list',
         viewname='dcim:device_list',
         url_params={'role': 'slug'},
         url_params={'role': 'slug'},
@@ -76,6 +79,9 @@ class DeviceRoleTable(BaseTable):
 
 
 class PlatformTable(BaseTable):
 class PlatformTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
+    name = tables.Column(
+        linkify=True
+    )
     device_count = LinkedCountColumn(
     device_count = LinkedCountColumn(
         viewname='dcim:device_list',
         viewname='dcim:device_list',
         url_params={'platform': 'slug'},
         url_params={'platform': 'slug'},

+ 4 - 0
netbox/dcim/urls.py

@@ -57,6 +57,7 @@ urlpatterns = [
     path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
     path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
     path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'),
     path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'),
     path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
     path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
+    path('rack-roles/<int:pk>/', views.RackRoleView.as_view(), name='rackrole'),
     path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
     path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
     path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
     path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
     path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
     path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
@@ -93,6 +94,7 @@ urlpatterns = [
     path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
     path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
     path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'),
     path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'),
     path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
     path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
+    path('manufacturers/<int:pk>/', views.ManufacturerView.as_view(), name='manufacturer'),
     path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
     path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
     path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
     path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
     path('manufacturers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
     path('manufacturers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
@@ -179,6 +181,7 @@ urlpatterns = [
     path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
     path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
     path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'),
     path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'),
     path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
     path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
+    path('device-roles/<int:pk>/', views.DeviceRoleView.as_view(), name='devicerole'),
     path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
     path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
     path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
     path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
     path('device-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
     path('device-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
@@ -189,6 +192,7 @@ urlpatterns = [
     path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
     path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
     path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'),
     path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'),
     path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
     path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
+    path('platforms/<int:pk>/', views.PlatformView.as_view(), name='platform'),
     path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
     path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
     path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
     path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
     path('platforms/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
     path('platforms/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),

+ 69 - 0
netbox/dcim/views.py

@@ -20,6 +20,7 @@ from secrets.models import Secret
 from utilities.forms import ConfirmationForm
 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.tables import paginate_table
 from utilities.utils import csv_format, count_related
 from utilities.utils import csv_format, count_related
 from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
 from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
@@ -341,6 +342,23 @@ class RackRoleListView(generic.ObjectListView):
     table = tables.RackRoleTable
     table = tables.RackRoleTable
 
 
 
 
+class RackRoleView(generic.ObjectView):
+    queryset = RackRole.objects.all()
+
+    def get_extra_context(self, request, instance):
+        racks = Rack.objects.restrict(request.user, 'view').filter(
+            role=instance
+        )
+
+        racks_table = tables.RackTable(racks)
+        racks_table.columns.hide('role')
+        paginate_table(racks_table, request)
+
+        return {
+            'racks_table': racks_table,
+        }
+
+
 class RackRoleEditView(generic.ObjectEditView):
 class RackRoleEditView(generic.ObjectEditView):
     queryset = RackRole.objects.all()
     queryset = RackRole.objects.all()
     model_form = forms.RackRoleForm
     model_form = forms.RackRoleForm
@@ -567,6 +585,23 @@ class ManufacturerListView(generic.ObjectListView):
     table = tables.ManufacturerTable
     table = tables.ManufacturerTable
 
 
 
 
+class ManufacturerView(generic.ObjectView):
+    queryset = Manufacturer.objects.all()
+
+    def get_extra_context(self, request, instance):
+        devicetypes = DeviceType.objects.restrict(request.user, 'view').filter(
+            manufacturer=instance
+        )
+
+        devicetypes_table = tables.DeviceTypeTable(devicetypes)
+        devicetypes_table.columns.hide('manufacturer')
+        paginate_table(devicetypes_table, request)
+
+        return {
+            'devicetypes_table': devicetypes_table,
+        }
+
+
 class ManufacturerEditView(generic.ObjectEditView):
 class ManufacturerEditView(generic.ObjectEditView):
     queryset = Manufacturer.objects.all()
     queryset = Manufacturer.objects.all()
     model_form = forms.ManufacturerForm
     model_form = forms.ManufacturerForm
@@ -1017,6 +1052,23 @@ class DeviceRoleListView(generic.ObjectListView):
     table = tables.DeviceRoleTable
     table = tables.DeviceRoleTable
 
 
 
 
+class DeviceRoleView(generic.ObjectView):
+    queryset = DeviceRole.objects.all()
+
+    def get_extra_context(self, request, instance):
+        devices = Device.objects.restrict(request.user, 'view').filter(
+            device_role=instance
+        )
+
+        devices_table = tables.DeviceTable(devices)
+        devices_table.columns.hide('device_role')
+        paginate_table(devices_table, request)
+
+        return {
+            'devices_table': devices_table,
+        }
+
+
 class DeviceRoleEditView(generic.ObjectEditView):
 class DeviceRoleEditView(generic.ObjectEditView):
     queryset = DeviceRole.objects.all()
     queryset = DeviceRole.objects.all()
     model_form = forms.DeviceRoleForm
     model_form = forms.DeviceRoleForm
@@ -1056,6 +1108,23 @@ class PlatformListView(generic.ObjectListView):
     table = tables.PlatformTable
     table = tables.PlatformTable
 
 
 
 
+class PlatformView(generic.ObjectView):
+    queryset = Platform.objects.all()
+
+    def get_extra_context(self, request, instance):
+        devices = Device.objects.restrict(request.user, 'view').filter(
+            platform=instance
+        )
+
+        devices_table = tables.DeviceTable(devices)
+        devices_table.columns.hide('platform')
+        paginate_table(devices_table, request)
+
+        return {
+            'devices_table': devices_table,
+        }
+
+
 class PlatformEditView(generic.ObjectEditView):
 class PlatformEditView(generic.ObjectEditView):
     queryset = Platform.objects.all()
     queryset = Platform.objects.all()
     model_form = forms.PlatformForm
     model_form = forms.PlatformForm

+ 4 - 1
netbox/ipam/models/ip.py

@@ -66,7 +66,7 @@ class RIR(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
+        return reverse('ipam:rir', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (
@@ -216,6 +216,9 @@ class Role(OrganizationalModel):
     def __str__(self):
     def __str__(self):
         return self.name
         return self.name
 
 
+    def get_absolute_url(self):
+        return reverse('ipam:role', args=[self.pk])
+
     def to_csv(self):
     def to_csv(self):
         return (
         return (
             self.name,
             self.name,

+ 1 - 1
netbox/ipam/models/vlans.py

@@ -70,7 +70,7 @@ class VLANGroup(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return reverse('ipam:vlangroup_vlans', args=[self.pk])
+        return reverse('ipam:vlangroup', args=[self.pk])
 
 
     def clean(self):
     def clean(self):
         super().clean()
         super().clean()

+ 5 - 3
netbox/ipam/tables.py

@@ -224,6 +224,9 @@ class AggregateDetailTable(AggregateTable):
 
 
 class RoleTable(BaseTable):
 class RoleTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
+    name = tables.Column(
+        linkify=True
+    )
     prefix_count = LinkedCountColumn(
     prefix_count = LinkedCountColumn(
         viewname='ipam:prefix_list',
         viewname='ipam:prefix_list',
         url_params={'role': 'slug'},
         url_params={'role': 'slug'},
@@ -450,9 +453,8 @@ class VLANTable(BaseTable):
     site = tables.Column(
     site = tables.Column(
         linkify=True
         linkify=True
     )
     )
-    group = tables.LinkColumn(
-        viewname='ipam:vlangroup_vlans',
-        args=[Accessor('group__pk')]
+    group = tables.Column(
+        linkify=True
     )
     )
     tenant = TenantColumn()
     tenant = TenantColumn()
     status = ChoiceFieldColumn(
     status = ChoiceFieldColumn(

+ 3 - 1
netbox/ipam/urls.py

@@ -37,6 +37,7 @@ urlpatterns = [
     path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
     path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
     path('rirs/edit/', views.RIRBulkEditView.as_view(), name='rir_bulk_edit'),
     path('rirs/edit/', views.RIRBulkEditView.as_view(), name='rir_bulk_edit'),
     path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
     path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
+    path('rirs/<int:pk>/', views.RIRView.as_view(), name='rir'),
     path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'),
     path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'),
     path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
     path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
     path('rirs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
     path('rirs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
@@ -59,6 +60,7 @@ urlpatterns = [
     path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
     path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
     path('roles/edit/', views.RoleBulkEditView.as_view(), name='role_bulk_edit'),
     path('roles/edit/', views.RoleBulkEditView.as_view(), name='role_bulk_edit'),
     path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
     path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
+    path('roles/<int:pk>/', views.RoleView.as_view(), name='role'),
     path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'),
     path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'),
     path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
     path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
     path('roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
     path('roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
@@ -97,9 +99,9 @@ urlpatterns = [
     path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
     path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
     path('vlan-groups/edit/', views.VLANGroupBulkEditView.as_view(), name='vlangroup_bulk_edit'),
     path('vlan-groups/edit/', views.VLANGroupBulkEditView.as_view(), name='vlangroup_bulk_edit'),
     path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
     path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
+    path('vlan-groups/<int:pk>/', views.VLANGroupView.as_view(), name='vlangroup'),
     path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
     path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
     path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
     path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
-    path('vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
     path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
     path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
 
 
     # VLANs
     # VLANs

+ 57 - 32
netbox/ipam/views.py

@@ -148,6 +148,23 @@ class RIRListView(generic.ObjectListView):
     template_name = 'ipam/rir_list.html'
     template_name = 'ipam/rir_list.html'
 
 
 
 
+class RIRView(generic.ObjectView):
+    queryset = RIR.objects.all()
+
+    def get_extra_context(self, request, instance):
+        aggregates = Aggregate.objects.restrict(request.user, 'view').filter(
+            rir=instance
+        )
+
+        aggregates_table = tables.AggregateTable(aggregates)
+        aggregates_table.columns.hide('rir')
+        paginate_table(aggregates_table, request)
+
+        return {
+            'aggregates_table': aggregates_table,
+        }
+
+
 class RIREditView(generic.ObjectEditView):
 class RIREditView(generic.ObjectEditView):
     queryset = RIR.objects.all()
     queryset = RIR.objects.all()
     model_form = forms.RIRForm
     model_form = forms.RIRForm
@@ -286,6 +303,23 @@ class RoleListView(generic.ObjectListView):
     table = tables.RoleTable
     table = tables.RoleTable
 
 
 
 
+class RoleView(generic.ObjectView):
+    queryset = Role.objects.all()
+
+    def get_extra_context(self, request, instance):
+        prefixes = Prefix.objects.restrict(request.user, 'view').filter(
+            role=instance
+        )
+
+        prefixes_table = tables.PrefixTable(prefixes)
+        prefixes_table.columns.hide('role')
+        paginate_table(prefixes_table, request)
+
+        return {
+            'prefixes_table': prefixes_table,
+        }
+
+
 class RoleEditView(generic.ObjectEditView):
 class RoleEditView(generic.ObjectEditView):
     queryset = Role.objects.all()
     queryset = Role.objects.all()
     model_form = forms.RoleForm
     model_form = forms.RoleForm
@@ -633,6 +667,29 @@ class VLANGroupListView(generic.ObjectListView):
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
 
 
 
 
+class VLANGroupView(generic.ObjectView):
+    queryset = VLANGroup.objects.all()
+
+    def get_extra_context(self, request, instance):
+        vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
+            Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
+        )
+        vlans_count = vlans.count()
+        vlans = add_available_vlans(instance, vlans)
+
+        vlans_table = tables.VLANDetailTable(vlans)
+        if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
+            vlans_table.columns.show('pk')
+        vlans_table.columns.hide('site')
+        vlans_table.columns.hide('group')
+        paginate_table(vlans_table, request)
+
+        return {
+            'vlans_count': vlans_count,
+            'vlans_table': vlans_table,
+        }
+
+
 class VLANGroupEditView(generic.ObjectEditView):
 class VLANGroupEditView(generic.ObjectEditView):
     queryset = VLANGroup.objects.all()
     queryset = VLANGroup.objects.all()
     model_form = forms.VLANGroupForm
     model_form = forms.VLANGroupForm
@@ -666,38 +723,6 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
 
 
 
 
-class VLANGroupVLANsView(generic.ObjectView):
-    queryset = VLANGroup.objects.all()
-    template_name = 'ipam/vlangroup_vlans.html'
-
-    def get_extra_context(self, request, instance):
-        vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
-            Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
-        )
-        vlans = add_available_vlans(instance, vlans)
-
-        vlan_table = tables.VLANDetailTable(vlans)
-        if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
-            vlan_table.columns.show('pk')
-        vlan_table.columns.hide('site')
-        vlan_table.columns.hide('group')
-        paginate_table(vlan_table, request)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'add': request.user.has_perm('ipam.add_vlan'),
-            'change': request.user.has_perm('ipam.change_vlan'),
-            'delete': request.user.has_perm('ipam.delete_vlan'),
-        }
-
-        return {
-            'first_available_vlan': instance.get_next_available_vid(),
-            'bulk_querystring': f'group_id={instance.pk}',
-            'vlan_table': vlan_table,
-            'permissions': permissions,
-        }
-
-
 #
 #
 # VLANs
 # VLANs
 #
 #

+ 1 - 1
netbox/secrets/models.py

@@ -263,7 +263,7 @@ class SecretRole(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?role={}".format(reverse('secrets:secret_list'), self.slug)
+        return reverse('secrets:secretrole', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (

+ 1 - 0
netbox/secrets/urls.py

@@ -13,6 +13,7 @@ urlpatterns = [
     path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
     path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
     path('secret-roles/edit/', views.SecretRoleBulkEditView.as_view(), name='secretrole_bulk_edit'),
     path('secret-roles/edit/', views.SecretRoleBulkEditView.as_view(), name='secretrole_bulk_edit'),
     path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
     path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
+    path('secret-roles/<int:pk>/', views.SecretRoleView.as_view(), name='secretrole'),
     path('secret-roles/<int:pk>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
     path('secret-roles/<int:pk>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
     path('secret-roles/<int:pk>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'),
     path('secret-roles/<int:pk>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'),
     path('secret-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
     path('secret-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),

+ 18 - 0
netbox/secrets/views.py

@@ -7,6 +7,7 @@ from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 from netbox.views import generic
 from netbox.views import generic
+from utilities.tables import paginate_table
 from utilities.utils import count_related
 from utilities.utils import count_related
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .models import SecretRole, Secret, SessionKey, UserKey
 from .models import SecretRole, Secret, SessionKey, UserKey
@@ -33,6 +34,23 @@ class SecretRoleListView(generic.ObjectListView):
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
 
 
+class SecretRoleView(generic.ObjectView):
+    queryset = SecretRole.objects.all()
+
+    def get_extra_context(self, request, instance):
+        secrets = Secret.objects.restrict(request.user, 'view').filter(
+            role=instance
+        )
+
+        secrets_table = tables.SecretTable(secrets)
+        secrets_table.columns.hide('role')
+        paginate_table(secrets_table, request)
+
+        return {
+            'secrets_table': secrets_table,
+        }
+
+
 class SecretRoleEditView(generic.ObjectEditView):
 class SecretRoleEditView(generic.ObjectEditView):
     queryset = SecretRole.objects.all()
     queryset = SecretRole.objects.all()
     model_form = forms.SecretRoleForm
     model_form = forms.SecretRoleForm

+ 60 - 0
netbox/templates/circuits/circuittype.html

@@ -0,0 +1,60 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'circuits:circuittype_list' %}">Circuit Types</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Circuit Type</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Circuits</td>
+          <td>
+            <a href="{% url 'circuits:circuit_list' %}?type_id={{ object.pk }}">{{ circuits_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+  </div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+	</div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Circuits</strong>
+      </div>
+      {% include 'inc/table.html' with table=circuits_table %}
+      {% if perms.circuits.add_circuit %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'circuits:circuit_add' %}?type={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add circuit
+          </a>
+        </div>
+      {% endif %}
+      </div>
+      {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
+      {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 76 - 0
netbox/templates/dcim/devicerole.html

@@ -0,0 +1,76 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'dcim:devicerole_list' %}">Device Roles</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Device Role</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Color</td>
+          <td>
+            <span class="label color-block" style="background-color: #{{ object.color }}">&nbsp;</span>
+          </td>
+        </tr>
+        <tr>
+          <td>VM Role</td>
+          <td>
+            {% if object.vm_role %}
+              <i class="mdi mdi-check-bold text-success" title="Yes"></i>
+            {% else %}
+              <i class="mdi mdi-close-thick text-danger" title="No"></i>
+            {% endif %}
+          </td>
+        </tr>
+        <tr>
+          <td>Devices</td>
+          <td>
+            <a href="{% url 'dcim:device_list' %}?role_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Devices</strong>
+      </div>
+      {% include 'inc/table.html' with table=devices_table %}
+      {% if perms.dcim.add_device %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 60 - 0
netbox/templates/dcim/manufacturer.html

@@ -0,0 +1,60 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Manufacturer</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Device types</td>
+          <td>
+            <a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.pk }}">{{ devicetypes_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Device Types</strong>
+      </div>
+      {% include 'inc/table.html' with table=devicetypes_table %}
+      {% if perms.dcim.add_devicetype %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'dcim:devicetype_add' %}?manufacturer={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device type
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=devicetypes_table.paginator page=devicetypes_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 68 - 0
netbox/templates/dcim/platform.html

@@ -0,0 +1,68 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'dcim:platform_list' %}">Platforms</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Platform</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>NAPALM Driver</td>
+          <td>{{ object.napalm_driver }}</td>
+        </tr>
+        <tr>
+          <td>NAPALM Arguments</td>
+          <td><pre>{{ object.napalm_args }}</pre></td>
+        </tr>
+        <tr>
+          <td>Devices</td>
+          <td>
+            <a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Devices</strong>
+      </div>
+      {% include 'inc/table.html' with table=devices_table %}
+      {% if perms.dcim.add_device %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
+        {% plugin_full_width_page object %}
+    </div>
+</div>
+{% endblock %}

+ 66 - 0
netbox/templates/dcim/rackrole.html

@@ -0,0 +1,66 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Rack Role</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Color</td>
+          <td>
+            <span class="label color-block" style="background-color: #{{ object.color }}">&nbsp;</span>
+          </td>
+        </tr>
+        <tr>
+          <td>Racks</td>
+          <td>
+            <a href="{% url 'dcim:rack_list' %}?role_id={{ object.pk }}">{{ racks_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Racks</strong>
+      </div>
+      {% include 'inc/table.html' with table=racks_table %}
+      {% if perms.dcim.add_rack %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'dcim:rack_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add rack
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=racks_table.paginator page=racks_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 70 - 0
netbox/templates/ipam/rir.html

@@ -0,0 +1,70 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'ipam:rir_list' %}">RIRs</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>RIR</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Private</td>
+          <td>
+            {% if object.is_private %}
+              <i class="mdi mdi-check-bold text-success" title="Yes"></i>
+            {% else %}
+              <i class="mdi mdi-close-thick text-danger" title="No"></i>
+            {% endif %}
+          </td>
+        </tr>
+        <tr>
+          <td>Aggregates</td>
+          <td>
+            <a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.pk }}">{{ aggregates_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Aggregates</strong>
+      </div>
+      {% include 'inc/table.html' with table=aggregates_table %}
+      {% if perms.ipam.add_aggregate %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'ipam:aggregate_add' %}?rir={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add aggregate
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=aggregates_table.paginator page=aggregates_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 64 - 0
netbox/templates/ipam/role.html

@@ -0,0 +1,64 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'ipam:role_list' %}">Roles</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Role</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Weight</td>
+          <td>{{ object.weight }}</td>
+        </tr>
+        <tr>
+          <td>Prefixes</td>
+          <td>
+            <a href="{% url 'ipam:prefix_list' %}?role_id={{ object.pk }}">{{ prefixes_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Prefixes</strong>
+      </div>
+      {% include 'inc/table.html' with table=prefixes_table %}
+      {% if perms.ipam.add_prefix %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'ipam:prefix_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add prefix
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=prefixes_table.paginator page=prefixes_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 72 - 0
netbox/templates/ipam/vlangroup.html

@@ -0,0 +1,72 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a></li>
+  {% if object.scope %}
+    <li><a href="{{ object.scope.get_absolute_url }}">{{ object.scope }}</a></li>
+  {% endif %}
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>VLAN Group</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Scope</td>
+          <td>
+          {% if object.scope %}
+            <a href="{{ object.scope.get_absolute_url }}">{{ object.scope }}</a>
+          {% else %}
+            <span class="text-muted">&mdash;</span>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>VLANs</td>
+          <td>
+            <a href="{% url 'ipam:vlan_list' %}?group_id={{ object.pk }}">{{ vlans_count }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>VLANs</strong>
+      </div>
+      {% include 'inc/table.html' with table=vlans_table %}
+      {% if perms.ipam.add_vlan %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'ipam:vlan_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add VLAN
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
+    </div>
+</div>
+{% endblock %}
+

+ 0 - 24
netbox/templates/ipam/vlangroup_vlans.html

@@ -1,24 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %}{{ object }} - VLANs{% endblock %}
-
-{% block content %}
-<div class="row noprint">
-    <div class="col-sm-12 col-md-12">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a></li>
-            {% if object.site %}
-                <li><a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a></li>
-            {% endif %}
-            <li>{{ object }}</li>
-        </ol>
-    </div>
-</div>
-    {% include 'ipam/inc/vlangroup_header.html' %}
-    <div class="row">
-        <div class="col-md-12">
-            {% include 'utilities/obj_table.html' with table=vlan_table table_template='panel_table.html' heading='VLANs' bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
-        </div>
-    </div>
-{% endblock %}
-

+ 60 - 0
netbox/templates/secrets/secretrole.html

@@ -0,0 +1,60 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'secrets:secretrole_list' %}">Secret Roles</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Secret Role</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Secrets</td>
+          <td>
+            <a href="{% url 'secrets:secret_list' %}?role_id={{ object.pk }}">{{ secrets_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Secrets</strong>
+      </div>
+      {% include 'inc/table.html' with table=secrets_table %}
+      {% if perms.secrets.add_secret %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'secrets:secret_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add secret
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=secrets_table.paginator page=secrets_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 60 - 0
netbox/templates/virtualization/clustergroup.html

@@ -0,0 +1,60 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'virtualization:clustertype_list' %}">Cluster Groups</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Cluster Group</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Clusters</td>
+          <td>
+            <a href="{% url 'virtualization:cluster_list' %}?group_id={{ object.pk }}">{{ clusters_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Clusters</strong>
+      </div>
+      {% include 'inc/table.html' with table=clusters_table %}
+      {% if perms.virtualization.add_cluster %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add cluster
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 60 - 0
netbox/templates/virtualization/clustertype.html

@@ -0,0 +1,60 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  <li><a href="{% url 'virtualization:clustertype_list' %}">Cluster Types</a></li>
+  <li>{{ object }}</li>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-6">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Cluster Type</strong>
+      </div>
+      <table class="table table-hover panel-body attr-table">
+        <tr>
+          <td>Name</td>
+          <td>{{ object.name }}</td>
+        </tr>
+        <tr>
+          <td>Description</td>
+          <td>{{ object.description|placeholder }}</td>
+        </tr>
+        <tr>
+          <td>Clusters</td>
+          <td>
+            <a href="{% url 'virtualization:cluster_list' %}?type_id={{ object.pk }}">{{ clusters_table.rows|length }}</a>
+          </td>
+        </tr>
+      </table>
+    </div>
+    {% plugin_left_page object %}
+	</div>
+	<div class="col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+  </div>
+</div>
+<div class="row">
+	<div class="col-md-12">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <strong>Clusters</strong>
+      </div>
+      {% include 'inc/table.html' with table=clusters_table %}
+      {% if perms.virtualization.add_cluster %}
+        <div class="panel-footer text-right noprint">
+          <a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-xs btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add cluster
+          </a>
+        </div>
+      {% endif %}
+    </div>
+    {% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
+    {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 2 - 2
netbox/virtualization/models.py

@@ -59,7 +59,7 @@ class ClusterType(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
+        return reverse('virtualization:clustertype', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (
@@ -102,7 +102,7 @@ class ClusterGroup(OrganizationalModel):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
+        return reverse('virtualization:clustergroup', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (

+ 2 - 0
netbox/virtualization/urls.py

@@ -14,6 +14,7 @@ urlpatterns = [
     path('cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
     path('cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
     path('cluster-types/edit/', views.ClusterTypeBulkEditView.as_view(), name='clustertype_bulk_edit'),
     path('cluster-types/edit/', views.ClusterTypeBulkEditView.as_view(), name='clustertype_bulk_edit'),
     path('cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
     path('cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
+    path('cluster-types/<int:pk>/', views.ClusterTypeView.as_view(), name='clustertype'),
     path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
     path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
     path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'),
     path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'),
     path('cluster-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}),
     path('cluster-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}),
@@ -24,6 +25,7 @@ urlpatterns = [
     path('cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
     path('cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
     path('cluster-groups/edit/', views.ClusterGroupBulkEditView.as_view(), name='clustergroup_bulk_edit'),
     path('cluster-groups/edit/', views.ClusterGroupBulkEditView.as_view(), name='clustergroup_bulk_edit'),
     path('cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
     path('cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
+    path('cluster-groups/<int:pk>/', views.ClusterGroupView.as_view(), name='clustergroup'),
     path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
     path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
     path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'),
     path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'),
     path('cluster-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),
     path('cluster-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),

+ 35 - 0
netbox/virtualization/views.py

@@ -11,6 +11,7 @@ from ipam.models import IPAddress, Service
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
 from netbox.views import generic
 from netbox.views import generic
 from secrets.models import Secret
 from secrets.models import Secret
+from utilities.tables import paginate_table
 from utilities.utils import count_related
 from utilities.utils import count_related
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@@ -27,6 +28,23 @@ class ClusterTypeListView(generic.ObjectListView):
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
 
 
+class ClusterTypeView(generic.ObjectView):
+    queryset = ClusterType.objects.all()
+
+    def get_extra_context(self, request, instance):
+        clusters = Cluster.objects.restrict(request.user, 'view').filter(
+            type=instance
+        )
+
+        clusters_table = tables.ClusterTable(clusters)
+        clusters_table.columns.hide('type')
+        paginate_table(clusters_table, request)
+
+        return {
+            'clusters_table': clusters_table,
+        }
+
+
 class ClusterTypeEditView(generic.ObjectEditView):
 class ClusterTypeEditView(generic.ObjectEditView):
     queryset = ClusterType.objects.all()
     queryset = ClusterType.objects.all()
     model_form = forms.ClusterTypeForm
     model_form = forms.ClusterTypeForm
@@ -69,6 +87,23 @@ class ClusterGroupListView(generic.ObjectListView):
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
 
 
+class ClusterGroupView(generic.ObjectView):
+    queryset = ClusterGroup.objects.all()
+
+    def get_extra_context(self, request, instance):
+        clusters = Cluster.objects.restrict(request.user, 'view').filter(
+            group=instance
+        )
+
+        clusters_table = tables.ClusterTable(clusters)
+        clusters_table.columns.hide('group')
+        paginate_table(clusters_table, request)
+
+        return {
+            'clusters_table': clusters_table,
+        }
+
+
 class ClusterGroupEditView(generic.ObjectEditView):
 class ClusterGroupEditView(generic.ObjectEditView):
     queryset = ClusterGroup.objects.all()
     queryset = ClusterGroup.objects.all()
     model_form = forms.ClusterGroupForm
     model_form = forms.ClusterGroupForm