Browse Source

Converted all object views to class-based views

Jeremy Stretch 8 years ago
parent
commit
fb85867d72

+ 2 - 2
netbox/circuits/urls.py

@@ -12,7 +12,7 @@ urlpatterns = [
     url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
     url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
     url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
-    url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'),
+    url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
     url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
     url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
 
@@ -28,7 +28,7 @@ urlpatterns = [
     url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
     url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
     url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
-    url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
+    url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
     url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
     url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
     url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),

+ 35 - 27
netbox/circuits/views.py

@@ -5,6 +5,7 @@ from django.db import transaction
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
+from django.views.generic import View
 
 from extras.models import Graph, GRAPH_TYPE_PROVIDER
 from utilities.forms import ConfirmationForm
@@ -28,18 +29,23 @@ class ProviderListView(ObjectListView):
     template_name = 'circuits/provider_list.html'
 
 
-def provider(request, slug):
+class ProviderView(View):
 
-    provider = get_object_or_404(Provider, slug=slug)
-    circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\
-        .prefetch_related('terminations__site')
-    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
+    def get(self, request, slug):
 
-    return render(request, 'circuits/provider.html', {
-        'provider': provider,
-        'circuits': circuits,
-        'show_graphs': show_graphs,
-    })
+        provider = get_object_or_404(Provider, slug=slug)
+        circuits = Circuit.objects.filter(provider=provider).select_related(
+            'type', 'tenant'
+        ).prefetch_related(
+            'terminations__site'
+        )
+        show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
+
+        return render(request, 'circuits/provider.html', {
+            'provider': provider,
+            'circuits': circuits,
+            'show_graphs': show_graphs,
+        })
 
 
 class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
@@ -117,25 +123,27 @@ class CircuitListView(ObjectListView):
     template_name = 'circuits/circuit_list.html'
 
 
-def circuit(request, pk):
+class CircuitView(View):
 
-    circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
-    termination_a = CircuitTermination.objects.select_related(
-        'site__region', 'interface__device'
-    ).filter(
-        circuit=circuit, term_side=TERM_SIDE_A
-    ).first()
-    termination_z = CircuitTermination.objects.select_related(
-        'site__region', 'interface__device'
-    ).filter(
-        circuit=circuit, term_side=TERM_SIDE_Z
-    ).first()
+    def get(self, request, pk):
 
-    return render(request, 'circuits/circuit.html', {
-        'circuit': circuit,
-        'termination_a': termination_a,
-        'termination_z': termination_z,
-    })
+        circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
+        termination_a = CircuitTermination.objects.select_related(
+            'site__region', 'interface__device'
+        ).filter(
+            circuit=circuit, term_side=TERM_SIDE_A
+        ).first()
+        termination_z = CircuitTermination.objects.select_related(
+            'site__region', 'interface__device'
+        ).filter(
+            circuit=circuit, term_side=TERM_SIDE_Z
+        ).first()
+
+        return render(request, 'circuits/circuit.html', {
+            'circuit': circuit,
+            'termination_a': termination_a,
+            'termination_z': termination_z,
+        })
 
 
 class CircuitEditView(PermissionRequiredMixin, ObjectEditView):

+ 6 - 6
netbox/dcim/urls.py

@@ -22,7 +22,7 @@ urlpatterns = [
     url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
     url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
     url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
-    url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
+    url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
     url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
     url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
     url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
@@ -52,7 +52,7 @@ urlpatterns = [
     url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
     url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
     url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
-    url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
+    url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
     url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
     url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
     url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
@@ -69,7 +69,7 @@ urlpatterns = [
     url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'),
     url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
     url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
-    url(r'^device-types/(?P<pk>\d+)/$', views.devicetype, name='devicetype'),
+    url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
     url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
     url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
 
@@ -117,11 +117,11 @@ urlpatterns = [
     url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
     url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
     url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
-    url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
+    url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
     url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
     url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
-    url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
-    url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
+    url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
+    url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
     url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
     url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),
     url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),

+ 209 - 176
netbox/dcim/views.py

@@ -178,27 +178,29 @@ class SiteListView(ObjectListView):
     template_name = 'dcim/site_list.html'
 
 
-def site(request, slug):
-
-    site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
-    stats = {
-        'rack_count': Rack.objects.filter(site=site).count(),
-        'device_count': Device.objects.filter(site=site).count(),
-        'prefix_count': Prefix.objects.filter(site=site).count(),
-        'vlan_count': VLAN.objects.filter(site=site).count(),
-        'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
-    }
-    rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
-    topology_maps = TopologyMap.objects.filter(site=site)
-    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
-
-    return render(request, 'dcim/site.html', {
-        'site': site,
-        'stats': stats,
-        'rack_groups': rack_groups,
-        'topology_maps': topology_maps,
-        'show_graphs': show_graphs,
-    })
+class SiteView(View):
+
+    def get(self, request, slug):
+
+        site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
+        stats = {
+            'rack_count': Rack.objects.filter(site=site).count(),
+            'device_count': Device.objects.filter(site=site).count(),
+            'prefix_count': Prefix.objects.filter(site=site).count(),
+            'vlan_count': VLAN.objects.filter(site=site).count(),
+            'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
+        }
+        rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
+        topology_maps = TopologyMap.objects.filter(site=site)
+        show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
+
+        return render(request, 'dcim/site.html', {
+            'site': site,
+            'stats': stats,
+            'rack_groups': rack_groups,
+            'topology_maps': topology_maps,
+            'show_graphs': show_graphs,
+        })
 
 
 class SiteEditView(PermissionRequiredMixin, ObjectEditView):
@@ -290,8 +292,13 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 #
 
 class RackListView(ObjectListView):
-    queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
-        .annotate(device_count=Count('devices', distinct=True))
+    queryset = Rack.objects.select_related(
+        'site', 'group', 'tenant', 'role'
+    ).prefetch_related(
+        'devices__device_type'
+    ).annotate(
+        device_count=Count('devices', distinct=True)
+    )
     filter = filters.RackFilter
     filter_form = forms.RackFilterForm
     table = tables.RackTable
@@ -338,31 +345,33 @@ class RackElevationListView(View):
         })
 
 
-def rack(request, pk):
-
-    rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
-
-    nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
-        .select_related('device_type__manufacturer')
-    next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
-    prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
+class RackView(View):
 
-    reservations = RackReservation.objects.filter(rack=rack)
-    reserved_units = {}
-    for r in reservations:
-        for u in r.units:
-            reserved_units[u] = r
+    def get(self, request, pk):
 
-    return render(request, 'dcim/rack.html', {
-        'rack': rack,
-        'reservations': reservations,
-        'reserved_units': reserved_units,
-        'nonracked_devices': nonracked_devices,
-        'next_rack': next_rack,
-        'prev_rack': prev_rack,
-        'front_elevation': rack.get_front_elevation(),
-        'rear_elevation': rack.get_rear_elevation(),
-    })
+        rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
+
+        nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
+            .select_related('device_type__manufacturer')
+        next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
+        prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
+
+        reservations = RackReservation.objects.filter(rack=rack)
+        reserved_units = {}
+        for r in reservations:
+            for u in r.units:
+                reserved_units[u] = r
+
+        return render(request, 'dcim/rack.html', {
+            'rack': rack,
+            'reservations': reservations,
+            'reserved_units': reserved_units,
+            'nonracked_devices': nonracked_devices,
+            'next_rack': next_rack,
+            'prev_rack': prev_rack,
+            'front_elevation': rack.get_front_elevation(),
+            'rear_elevation': rack.get_rear_elevation(),
+        })
 
 
 class RackEditView(PermissionRequiredMixin, ObjectEditView):
@@ -481,53 +490,57 @@ class DeviceTypeListView(ObjectListView):
     template_name = 'dcim/devicetype_list.html'
 
 
-def devicetype(request, pk):
+class DeviceTypeView(View):
+
+    def get(self, request, pk):
 
-    devicetype = get_object_or_404(DeviceType, pk=pk)
+        devicetype = get_object_or_404(DeviceType, pk=pk)
 
-    # Component tables
-    consoleport_table = tables.ConsolePortTemplateTable(
-        natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
-    )
-    consoleserverport_table = tables.ConsoleServerPortTemplateTable(
-        natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
-    )
-    powerport_table = tables.PowerPortTemplateTable(
-        natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
-    )
-    poweroutlet_table = tables.PowerOutletTemplateTable(
-        natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
-    )
-    mgmt_interface_table = tables.InterfaceTemplateTable(
-        list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
-                                                                                             mgmt_only=True))
-    )
-    interface_table = tables.InterfaceTemplateTable(
-        list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
-                                                                                             mgmt_only=False))
-    )
-    devicebay_table = tables.DeviceBayTemplateTable(
-        natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
-    )
-    if request.user.has_perm('dcim.change_devicetype'):
-        consoleport_table.base_columns['pk'].visible = True
-        consoleserverport_table.base_columns['pk'].visible = True
-        powerport_table.base_columns['pk'].visible = True
-        poweroutlet_table.base_columns['pk'].visible = True
-        mgmt_interface_table.base_columns['pk'].visible = True
-        interface_table.base_columns['pk'].visible = True
-        devicebay_table.base_columns['pk'].visible = True
-
-    return render(request, 'dcim/devicetype.html', {
-        'devicetype': devicetype,
-        'consoleport_table': consoleport_table,
-        'consoleserverport_table': consoleserverport_table,
-        'powerport_table': powerport_table,
-        'poweroutlet_table': poweroutlet_table,
-        'mgmt_interface_table': mgmt_interface_table,
-        'interface_table': interface_table,
-        'devicebay_table': devicebay_table,
-    })
+        # Component tables
+        consoleport_table = tables.ConsolePortTemplateTable(
+            natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
+        )
+        consoleserverport_table = tables.ConsoleServerPortTemplateTable(
+            natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
+        )
+        powerport_table = tables.PowerPortTemplateTable(
+            natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
+        )
+        poweroutlet_table = tables.PowerOutletTemplateTable(
+            natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
+        )
+        mgmt_interface_table = tables.InterfaceTemplateTable(
+            list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
+                device_type=devicetype, mgmt_only=True
+            ))
+        )
+        interface_table = tables.InterfaceTemplateTable(
+            list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
+                device_type=devicetype, mgmt_only=False
+            ))
+        )
+        devicebay_table = tables.DeviceBayTemplateTable(
+            natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
+        )
+        if request.user.has_perm('dcim.change_devicetype'):
+            consoleport_table.base_columns['pk'].visible = True
+            consoleserverport_table.base_columns['pk'].visible = True
+            powerport_table.base_columns['pk'].visible = True
+            poweroutlet_table.base_columns['pk'].visible = True
+            mgmt_interface_table.base_columns['pk'].visible = True
+            interface_table.base_columns['pk'].visible = True
+            devicebay_table.base_columns['pk'].visible = True
+
+        return render(request, 'dcim/devicetype.html', {
+            'devicetype': devicetype,
+            'consoleport_table': consoleport_table,
+            'consoleserverport_table': consoleserverport_table,
+            'powerport_table': powerport_table,
+            'poweroutlet_table': poweroutlet_table,
+            'mgmt_interface_table': mgmt_interface_table,
+            'interface_table': interface_table,
+            'devicebay_table': devicebay_table,
+        })
 
 
 class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
@@ -727,70 +740,114 @@ class DeviceListView(ObjectListView):
     template_name = 'dcim/device_list.html'
 
 
-def device(request, pk):
+class DeviceView(View):
 
-    device = get_object_or_404(Device.objects.select_related(
-        'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
-    ), pk=pk)
-    console_ports = natsorted(
-        ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
-    )
-    cs_ports = natsorted(
-        ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
-    )
-    power_ports = natsorted(
-        PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
-    )
-    power_outlets = natsorted(
-        PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
-    )
-    interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
-        .filter(device=device, mgmt_only=False)\
-        .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
-                        'circuit_termination__circuit').prefetch_related('ip_addresses')
-    mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
-        .filter(device=device, mgmt_only=True)\
-        .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
-                        'circuit_termination__circuit').prefetch_related('ip_addresses')
-    device_bays = natsorted(
-        DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
-        key=attrgetter('name')
-    )
-    services = Service.objects.filter(device=device)
-    secrets = device.secrets.all()
-
-    # Find any related devices for convenient linking in the UI
-    related_devices = []
-    if device.name:
-        if re.match('.+[0-9]+$', device.name):
-            # Strip 1 or more trailing digits (e.g. core-switch1)
-            base_name = re.match('(.*?)[0-9]+$', device.name).group(1)
-        elif re.match('.+\d[a-z]$', device.name.lower()):
-            # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3)
-            base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1)
-        else:
-            base_name = None
-        if base_name:
-            related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
-                .select_related('rack', 'device_type__manufacturer')[:10]
+    def get(self, request, pk):
+
+        device = get_object_or_404(Device.objects.select_related(
+            'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
+        ), pk=pk)
+        console_ports = natsorted(
+            ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
+        )
+        cs_ports = natsorted(
+            ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
+        )
+        power_ports = natsorted(
+            PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
+        )
+        power_outlets = natsorted(
+            PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
+        )
+        interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
+            device=device, mgmt_only=False
+        ).select_related(
+            'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
+            'circuit_termination__circuit'
+        ).prefetch_related('ip_addresses')
+        mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
+            device=device, mgmt_only=True
+        ).select_related(
+            'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
+            'circuit_termination__circuit'
+        ).prefetch_related('ip_addresses')
+        device_bays = natsorted(
+            DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
+            key=attrgetter('name')
+        )
+        services = Service.objects.filter(device=device)
+        secrets = device.secrets.all()
+
+        # Find any related devices for convenient linking in the UI
+        related_devices = []
+        if device.name:
+            if re.match('.+[0-9]+$', device.name):
+                # Strip 1 or more trailing digits (e.g. core-switch1)
+                base_name = re.match('(.*?)[0-9]+$', device.name).group(1)
+            elif re.match('.+\d[a-z]$', device.name.lower()):
+                # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3)
+                base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1)
+            else:
+                base_name = None
+            if base_name:
+                related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
+                    .select_related('rack', 'device_type__manufacturer')[:10]
+
+        # Show graph button on interfaces only if at least one graph has been created.
+        show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
+
+        return render(request, 'dcim/device.html', {
+            'device': device,
+            'console_ports': console_ports,
+            'cs_ports': cs_ports,
+            'power_ports': power_ports,
+            'power_outlets': power_outlets,
+            'interfaces': interfaces,
+            'mgmt_interfaces': mgmt_interfaces,
+            'device_bays': device_bays,
+            'services': services,
+            'secrets': secrets,
+            'related_devices': related_devices,
+            'show_graphs': show_graphs,
+        })
 
-    # Show graph button on interfaces only if at least one graph has been created.
-    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
 
-    return render(request, 'dcim/device.html', {
-        'device': device,
-        'console_ports': console_ports,
-        'cs_ports': cs_ports,
-        'power_ports': power_ports,
-        'power_outlets': power_outlets,
-        'interfaces': interfaces,
-        'mgmt_interfaces': mgmt_interfaces,
-        'device_bays': device_bays,
-        'services': services,
-        'secrets': secrets,
-        'related_devices': related_devices,
-        'show_graphs': show_graphs,
-    })
+class DeviceInventoryView(View):
+
+    def get(self, request, pk):
+
+        device = get_object_or_404(Device, pk=pk)
+        inventory_items = InventoryItem.objects.filter(
+            device=device, parent=None
+        ).select_related(
+            'manufacturer'
+        ).prefetch_related(
+            'child_items'
+        )
+
+        return render(request, 'dcim/device_inventory.html', {
+            'device': device,
+            'inventory_items': inventory_items,
+        })
+
+
+class DeviceLLDPNeighborsView(View):
+
+    def get(self, request, pk):
+
+        device = get_object_or_404(Device, pk=pk)
+        interfaces = Interface.objects.order_naturally(
+            device.device_type.interface_ordering
+        ).filter(
+            device=device
+        ).select_related(
+            'connected_as_a', 'connected_as_b'
+        )
+
+        return render(request, 'dcim/device_lldp_neighbors.html', {
+            'device': device,
+            'interfaces': interfaces,
+        })
 
 
 class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
@@ -851,30 +908,6 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     default_return_url = 'dcim:device_list'
 
 
-def device_inventory(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
-    inventory_items = InventoryItem.objects.filter(device=device, parent=None).select_related('manufacturer')\
-        .prefetch_related('child_items')
-
-    return render(request, 'dcim/device_inventory.html', {
-        'device': device,
-        'inventory_items': inventory_items,
-    })
-
-
-def device_lldp_neighbors(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
-    interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
-        .select_related('connected_as_a', 'connected_as_b')
-
-    return render(request, 'dcim/device_lldp_neighbors.html', {
-        'device': device,
-        'interfaces': interfaces,
-    })
-
-
 #
 # Console ports
 #

+ 6 - 6
netbox/ipam/urls.py

@@ -12,7 +12,7 @@ urlpatterns = [
     url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
     url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
     url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
-    url(r'^vrfs/(?P<pk>\d+)/$', views.vrf, name='vrf'),
+    url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
     url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
     url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
 
@@ -28,7 +28,7 @@ urlpatterns = [
     url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
     url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
     url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
-    url(r'^aggregates/(?P<pk>\d+)/$', views.aggregate, name='aggregate'),
+    url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
     url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
     url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
 
@@ -44,10 +44,10 @@ urlpatterns = [
     url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
     url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
     url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
-    url(r'^prefixes/(?P<pk>\d+)/$', views.prefix, name='prefix'),
+    url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
     url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
     url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
-    url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'),
+    url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
 
     # IP addresses
     url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
@@ -56,7 +56,7 @@ urlpatterns = [
     url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
     url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
     url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
-    url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'),
+    url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
     url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
     url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
 
@@ -72,7 +72,7 @@ urlpatterns = [
     url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
     url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
     url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
-    url(r'^vlans/(?P<pk>\d+)/$', views.vlan, name='vlan'),
+    url(r'^vlans/(?P<pk>\d+)/$', views.VLANView.as_view(), name='vlan'),
     url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
     url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
 

+ 213 - 164
netbox/ipam/views.py

@@ -6,6 +6,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count, Q
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
+from django.views.generic import View
 
 from dcim.models import Device
 from utilities.paginator import EnhancedPaginator
@@ -96,18 +97,20 @@ class VRFListView(ObjectListView):
     template_name = 'ipam/vrf_list.html'
 
 
-def vrf(request, pk):
+class VRFView(View):
 
-    vrf = get_object_or_404(VRF.objects.all(), pk=pk)
-    prefix_table = tables.PrefixBriefTable(
-        list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
-    )
-    prefix_table.exclude = ('vrf',)
+    def get(self, request, pk):
 
-    return render(request, 'ipam/vrf.html', {
-        'vrf': vrf,
-        'prefix_table': prefix_table,
-    })
+        vrf = get_object_or_404(VRF.objects.all(), pk=pk)
+        prefix_table = tables.PrefixBriefTable(
+            list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
+        )
+        prefix_table.exclude = ('vrf',)
+
+        return render(request, 'ipam/vrf.html', {
+            'vrf': vrf,
+            'prefix_table': prefix_table,
+        })
 
 
 class VRFEditView(PermissionRequiredMixin, ObjectEditView):
@@ -281,37 +284,44 @@ class AggregateListView(ObjectListView):
         }
 
 
-def aggregate(request, pk):
+class AggregateView(View):
+
+    def get(self, request, pk):
 
-    aggregate = get_object_or_404(Aggregate, pk=pk)
+        aggregate = get_object_or_404(Aggregate, pk=pk)
 
-    # Find all child prefixes contained by this aggregate
-    child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\
-        .select_related('site', 'role').annotate_depth(limit=0)
-    child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
+        # Find all child prefixes contained by this aggregate
+        child_prefixes = Prefix.objects.filter(
+            prefix__net_contained_or_equal=str(aggregate.prefix)
+        ).select_related(
+            'site', 'role'
+        ).annotate_depth(
+            limit=0
+        )
+        child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
 
-    prefix_table = tables.PrefixTable(child_prefixes)
-    if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
-        prefix_table.base_columns['pk'].visible = True
+        prefix_table = tables.PrefixTable(child_prefixes)
+        if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
+            prefix_table.base_columns['pk'].visible = True
 
-    paginate = {
-        'klass': EnhancedPaginator,
-        'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
-    }
-    RequestConfig(request, paginate).configure(prefix_table)
+        paginate = {
+            'klass': EnhancedPaginator,
+            'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
+        }
+        RequestConfig(request, paginate).configure(prefix_table)
 
-    # Compile permissions list for rendering the object table
-    permissions = {
-        'add': request.user.has_perm('ipam.add_prefix'),
-        'change': request.user.has_perm('ipam.change_prefix'),
-        'delete': request.user.has_perm('ipam.delete_prefix'),
-    }
+        # Compile permissions list for rendering the object table
+        permissions = {
+            'add': request.user.has_perm('ipam.add_prefix'),
+            'change': request.user.has_perm('ipam.change_prefix'),
+            'delete': request.user.has_perm('ipam.delete_prefix'),
+        }
 
-    return render(request, 'ipam/aggregate.html', {
-        'aggregate': aggregate,
-        'prefix_table': prefix_table,
-        'permissions': permissions,
-    })
+        return render(request, 'ipam/aggregate.html', {
+            'aggregate': aggregate,
+            'prefix_table': prefix_table,
+            'permissions': permissions,
+        })
 
 
 class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
@@ -394,66 +404,120 @@ class PrefixListView(ObjectListView):
         return self.queryset.annotate_depth(limit=limit)
 
 
-def prefix(request, pk):
-
-    prefix = get_object_or_404(Prefix.objects.select_related(
-        'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
-    ), pk=pk)
-
-    try:
-        aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
-    except Aggregate.DoesNotExist:
-        aggregate = None
-
-    # Count child IP addresses
-    ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
-        .count()
-
-    # Parent prefixes table
-    parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
-        .filter(prefix__net_contains=str(prefix.prefix))\
-        .select_related('site', 'role').annotate_depth()
-    parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
-    parent_prefix_table.exclude = ('vrf',)
-
-    # Duplicate prefixes table
-    duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
-        .select_related('site', 'role')
-    duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
-    duplicate_prefix_table.exclude = ('vrf',)
-
-    # Child prefixes table
-    child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
-        .select_related('site', 'role').annotate_depth(limit=0)
-    if child_prefixes:
-        child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
-    child_prefix_table = tables.PrefixTable(child_prefixes)
-    if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
-        child_prefix_table.base_columns['pk'].visible = True
-
-    paginate = {
-        'klass': EnhancedPaginator,
-        'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
-    }
-    RequestConfig(request, paginate).configure(child_prefix_table)
-
-    # Compile permissions list for rendering the object table
-    permissions = {
-        'add': request.user.has_perm('ipam.add_prefix'),
-        'change': request.user.has_perm('ipam.change_prefix'),
-        'delete': request.user.has_perm('ipam.delete_prefix'),
-    }
-
-    return render(request, 'ipam/prefix.html', {
-        'prefix': prefix,
-        'aggregate': aggregate,
-        'ipaddress_count': ipaddress_count,
-        'parent_prefix_table': parent_prefix_table,
-        'child_prefix_table': child_prefix_table,
-        'duplicate_prefix_table': duplicate_prefix_table,
-        'permissions': permissions,
-        'return_url': prefix.get_absolute_url(),
-    })
+class PrefixView(View):
+
+    def get(self, request, pk):
+
+        prefix = get_object_or_404(Prefix.objects.select_related(
+            'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
+        ), pk=pk)
+
+        try:
+            aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
+        except Aggregate.DoesNotExist:
+            aggregate = None
+
+        # Count child IP addresses
+        ipaddress_count = IPAddress.objects.filter(
+            vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
+        ).count()
+
+        # Parent prefixes table
+        parent_prefixes = Prefix.objects.filter(
+            Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
+        ).filter(
+            prefix__net_contains=str(prefix.prefix)
+        ).select_related(
+            'site', 'role'
+        ).annotate_depth()
+        parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
+        parent_prefix_table.exclude = ('vrf',)
+
+        # Duplicate prefixes table
+        duplicate_prefixes = Prefix.objects.filter(
+            vrf=prefix.vrf, prefix=str(prefix.prefix)
+        ).exclude(
+            pk=prefix.pk
+        ).select_related(
+            'site', 'role'
+        )
+        duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
+        duplicate_prefix_table.exclude = ('vrf',)
+
+        # Child prefixes table
+        child_prefixes = Prefix.objects.filter(
+            vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
+        ).select_related(
+            'site', 'role'
+        ).annotate_depth(limit=0)
+        if child_prefixes:
+            child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
+        child_prefix_table = tables.PrefixTable(child_prefixes)
+        if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
+            child_prefix_table.base_columns['pk'].visible = True
+
+        paginate = {
+            'klass': EnhancedPaginator,
+            'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
+        }
+        RequestConfig(request, paginate).configure(child_prefix_table)
+
+        # Compile permissions list for rendering the object table
+        permissions = {
+            'add': request.user.has_perm('ipam.add_prefix'),
+            'change': request.user.has_perm('ipam.change_prefix'),
+            'delete': request.user.has_perm('ipam.delete_prefix'),
+        }
+
+        return render(request, 'ipam/prefix.html', {
+            'prefix': prefix,
+            'aggregate': aggregate,
+            'ipaddress_count': ipaddress_count,
+            'parent_prefix_table': parent_prefix_table,
+            'child_prefix_table': child_prefix_table,
+            'duplicate_prefix_table': duplicate_prefix_table,
+            'permissions': permissions,
+            'return_url': prefix.get_absolute_url(),
+        })
+
+
+class PrefixIPAddressesView(View):
+
+    def get(self, request, pk):
+
+        prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
+
+        # Find all IPAddresses belonging to this Prefix
+        ipaddresses = IPAddress.objects.filter(
+            vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
+        ).select_related(
+            'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
+        )
+        ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
+
+        ip_table = tables.IPAddressTable(ipaddresses)
+        if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
+            ip_table.base_columns['pk'].visible = True
+
+        paginate = {
+            'klass': EnhancedPaginator,
+            'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
+        }
+        RequestConfig(request, paginate).configure(ip_table)
+
+        # Compile permissions list for rendering the object table
+        permissions = {
+            'add': request.user.has_perm('ipam.add_ipaddress'),
+            'change': request.user.has_perm('ipam.change_ipaddress'),
+            'delete': request.user.has_perm('ipam.delete_ipaddress'),
+        }
+
+        return render(request, 'ipam/prefix_ipaddresses.html', {
+            'prefix': prefix,
+            'ip_table': ip_table,
+            'permissions': permissions,
+            'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
+        })
 
 
 class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
@@ -495,40 +559,6 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     default_return_url = 'ipam:prefix_list'
 
 
-def prefix_ipaddresses(request, pk):
-
-    prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
-
-    # Find all IPAddresses belonging to this Prefix
-    ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
-        .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
-    ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
-
-    ip_table = tables.IPAddressTable(ipaddresses)
-    if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
-        ip_table.base_columns['pk'].visible = True
-
-    paginate = {
-        'klass': EnhancedPaginator,
-        'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
-    }
-    RequestConfig(request, paginate).configure(ip_table)
-
-    # Compile permissions list for rendering the object table
-    permissions = {
-        'add': request.user.has_perm('ipam.add_ipaddress'),
-        'change': request.user.has_perm('ipam.change_ipaddress'),
-        'delete': request.user.has_perm('ipam.delete_ipaddress'),
-    }
-
-    return render(request, 'ipam/prefix_ipaddresses.html', {
-        'prefix': prefix,
-        'ip_table': ip_table,
-        'permissions': permissions,
-        'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
-    })
-
-
 #
 # IP addresses
 #
@@ -541,32 +571,47 @@ class IPAddressListView(ObjectListView):
     template_name = 'ipam/ipaddress_list.html'
 
 
-def ipaddress(request, pk):
-
-    ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
-
-    # Parent prefixes table
-    parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\
-        .select_related('site', 'role')
-    parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
-    parent_prefixes_table.exclude = ('vrf',)
-
-    # Duplicate IPs table
-    duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
-        .exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
-    duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
-
-    # Related IP table
-    related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
-        .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
-    related_ips_table = tables.IPAddressBriefTable(list(related_ips))
-
-    return render(request, 'ipam/ipaddress.html', {
-        'ipaddress': ipaddress,
-        'parent_prefixes_table': parent_prefixes_table,
-        'duplicate_ips_table': duplicate_ips_table,
-        'related_ips_table': related_ips_table,
-    })
+class IPAddressView(View):
+
+    def get(self, request, pk):
+
+        ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
+
+        # Parent prefixes table
+        parent_prefixes = Prefix.objects.filter(
+            vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
+        ).select_related(
+            'site', 'role'
+        )
+        parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
+        parent_prefixes_table.exclude = ('vrf',)
+
+        # Duplicate IPs table
+        duplicate_ips = IPAddress.objects.filter(
+            vrf=ipaddress.vrf, address=str(ipaddress.address)
+        ).exclude(
+            pk=ipaddress.pk
+        ).select_related(
+            'interface__device', 'nat_inside'
+        )
+        duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
+
+        # Related IP table
+        related_ips = IPAddress.objects.select_related(
+            'interface__device'
+        ).exclude(
+            address=str(ipaddress.address)
+        ).filter(
+            vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
+        )
+        related_ips_table = tables.IPAddressBriefTable(list(related_ips))
+
+        return render(request, 'ipam/ipaddress.html', {
+            'ipaddress': ipaddress,
+            'parent_prefixes_table': parent_prefixes_table,
+            'duplicate_ips_table': duplicate_ips_table,
+            'related_ips_table': related_ips_table,
+        })
 
 
 class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
@@ -669,17 +714,21 @@ class VLANListView(ObjectListView):
     template_name = 'ipam/vlan_list.html'
 
 
-def vlan(request, pk):
+class VLANView(View):
 
-    vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
-    prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
-    prefix_table = tables.PrefixBriefTable(list(prefixes))
-    prefix_table.exclude = ('vlan',)
+    def get(self, request, pk):
 
-    return render(request, 'ipam/vlan.html', {
-        'vlan': vlan,
-        'prefix_table': prefix_table,
-    })
+        vlan = get_object_or_404(VLAN.objects.select_related(
+            'site__region', 'tenant__group', 'role'
+        ), pk=pk)
+        prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
+        prefix_table = tables.PrefixBriefTable(list(prefixes))
+        prefix_table.exclude = ('vlan',)
+
+        return render(request, 'ipam/vlan.html', {
+            'vlan': vlan,
+            'prefix_table': prefix_table,
+        })
 
 
 class VLANEditView(PermissionRequiredMixin, ObjectEditView):

+ 1 - 1
netbox/secrets/urls.py

@@ -17,7 +17,7 @@ urlpatterns = [
     url(r'^secrets/import/$', views.secret_import, name='secret_import'),
     url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
     url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
-    url(r'^secrets/(?P<pk>\d+)/$', views.secret, name='secret'),
+    url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
     url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
     url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
 

+ 9 - 6
netbox/secrets/views.py

@@ -8,6 +8,7 @@ from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils.decorators import method_decorator
+from django.views.generic import View
 
 from dcim.models import Device
 from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
@@ -65,14 +66,16 @@ class SecretListView(ObjectListView):
     template_name = 'secrets/secret_list.html'
 
 
-@login_required
-def secret(request, pk):
+@method_decorator(login_required, name='dispatch')
+class SecretView(View):
 
-    secret = get_object_or_404(Secret, pk=pk)
+    def get(self, request, pk):
 
-    return render(request, 'secrets/secret.html', {
-        'secret': secret,
-    })
+        secret = get_object_or_404(Secret, pk=pk)
+
+        return render(request, 'secrets/secret.html', {
+            'secret': secret,
+        })
 
 
 @permission_required('secrets.add_secret')

+ 1 - 1
netbox/tenancy/urls.py

@@ -18,7 +18,7 @@ urlpatterns = [
     url(r'^tenants/import/$', views.TenantBulkImportView.as_view(), name='tenant_import'),
     url(r'^tenants/edit/$', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'),
     url(r'^tenants/delete/$', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'),
-    url(r'^tenants/(?P<slug>[\w-]+)/$', views.tenant, name='tenant'),
+    url(r'^tenants/(?P<slug>[\w-]+)/$', views.TenantView.as_view(), name='tenant'),
     url(r'^tenants/(?P<slug>[\w-]+)/edit/$', views.TenantEditView.as_view(), name='tenant_edit'),
     url(r'^tenants/(?P<slug>[\w-]+)/delete/$', views.TenantDeleteView.as_view(), name='tenant_delete'),
 

+ 27 - 24
netbox/tenancy/views.py

@@ -2,6 +2,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count, Q
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
+from django.views.generic import View
 
 from circuits.models import Circuit
 from dcim.models import Site, Rack, Device
@@ -51,30 +52,32 @@ class TenantListView(ObjectListView):
     template_name = 'tenancy/tenant_list.html'
 
 
-def tenant(request, slug):
-
-    tenant = get_object_or_404(Tenant, slug=slug)
-    stats = {
-        'site_count': Site.objects.filter(tenant=tenant).count(),
-        'rack_count': Rack.objects.filter(tenant=tenant).count(),
-        'device_count': Device.objects.filter(tenant=tenant).count(),
-        'vrf_count': VRF.objects.filter(tenant=tenant).count(),
-        'prefix_count': Prefix.objects.filter(
-            Q(tenant=tenant) |
-            Q(tenant__isnull=True, vrf__tenant=tenant)
-        ).count(),
-        'ipaddress_count': IPAddress.objects.filter(
-            Q(tenant=tenant) |
-            Q(tenant__isnull=True, vrf__tenant=tenant)
-        ).count(),
-        'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
-        'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
-    }
-
-    return render(request, 'tenancy/tenant.html', {
-        'tenant': tenant,
-        'stats': stats,
-    })
+class TenantView(View):
+
+    def get(self, request, slug):
+
+        tenant = get_object_or_404(Tenant, slug=slug)
+        stats = {
+            'site_count': Site.objects.filter(tenant=tenant).count(),
+            'rack_count': Rack.objects.filter(tenant=tenant).count(),
+            'device_count': Device.objects.filter(tenant=tenant).count(),
+            'vrf_count': VRF.objects.filter(tenant=tenant).count(),
+            'prefix_count': Prefix.objects.filter(
+                Q(tenant=tenant) |
+                Q(tenant__isnull=True, vrf__tenant=tenant)
+            ).count(),
+            'ipaddress_count': IPAddress.objects.filter(
+                Q(tenant=tenant) |
+                Q(tenant__isnull=True, vrf__tenant=tenant)
+            ).count(),
+            'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
+            'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
+        }
+
+        return render(request, 'tenancy/tenant.html', {
+            'tenant': tenant,
+            'stats': stats,
+        })
 
 
 class TenantEditView(PermissionRequiredMixin, ObjectEditView):