Bläddra i källkod

Merge pull request #4790 from netbox-community/4788-component-views

#4788: Add individual views for device components
Jeremy Stretch 5 år sedan
förälder
incheckning
6d23d9ebb7

+ 1 - 0
docs/release-notes/version-2.9.md

@@ -14,6 +14,7 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
 * [#3703](https://github.com/netbox-community/netbox/issues/3703) - Tags must be created administratively before being assigned to an object
 * [#4615](https://github.com/netbox-community/netbox/issues/4615) - Add `label` field for all device components
 * [#4742](https://github.com/netbox-community/netbox/issues/4742) - Add tagging for cables, power panels, and rack reservations
+* [#4788](https://github.com/netbox-community/netbox/issues/4788) - Add dedicated views for all device components
 
 ### Configuration Changes
 

+ 11 - 5
netbox/dcim/models/device_components.py

@@ -268,7 +268,7 @@ class ConsolePort(CableTermination, ComponentModel):
         unique_together = ('device', 'name')
 
     def get_absolute_url(self):
-        return self.device.get_absolute_url()
+        return reverse('dcim:consoleport', kwargs={'pk': self.pk})
 
     def to_csv(self):
         return (
@@ -325,7 +325,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
         unique_together = ('device', 'name')
 
     def get_absolute_url(self):
-        return self.device.get_absolute_url()
+        return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
 
     def to_csv(self):
         return (
@@ -408,7 +408,7 @@ class PowerPort(CableTermination, ComponentModel):
         unique_together = ('device', 'name')
 
     def get_absolute_url(self):
-        return self.device.get_absolute_url()
+        return reverse('dcim:powerport', kwargs={'pk': self.pk})
 
     def to_csv(self):
         return (
@@ -560,7 +560,7 @@ class PowerOutlet(CableTermination, ComponentModel):
         unique_together = ('device', 'name')
 
     def get_absolute_url(self):
-        return self.device.get_absolute_url()
+        return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
 
     def to_csv(self):
         return (
@@ -881,6 +881,9 @@ class FrontPort(CableTermination, ComponentModel):
     def __str__(self):
         return self.name
 
+    def get_absolute_url(self):
+        return reverse('dcim:frontport', kwargs={'pk': self.pk})
+
     def to_csv(self):
         return (
             self.device.identifier,
@@ -946,6 +949,9 @@ class RearPort(CableTermination, ComponentModel):
     def __str__(self):
         return self.name
 
+    def get_absolute_url(self):
+        return reverse('dcim:rearport', kwargs={'pk': self.pk})
+
     def to_csv(self):
         return (
             self.device.identifier,
@@ -1005,7 +1011,7 @@ class DeviceBay(ComponentModel):
         return '{} - {}'.format(self.device.name, self.name)
 
     def get_absolute_url(self):
-        return self.device.get_absolute_url()
+        return reverse('dcim:devicebay', kwargs={'pk': self.pk})
 
     def to_csv(self):
         return (

+ 51 - 187
netbox/dcim/tables.py

@@ -490,18 +490,6 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
         empty_text = "None"
 
 
-class ConsolePortImportTable(BaseTable):
-    device = tables.LinkColumn(
-        viewname='dcim:device',
-        args=[Accessor('device.pk')]
-    )
-
-    class Meta(BaseTable.Meta):
-        model = ConsolePort
-        fields = ('device', 'name', 'description')
-        empty_text = False
-
-
 class ConsoleServerPortTemplateTable(ComponentTemplateTable):
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('consoleserverporttemplate'),
@@ -515,18 +503,6 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
         empty_text = "None"
 
 
-class ConsoleServerPortImportTable(BaseTable):
-    device = tables.LinkColumn(
-        viewname='dcim:device',
-        args=[Accessor('device.pk')]
-    )
-
-    class Meta(BaseTable.Meta):
-        model = ConsoleServerPort
-        fields = ('device', 'name', 'description')
-        empty_text = False
-
-
 class PowerPortTemplateTable(ComponentTemplateTable):
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('powerporttemplate'),
@@ -540,18 +516,6 @@ class PowerPortTemplateTable(ComponentTemplateTable):
         empty_text = "None"
 
 
-class PowerPortImportTable(BaseTable):
-    device = tables.LinkColumn(
-        viewname='dcim:device',
-        args=[Accessor('device.pk')]
-    )
-
-    class Meta(BaseTable.Meta):
-        model = PowerPort
-        fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
-        empty_text = False
-
-
 class PowerOutletTemplateTable(ComponentTemplateTable):
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('poweroutlettemplate'),
@@ -565,18 +529,6 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
         empty_text = "None"
 
 
-class PowerOutletImportTable(BaseTable):
-    device = tables.LinkColumn(
-        viewname='dcim:device',
-        args=[Accessor('device.pk')]
-    )
-
-    class Meta(BaseTable.Meta):
-        model = PowerOutlet
-        fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
-        empty_text = False
-
-
 class InterfaceTemplateTable(ComponentTemplateTable):
     mgmt_only = BooleanColumn(
         verbose_name='Management Only'
@@ -593,20 +545,6 @@ class InterfaceTemplateTable(ComponentTemplateTable):
         empty_text = "None"
 
 
-class InterfaceImportTable(BaseTable):
-    device = tables.LinkColumn(
-        viewname='dcim:device',
-        args=[Accessor('device.pk')]
-    )
-
-    class Meta(BaseTable.Meta):
-        model = Interface
-        fields = (
-            'device', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode',
-        )
-        empty_text = False
-
-
 class FrontPortTemplateTable(ComponentTemplateTable):
     rear_port_position = tables.Column(
         verbose_name='Position'
@@ -623,18 +561,6 @@ class FrontPortTemplateTable(ComponentTemplateTable):
         empty_text = "None"
 
 
-class FrontPortImportTable(BaseTable):
-    device = tables.LinkColumn(
-        viewname='dcim:device',
-        args=[Accessor('device.pk')]
-    )
-
-    class Meta(BaseTable.Meta):
-        model = FrontPort
-        fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
-        empty_text = False
-
-
 class RearPortTemplateTable(ComponentTemplateTable):
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('rearporttemplate'),
@@ -648,18 +574,6 @@ class RearPortTemplateTable(ComponentTemplateTable):
         empty_text = "None"
 
 
-class RearPortImportTable(BaseTable):
-    device = tables.LinkColumn(
-        viewname='dcim:device',
-        args=[Accessor('device.pk')]
-    )
-
-    class Meta(BaseTable.Meta):
-        model = RearPort
-        fields = ('device', 'name', 'description', 'type', 'position')
-        empty_text = False
-
-
 class DeviceBayTemplateTable(ComponentTemplateTable):
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('devicebaytemplate'),
@@ -855,144 +769,94 @@ class DeviceImportTable(BaseTable):
 # Device components
 #
 
-class DeviceComponentDetailTable(BaseTable):
+class DeviceComponentTable(BaseTable):
     pk = ToggleColumn()
-    device = tables.LinkColumn()
-    name = tables.Column(order_by=('_name',))
-    cable = tables.LinkColumn()
+    device = tables.Column(
+        linkify=True
+    )
+    name = tables.Column(
+        linkify=True,
+        order_by=('_name',)
+    )
+    cable = tables.Column(
+        linkify=True
+    )
 
     class Meta(BaseTable.Meta):
         order_by = ('device', 'name')
-        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
-        sequence = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
 
 
-class ConsolePortTable(BaseTable):
-    name = tables.Column(order_by=('_name',))
+class ConsolePortTable(DeviceComponentTable):
 
-    class Meta(BaseTable.Meta):
+    class Meta(DeviceComponentTable.Meta):
         model = ConsolePort
-        fields = ('name', 'label', 'type')
-
-
-class ConsolePortDetailTable(DeviceComponentDetailTable):
-
-    class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
-        pass
+        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
 
 
-class ConsoleServerPortTable(BaseTable):
-    name = tables.Column(order_by=('_name',))
+class ConsoleServerPortTable(DeviceComponentTable):
 
-    class Meta(BaseTable.Meta):
+    class Meta(DeviceComponentTable.Meta):
         model = ConsoleServerPort
-        fields = ('name', 'label', 'description')
-
-
-class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
-
-    class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
-        pass
+        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
 
 
-class PowerPortTable(BaseTable):
-    name = tables.Column(order_by=('_name',))
+class PowerPortTable(DeviceComponentTable):
 
-    class Meta(BaseTable.Meta):
+    class Meta(DeviceComponentTable.Meta):
         model = PowerPort
-        fields = ('name', 'label', 'type')
+        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
 
 
-class PowerPortDetailTable(DeviceComponentDetailTable):
+class PowerOutletTable(DeviceComponentTable):
 
-    class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
-        pass
-
-
-class PowerOutletTable(BaseTable):
-    name = tables.Column(order_by=('_name',))
-
-    class Meta(BaseTable.Meta):
+    class Meta(DeviceComponentTable.Meta):
         model = PowerOutlet
-        fields = ('name', 'label', 'type', 'description')
-
-
-class PowerOutletDetailTable(DeviceComponentDetailTable):
-
-    class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
-        pass
-
+        fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
 
-class InterfaceTable(BaseTable):
 
-    class Meta(BaseTable.Meta):
-        model = Interface
-        fields = ('name', 'label', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
-
-
-class InterfaceDetailTable(DeviceComponentDetailTable):
+class InterfaceTable(DeviceComponentTable):
     enabled = BooleanColumn()
 
-    class Meta(DeviceComponentDetailTable.Meta, InterfaceTable.Meta):
-        fields = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description', 'cable')
-        sequence = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description', 'cable')
+    class Meta(DeviceComponentTable.Meta):
+        model = Interface
+        fields = (
+            'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'description', 'cable',
+        )
+        default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
 
 
-class FrontPortTable(BaseTable):
-    name = tables.Column(order_by=('_name',))
+class FrontPortTable(DeviceComponentTable):
+    rear_port_position = tables.Column(
+        verbose_name='Position'
+    )
 
-    class Meta(BaseTable.Meta):
+    class Meta(DeviceComponentTable.Meta):
         model = FrontPort
-        fields = ('name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
-        empty_text = "None"
+        fields = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
 
 
-class FrontPortDetailTable(DeviceComponentDetailTable):
+class RearPortTable(DeviceComponentTable):
 
-    class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
-        pass
-
-
-class RearPortTable(BaseTable):
-    name = tables.Column(order_by=('_name',))
-
-    class Meta(BaseTable.Meta):
+    class Meta(DeviceComponentTable.Meta):
         model = RearPort
-        fields = ('name', 'label', 'type', 'positions', 'description')
-        empty_text = "None"
-
-
-class RearPortDetailTable(DeviceComponentDetailTable):
-
-    class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
-        pass
+        fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
 
 
-class DeviceBayTable(BaseTable):
-    name = tables.Column(order_by=('_name',))
+class DeviceBayTable(DeviceComponentTable):
+    installed_device = tables.Column(
+        linkify=True
+    )
 
-    class Meta(BaseTable.Meta):
+    class Meta(DeviceComponentTable.Meta):
         model = DeviceBay
-        fields = ('name', 'label', 'description')
-
-
-class DeviceBayDetailTable(DeviceComponentDetailTable):
-    installed_device = tables.LinkColumn()
-
-    class Meta(DeviceBayTable.Meta):
         fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
-        sequence = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
-        exclude = ('cable',)
-
-
-class DeviceBayImportTable(BaseTable):
-    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
-    installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
-
-    class Meta(BaseTable.Meta):
-        model = DeviceBay
-        fields = ('device', 'name', 'installed_device', 'description')
-        empty_text = False
+        default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
 
 
 #

+ 11 - 5
netbox/dcim/tests/test_views.py

@@ -1194,10 +1194,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
         )
 
 
-class InterfaceTestCase(
-    ViewTestCases.GetObjectViewTestCase,
-    ViewTestCases.DeviceComponentViewTestCase,
-):
+class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = Interface
 
     @classmethod
@@ -1425,7 +1422,16 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
         )
 
 
-class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
+# TODO: Convert to DeviceComponentViewTestCase?
+class InventoryItemTestCase(
+    ViewTestCases.EditObjectViewTestCase,
+    ViewTestCases.DeleteObjectViewTestCase,
+    ViewTestCases.ListObjectsViewTestCase,
+    ViewTestCases.BulkCreateObjectsViewTestCase,
+    ViewTestCases.BulkImportObjectsViewTestCase,
+    ViewTestCases.BulkEditObjectsViewTestCase,
+    ViewTestCases.BulkDeleteObjectsViewTestCase
+):
     model = InventoryItem
 
     @classmethod

+ 24 - 10
netbox/dcim/urls.py

@@ -4,9 +4,9 @@ from extras.views import ObjectChangeLogView, ImageAttachmentEditView
 from ipam.views import ServiceEditView
 from . import views
 from .models import (
-    Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
-    PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site,
-    VirtualChassis,
+    Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, FrontPort, Interface,
+    Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole,
+    RearPort, Region, Site, VirtualChassis,
 )
 
 app_name = 'dcim'
@@ -189,10 +189,12 @@ urlpatterns = [
     path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'),
     # TODO: Bulk rename, disconnect views for ConsolePorts
     path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
-    path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
+    path('console-ports/<int:pk>/', views.ConsolePortView.as_view(), name='consoleport'),
     path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
     path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
+    path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
     path('console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
+    path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
     path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
 
     # Console server ports
@@ -203,10 +205,12 @@ urlpatterns = [
     path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
     path('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
     path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
-    path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
+    path('console-server-ports/<int:pk>/', views.ConsoleServerPortView.as_view(), name='consoleserverport'),
     path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
     path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
+    path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
     path('console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
+    path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
     path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
 
     # Power ports
@@ -216,10 +220,12 @@ urlpatterns = [
     path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'),
     # TODO: Bulk rename, disconnect views for PowerPorts
     path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
-    path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
+    path('power-ports/<int:pk>/', views.PowerPortView.as_view(), name='powerport'),
     path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
     path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
+    path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
     path('power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
+    path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
     path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
 
     # Power outlets
@@ -230,10 +236,12 @@ urlpatterns = [
     path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
     path('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
     path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
-    path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
+    path('power-outlets/<int:pk>/', views.PowerOutletView.as_view(), name='poweroutlet'),
     path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
     path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
+    path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
     path('power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
+    path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
     path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
 
     # Interfaces
@@ -244,12 +252,12 @@ urlpatterns = [
     path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
     path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
     path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
-    path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
     path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
     path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
     path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
     path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
     path('interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
+    path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
     path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
 
     # Front ports
@@ -260,10 +268,12 @@ urlpatterns = [
     path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
     path('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
     path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
-    path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
+    path('front-ports/<int:pk>/', views.FrontPortView.as_view(), name='frontport'),
     path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
     path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
+    path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
     path('front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
+    path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
     # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
 
     # Rear ports
@@ -274,10 +284,12 @@ urlpatterns = [
     path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
     path('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
     path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
-    path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
+    path('rear-ports/<int:pk>/', views.RearPortView.as_view(), name='rearport'),
     path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
     path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
+    path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
     path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
+    path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
     path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
 
     # Device bays
@@ -287,8 +299,10 @@ urlpatterns = [
     path('device-bays/edit/', views.DeviceBayBulkEditView.as_view(), name='devicebay_bulk_edit'),
     path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
     path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
+    path('device-bays/<int:pk>/', views.DeviceBayView.as_view(), name='devicebay'),
     path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
     path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
+    path('device-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicebay_changelog', kwargs={'model': DeviceBay}),
     path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
     path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
     path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),

+ 53 - 27
netbox/dcim/views.py

@@ -1158,13 +1158,17 @@ class DeviceBulkDeleteView(BulkDeleteView):
 #
 
 class ConsolePortListView(ObjectListView):
-    queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    queryset = ConsolePort.objects.prefetch_related('device', 'cable')
     filterset = filters.ConsolePortFilterSet
     filterset_form = forms.ConsolePortFilterForm
-    table = tables.ConsolePortDetailTable
+    table = tables.ConsolePortTable
     action_buttons = ('import', 'export')
 
 
+class ConsolePortView(ObjectView):
+    queryset = ConsolePort.objects.all()
+
+
 class ConsolePortCreateView(ComponentCreateView):
     queryset = ConsolePort.objects.all()
     form = forms.ConsolePortCreateForm
@@ -1184,7 +1188,7 @@ class ConsolePortDeleteView(ObjectDeleteView):
 class ConsolePortBulkImportView(BulkImportView):
     queryset = ConsolePort.objects.all()
     model_form = forms.ConsolePortCSVForm
-    table = tables.ConsolePortImportTable
+    table = tables.ConsolePortTable
     default_return_url = 'dcim:consoleport_list'
 
 
@@ -1207,13 +1211,17 @@ class ConsolePortBulkDeleteView(BulkDeleteView):
 #
 
 class ConsoleServerPortListView(ObjectListView):
-    queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    queryset = ConsoleServerPort.objects.prefetch_related('device', 'cable')
     filterset = filters.ConsoleServerPortFilterSet
     filterset_form = forms.ConsoleServerPortFilterForm
-    table = tables.ConsoleServerPortDetailTable
+    table = tables.ConsoleServerPortTable
     action_buttons = ('import', 'export')
 
 
+class ConsoleServerPortView(ObjectView):
+    queryset = ConsoleServerPort.objects.all()
+
+
 class ConsoleServerPortCreateView(ComponentCreateView):
     queryset = ConsoleServerPort.objects.all()
     form = forms.ConsoleServerPortCreateForm
@@ -1233,7 +1241,7 @@ class ConsoleServerPortDeleteView(ObjectDeleteView):
 class ConsoleServerPortBulkImportView(BulkImportView):
     queryset = ConsoleServerPort.objects.all()
     model_form = forms.ConsoleServerPortCSVForm
-    table = tables.ConsoleServerPortImportTable
+    table = tables.ConsoleServerPortTable
     default_return_url = 'dcim:consoleserverport_list'
 
 
@@ -1266,13 +1274,17 @@ class ConsoleServerPortBulkDeleteView(BulkDeleteView):
 #
 
 class PowerPortListView(ObjectListView):
-    queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    queryset = PowerPort.objects.prefetch_related('device', 'cable')
     filterset = filters.PowerPortFilterSet
     filterset_form = forms.PowerPortFilterForm
-    table = tables.PowerPortDetailTable
+    table = tables.PowerPortTable
     action_buttons = ('import', 'export')
 
 
+class PowerPortView(ObjectView):
+    queryset = PowerPort.objects.all()
+
+
 class PowerPortCreateView(ComponentCreateView):
     queryset = PowerPort.objects.all()
     form = forms.PowerPortCreateForm
@@ -1292,7 +1304,7 @@ class PowerPortDeleteView(ObjectDeleteView):
 class PowerPortBulkImportView(BulkImportView):
     queryset = PowerPort.objects.all()
     model_form = forms.PowerPortCSVForm
-    table = tables.PowerPortImportTable
+    table = tables.PowerPortTable
     default_return_url = 'dcim:powerport_list'
 
 
@@ -1315,13 +1327,17 @@ class PowerPortBulkDeleteView(BulkDeleteView):
 #
 
 class PowerOutletListView(ObjectListView):
-    queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    queryset = PowerOutlet.objects.prefetch_related('device', 'cable')
     filterset = filters.PowerOutletFilterSet
     filterset_form = forms.PowerOutletFilterForm
-    table = tables.PowerOutletDetailTable
+    table = tables.PowerOutletTable
     action_buttons = ('import', 'export')
 
 
+class PowerOutletView(ObjectView):
+    queryset = PowerOutlet.objects.all()
+
+
 class PowerOutletCreateView(ComponentCreateView):
     queryset = PowerOutlet.objects.all()
     form = forms.PowerOutletCreateForm
@@ -1341,7 +1357,7 @@ class PowerOutletDeleteView(ObjectDeleteView):
 class PowerOutletBulkImportView(BulkImportView):
     queryset = PowerOutlet.objects.all()
     model_form = forms.PowerOutletCSVForm
-    table = tables.PowerOutletImportTable
+    table = tables.PowerOutletTable
     default_return_url = 'dcim:poweroutlet_list'
 
 
@@ -1374,10 +1390,10 @@ class PowerOutletBulkDeleteView(BulkDeleteView):
 #
 
 class InterfaceListView(ObjectListView):
-    queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    queryset = Interface.objects.prefetch_related('device', 'cable')
     filterset = filters.InterfaceFilterSet
     filterset_form = forms.InterfaceFilterForm
-    table = tables.InterfaceDetailTable
+    table = tables.InterfaceTable
     action_buttons = ('import', 'export')
 
 
@@ -1409,7 +1425,7 @@ class InterfaceView(ObjectView):
         )
 
         return render(request, 'dcim/interface.html', {
-            'interface': interface,
+            'instance': interface,
             'connected_interface': interface._connected_interface,
             'connected_circuittermination': interface._connected_circuittermination,
             'ipaddress_table': ipaddress_table,
@@ -1437,7 +1453,7 @@ class InterfaceDeleteView(ObjectDeleteView):
 class InterfaceBulkImportView(BulkImportView):
     queryset = Interface.objects.all()
     model_form = forms.InterfaceCSVForm
-    table = tables.InterfaceImportTable
+    table = tables.InterfaceTable
     default_return_url = 'dcim:interface_list'
 
 
@@ -1470,13 +1486,17 @@ class InterfaceBulkDeleteView(BulkDeleteView):
 #
 
 class FrontPortListView(ObjectListView):
-    queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    queryset = FrontPort.objects.prefetch_related('device', 'cable')
     filterset = filters.FrontPortFilterSet
     filterset_form = forms.FrontPortFilterForm
-    table = tables.FrontPortDetailTable
+    table = tables.FrontPortTable
     action_buttons = ('import', 'export')
 
 
+class FrontPortView(ObjectView):
+    queryset = FrontPort.objects.all()
+
+
 class FrontPortCreateView(ComponentCreateView):
     queryset = FrontPort.objects.all()
     form = forms.FrontPortCreateForm
@@ -1496,7 +1516,7 @@ class FrontPortDeleteView(ObjectDeleteView):
 class FrontPortBulkImportView(BulkImportView):
     queryset = FrontPort.objects.all()
     model_form = forms.FrontPortCSVForm
-    table = tables.FrontPortImportTable
+    table = tables.FrontPortTable
     default_return_url = 'dcim:frontport_list'
 
 
@@ -1529,13 +1549,17 @@ class FrontPortBulkDeleteView(BulkDeleteView):
 #
 
 class RearPortListView(ObjectListView):
-    queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    queryset = RearPort.objects.prefetch_related('device', 'cable')
     filterset = filters.RearPortFilterSet
     filterset_form = forms.RearPortFilterForm
-    table = tables.RearPortDetailTable
+    table = tables.RearPortTable
     action_buttons = ('import', 'export')
 
 
+class RearPortView(ObjectView):
+    queryset = RearPort.objects.all()
+
+
 class RearPortCreateView(ComponentCreateView):
     queryset = RearPort.objects.all()
     form = forms.RearPortCreateForm
@@ -1555,7 +1579,7 @@ class RearPortDeleteView(ObjectDeleteView):
 class RearPortBulkImportView(BulkImportView):
     queryset = RearPort.objects.all()
     model_form = forms.RearPortCSVForm
-    table = tables.RearPortImportTable
+    table = tables.RearPortTable
     default_return_url = 'dcim:rearport_list'
 
 
@@ -1588,15 +1612,17 @@ class RearPortBulkDeleteView(BulkDeleteView):
 #
 
 class DeviceBayListView(ObjectListView):
-    queryset = DeviceBay.objects.prefetch_related(
-        'device', 'device__site', 'installed_device', 'installed_device__site'
-    )
+    queryset = DeviceBay.objects.prefetch_related('device', 'installed_device')
     filterset = filters.DeviceBayFilterSet
     filterset_form = forms.DeviceBayFilterForm
-    table = tables.DeviceBayDetailTable
+    table = tables.DeviceBayTable
     action_buttons = ('import', 'export')
 
 
+class DeviceBayView(ObjectView):
+    queryset = DeviceBay.objects.all()
+
+
 class DeviceBayCreateView(ComponentCreateView):
     queryset = DeviceBay.objects.all()
     form = forms.DeviceBayCreateForm
@@ -1683,7 +1709,7 @@ class DeviceBayDepopulateView(ObjectEditView):
 class DeviceBayBulkImportView(BulkImportView):
     queryset = DeviceBay.objects.all()
     model_form = forms.DeviceBayCSVForm
-    table = tables.DeviceBayImportTable
+    table = tables.DeviceBayTable
     default_return_url = 'dcim:devicebay_list'
 
 

+ 1 - 0
netbox/extras/views.py

@@ -296,6 +296,7 @@ class ObjectChangeLogView(View):
 
         return render(request, 'extras/object_changelog.html', {
             object_var: obj,
+            'instance': obj,  # We'll eventually standardize on 'instance` for the object variable name
             'table': objectchanges_table,
             'base_template': base_template,
             'active_tab': 'changelog',

+ 103 - 0
netbox/templates/dcim/consoleport.html

@@ -0,0 +1,103 @@
+{% extends 'dcim/device_component.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Console Port</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Type</td>
+                        <td>{{ instance.get_type_display }}</td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Connection</strong>
+                </div>
+                {% if instance.cable %}
+                    <table class="table table-hover panel-body attr-table">
+                        {% if instance.connected_endpoint %}
+                            <tr>
+                                <td>Device</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Name</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Type</td>
+                                <td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
+                            </tr>
+                            <tr>
+                                <td>Description</td>
+                                <td>{{ instance.connected_endpoint.description|placeholder }}</td>
+                            </tr>
+                        {% endif %}
+                        <tr>
+                            <td>Cable</td>
+                            <td>
+                                <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
+                                <a href="{% url 'dcim:consoleport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                    <i class="fa fa-share-alt" aria-hidden="true"></i>
+                                </a>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Connection Status</td>
+                            <td>
+                                {% if instance.connection_status %}
+                                    <span class="label label-success">{{ instance.get_connection_status_display }}</span>
+                                {% else %}
+                                    <span class="label label-info">{{ instance.get_connection_status_display }}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    </table>
+                {% else %}
+                    <div class="panel-body text-muted">
+                        Not connected
+                    </div>
+                {% endif %}
+            </div>
+            {% plugin_right_page instance %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
+    </div>
+{% endblock %}

+ 103 - 0
netbox/templates/dcim/consoleserverport.html

@@ -0,0 +1,103 @@
+{% extends 'dcim/device_component.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Console Server Port</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Type</td>
+                        <td>{{ instance.get_type_display }}</td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Connection</strong>
+                </div>
+                {% if instance.cable %}
+                    <table class="table table-hover panel-body attr-table">
+                        {% if instance.connected_endpoint %}
+                            <tr>
+                                <td>Device</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Name</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Type</td>
+                                <td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
+                            </tr>
+                            <tr>
+                                <td>Description</td>
+                                <td>{{ instance.connected_endpoint.description|placeholder }}</td>
+                            </tr>
+                        {% endif %}
+                        <tr>
+                            <td>Cable</td>
+                            <td>
+                                <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
+                                <a href="{% url 'dcim:consoleserverport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                    <i class="fa fa-share-alt" aria-hidden="true"></i>
+                                </a>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Connection Status</td>
+                            <td>
+                                {% if instance.connection_status %}
+                                    <span class="label label-success">{{ instance.get_connection_status_display }}</span>
+                                {% else %}
+                                    <span class="label label-info">{{ instance.get_connection_status_display }}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    </table>
+                {% else %}
+                    <div class="panel-body text-muted">
+                        Not connected
+                    </div>
+                {% endif %}
+            </div>
+            {% plugin_right_page instance %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
+    </div>
+{% endblock %}

+ 41 - 0
netbox/templates/dcim/device_component.html

@@ -0,0 +1,41 @@
+{% extends 'base.html' %}
+{% load helpers %}
+{% load perms %}
+{% load plugins %}
+
+{% block header %}
+    <div class="row noprint">
+        <div class="col-md-12">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
+                <li><a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a></li>
+                <li><a href="{% url instance|url_name:"list" %}?device_id={{ instance.device.pk }}">{{ instance|meta:"verbose_name_plural"|bettertitle }}</a></li>
+                <li>{{ instance }}</li>
+            </ol>
+        </div>
+    </div>
+    <div class="pull-right noprint">
+        {% plugin_buttons instance %}
+        {% if request.user|can_change:instance %}
+            <a href="{% url instance|url_name:"edit" pk=instance.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span> Edit
+            </a>
+        {% endif %}
+        {% if request.user|can_delete:instance %}
+            <a href="{% url instance|url_name:"delete" pk=instance.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span> Delete
+            </a>
+        {% endif %}
+    </div>
+    <h1>{% block title %}{{ instance.device }} / {{ instance }}{% endblock %}</h1>
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ instance.get_absolute_url }}">{{ instance|meta:"verbose_name"|bettertitle }}</a>
+        </li>
+        {% if perms.extras.view_objectchange %}
+            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+                <a href="{% url instance|url_name:"changelog" pk=instance.pk %}">Change Log</a>
+            </li>
+        {% endif %}
+    </ul>
+{% endblock %}

+ 70 - 0
netbox/templates/dcim/devicebay.html

@@ -0,0 +1,70 @@
+{% extends 'dcim/device_component.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Device Bay</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Installed Device</strong>
+                </div>
+                {% if instance.installed_device %}
+                    {% with device=instance.installed_device %}
+                        <table class="table table-hover panel-body attr-table">
+                            <tr>
+                                <td>Device</td>
+                                <td>
+                                    <a href="{{ device.get_absolute_url }}">{{ device }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Device Type</td>
+                                <td>{{ device.device_type }}</td>
+                            </tr>
+                        </table>
+                    {% endwith %}
+                {% else %}
+                    <div class="panel-body text-muted">
+                        None
+                    </div>
+                {% endif %}
+            </div>
+            {% plugin_right_page instance %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
+    </div>
+{% endblock %}

+ 91 - 0
netbox/templates/dcim/frontport.html

@@ -0,0 +1,91 @@
+{% extends 'dcim/device_component.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Front Port</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Type</td>
+                        <td>{{ instance.get_type_display }}</td>
+                    </tr>
+                    <tr>
+                        <td>Rear Port</td>
+                        <td>
+                            <a href="{{ instance.rear_port.get_absolute_url }}">{{ instance.rear_port }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Rear Port Position</td>
+                        <td>{{ instance.rear_port_position }}</td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Connection</strong>
+                </div>
+                {% if instance.cable %}
+                    <table class="table table-hover panel-body attr-table">
+                        <tr>
+                            <td>Cable</td>
+                            <td>
+                                <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
+                                <a href="{% url 'dcim:frontport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                    <i class="fa fa-share-alt" aria-hidden="true"></i>
+                                </a>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Connection Status</td>
+                            <td>
+                                {% if instance.cable.status %}
+                                    <span class="label label-success">{{ instance.cable.get_status_display }}</span>
+                                {% else %}
+                                    <span class="label label-info">{{ instance.cable.get_status_display }}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    </table>
+                {% else %}
+                    <div class="panel-body text-muted">
+                        Not connected
+                    </div>
+                {% endif %}
+            </div>
+            {% plugin_right_page instance %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
+    </div>
+{% endblock %}

+ 2 - 1
netbox/templates/dcim/inc/consoleport.html

@@ -2,7 +2,8 @@
 
     {# Name #}
     <td>
-        <i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
+        <i class="fa fa-fw fa-keyboard-o"></i>
+        <a href="{{ cp.get_absolute_url }}">{{ cp }}</a>
     </td>
 
     {# Type #}

+ 2 - 1
netbox/templates/dcim/inc/consoleserverport.html

@@ -11,7 +11,8 @@
 
     {# Name #}
     <td>
-        <i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
+        <i class="fa fa-fw fa-keyboard-o"></i>
+        <a href="{{ csp.get_absolute_url }}">{{ csp }}</a>
     </td>
 
     {# Type #}

+ 2 - 1
netbox/templates/dcim/inc/devicebay.html

@@ -9,7 +9,8 @@
 
     {# Name #}
     <td>
-        <i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
+        <i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i>
+        <a href="{{ devicebay.get_absolute_url }}">{{ devicebay.name }}</a>
     </td>
 
     {# Status #}

+ 2 - 1
netbox/templates/dcim/inc/frontport.html

@@ -10,7 +10,8 @@
 
     {# Name #}
     <td>
-        <i class="fa fa-fw fa-square{% if not frontport.cable %}-o{% endif %}"></i> {{ frontport }}
+        <i class="fa fa-fw fa-square{% if not frontport.cable %}-o{% endif %}"></i>
+        <a href="{{ frontport.get_absolute_url }}">{{ frontport }}</a>
     </td>
 
     {# Type #}

+ 2 - 1
netbox/templates/dcim/inc/poweroutlet.html

@@ -11,7 +11,8 @@
 
     {# Name #}
     <td>
-        <i class="fa fa-fw fa-bolt"></i> {{ po }}
+        <i class="fa fa-fw fa-bolt"></i>
+        <a href="{{ po.get_absolute_url }}">{{ po }}</a>
     </td>
 
     {# Type #}

+ 2 - 1
netbox/templates/dcim/inc/powerport.html

@@ -2,7 +2,8 @@
 
     {# Name #}
     <td>
-        <i class="fa fa-fw fa-bolt"></i> {{ pp }}
+        <i class="fa fa-fw fa-bolt"></i>
+        <a href="{{ pp.get_absolute_url }}">{{ pp }}</a>
     </td>
 
     {# Type #}

+ 2 - 1
netbox/templates/dcim/inc/rearport.html

@@ -10,7 +10,8 @@
 
     {# Name #}
     <td>
-        <i class="fa fa-fw fa-square{% if not rearport.cable %}-o{% endif %}"></i> {{ rearport }}
+        <i class="fa fa-fw fa-square{% if not rearport.cable %}-o{% endif %}"></i>
+        <a href="{{ rearport.get_absolute_url }}">{{ rearport }}</a>
     </td>
 
     {# Type #}

+ 200 - 227
netbox/templates/dcim/interface.html

@@ -1,254 +1,227 @@
-{% extends 'base.html' %}
+{% extends 'dcim/device_component.html' %}
 {% load helpers %}
-
-{% block header %}
-    <div class="row noprint">
-        <div class="col-md-12">
-            <ol class="breadcrumb">
-                <li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
-                <li><a href="{{ interface.device.get_absolute_url }}">{{ interface.device }}</a></li>
-                <li>{{ interface }}</li>
-            </ol>
-        </div>
-    </div>
-    <div class="pull-right noprint">
-        {% if perms.dcim.change_interface %}
-            <a href="{% url 'dcim:interface_edit' pk=interface.pk %}" class="btn btn-warning">
-                <span class="fa fa-pencil" aria-hidden="true"></span> Edit
-            </a>
-        {% endif %}
-        {% if perms.dcim.delete_interface %}
-            <a href="{% url 'dcim:interface_delete' pk=interface.pk %}" class="btn btn-danger">
-                <span class="fa fa-trash" aria-hidden="true"></span> Delete
-            </a>
-        {% endif %}
-    </div>
-    <h1>{% block title %}{{ interface.device }} / {{ interface.name }}{% endblock %}</h1>
-    <ul class="nav nav-tabs">
-        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
-            <a href="{{ interface.get_absolute_url }}">Interface</a>
-        </li>
-        {% if perms.extras.view_objectchange %}
-            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
-                <a href="{% url 'dcim:interface_changelog' pk=interface.pk %}">Change Log</a>
-            </li>
-        {% endif %}
-    </ul>
-{% endblock %}
+{% load plugins %}
 
 {% block content %}
-<div class="row">
-	<div class="col-md-6">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Interface</strong>
-            </div>
-            <table class="table table-hover panel-body attr-table">
-                <tr>
-                    <td>Device</td>
-                    <td>
-                        <a href="{{ interface.device.get_absolute_url }}">{{ interface.device }}</a>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Name</td>
-                    <td>{{ interface.name }}</td>
-                </tr>
-                <tr>
-                    <td>Label</td>
-                    <td>{{ interface.label|placeholder }}</td>
-                </tr>
-                <tr>
-                    <td>Type</td>
-                    <td>{{ interface.get_type_display }}</td>
-                </tr>
-                <tr>
-                    <td>Enabled</td>
-                    <td>
-                        {% if interface.enabled %}
-                            <span class="text-success"><i class="fa fa-check"></i></span>
-                        {% else %}
-                            <span class="text-danger"><i class="fa fa-close"></i></span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>LAG</td>
-                    <td>
-                        {% if interface.lag%}
-                            <a href="{{ interface.lag.get_absolute_url }}">{{ interface.lag }}</a>
-                        {% else %}
-                            <span class="text-muted">None</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Description</td>
-                    <td>{{ interface.description|placeholder }} </td>
-                </tr>
-                <tr>
-                    <td>MTU</td>
-                    <td>{{ interface.mtu|placeholder }}</td>
-                </tr>
-                <tr>
-                    <td>MAC Address</td>
-                    <td><span class="text-monospace">{{ interface.mac_address|placeholder }}</span></td>
-                </tr>
-                <tr>
-                    <td>802.1Q Mode</td>
-                    <td>{{ interface.get_mode_display }}</td>
-                </tr>
-            </table>
-        </div>
-        {% include 'extras/inc/tags_panel.html' with tags=interface.tags.all %}
-    </div>
-	<div class="col-md-6">
-        {% if interface.is_connectable %}
+    <div class="row">
+        <div class="col-md-6">
             <div class="panel panel-default">
                 <div class="panel-heading">
-                    <strong>Connection</strong>
+                    <strong>Interface</strong>
                 </div>
-                {% if interface.cable %}
-                    <table class="table table-hover panel-body attr-table">
-                        {% if connected_interface %}
-                            <tr>
-                                <td>Device</td>
-                                <td>
-                                    <a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a>
-                                </td>
-                            </tr>
-                            <tr>
-                                <td>Name</td>
-                                <td>
-                                    <a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a>
-                                </td>
-                            </tr>
-                            <tr>
-                                <td>Type</td>
-                                <td>{{ connected_interface.get_type_display }}</td>
-                            </tr>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Type</td>
+                        <td>{{ instance.get_type_display }}</td>
+                    </tr>
+                    <tr>
+                        <td>Enabled</td>
+                        <td>
+                            {% if instance.enabled %}
+                                <span class="text-success"><i class="fa fa-check"></i></span>
+                            {% else %}
+                                <span class="text-danger"><i class="fa fa-close"></i></span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>LAG</td>
+                        <td>
+                            {% if instance.lag%}
+                                <a href="{{ instance.lag.get_absolute_url }}">{{ instance.lag }}</a>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }} </td>
+                    </tr>
+                    <tr>
+                        <td>MTU</td>
+                        <td>{{ instance.mtu|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>MAC Address</td>
+                        <td><span class="text-monospace">{{ instance.mac_address|placeholder }}</span></td>
+                    </tr>
+                    <tr>
+                        <td>802.1Q Mode</td>
+                        <td>{{ instance.get_mode_display }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            {% if instance.is_connectable %}
+                <div class="panel panel-default">
+                    <div class="panel-heading">
+                        <strong>Connection</strong>
+                    </div>
+                    {% if instance.cable %}
+                        <table class="table table-hover panel-body attr-table">
+                            {% if connected_interface %}
+                                <tr>
+                                    <td>Device</td>
+                                    <td>
+                                        <a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a>
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>Name</td>
+                                    <td>
+                                        <a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a>
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>Type</td>
+                                    <td>{{ connected_interface.get_type_display }}</td>
+                                </tr>
+                                <tr>
+                                    <td>Enabled</td>
+                                    <td>
+                                        {% if connected_interface.enabled %}
+                                            <span class="text-success"><i class="fa fa-check"></i></span>
+                                        {% else %}
+                                            <span class="text-danger"><i class="fa fa-close"></i></span>
+                                        {% endif %}
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>LAG</td>
+                                    <td>
+                                        {% if connected_interface.lag%}
+                                            <a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a>
+                                        {% else %}
+                                            <span class="text-muted">None</span>
+                                        {% endif %}
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>Description</td>
+                                    <td>{{ connected_interface.description|placeholder }}</td>
+                                </tr>
+                                <tr>
+                                    <td>MTU</td>
+                                    <td>{{ connected_interface.mtu|placeholder }}</td>
+                                </tr>
+                                <tr>
+                                    <td>MAC Address</td>
+                                    <td>{{ connected_interface.mac_address|placeholder }}</td>
+                                </tr>
+                                <tr>
+                                    <td>802.1Q Mode</td>
+                                    <td>{{ connected_interface.get_mode_display }}</td>
+                                </tr>
+                            {% elif connected_circuittermination %}
+                                {% with ct=connected_circuittermination %}
+                                    <tr>
+                                        <td>Provider</td>
+                                        <td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
+                                    </tr>
+                                    <tr>
+                                        <td>Circuit</td>
+                                        <td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
+                                    </tr>
+                                    <tr>
+                                        <td>Side</td>
+                                        <td>{{ ct.term_side }}</td>
+                                    </tr>
+                                {% endwith %}
+                            {% endif %}
                             <tr>
-                                <td>Enabled</td>
+                                <td>Cable</td>
                                 <td>
-                                    {% if connected_interface.enabled %}
-                                        <span class="text-success"><i class="fa fa-check"></i></span>
-                                    {% else %}
-                                        <span class="text-danger"><i class="fa fa-close"></i></span>
-                                    {% endif %}
+                                    <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
+                                    <a href="{% url 'dcim:interface_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                        <i class="fa fa-share-alt" aria-hidden="true"></i>
+                                    </a>
                                 </td>
                             </tr>
                             <tr>
-                                <td>LAG</td>
+                                <td>Connection Status</td>
                                 <td>
-                                    {% if connected_interface.lag%}
-                                        <a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a>
+                                    {% if instance.connection_status %}
+                                        <span class="label label-success">{{ instance.get_connection_status_display }}</span>
                                     {% else %}
-                                        <span class="text-muted">None</span>
+                                        <span class="label label-info">{{ instance.get_connection_status_display }}</span>
                                     {% endif %}
                                 </td>
                             </tr>
+                        </table>
+                    {% else %}
+                        <div class="panel-body text-muted">
+                            Not connected
+                        </div>
+                    {% endif %}
+                </div>
+            {% endif %}
+            {% if instance.is_lag %}
+                <div class="panel panel-default">
+                    <div class="panel-heading"><strong>LAG Members</strong></div>
+                    <table class="table table-hover table-headings panel-body">
+                        <thead>
                             <tr>
-                                <td>Description</td>
-                                <td>{{ connected_interface.description|placeholder }}</td>
-                            </tr>
-                            <tr>
-                                <td>MTU</td>
-                                <td>{{ connected_interface.mtu|placeholder }}</td>
-                            </tr>
-                            <tr>
-                                <td>MAC Address</td>
-                                <td>{{ connected_interface.mac_address|placeholder }}</td>
-                            </tr>
-                            <tr>
-                                <td>802.1Q Mode</td>
-                                <td>{{ connected_interface.get_mode_display }}</td>
+                                <th>Parent</th>
+                                <th>Interface</th>
+                                <th>Type</th>
                             </tr>
-                        {% elif connected_circuittermination %}
-                            {% with ct=connected_circuittermination %}
+                        </thead>
+                        <tbody>
+                            {% for member in instance.member_interfaces.all %}
                                 <tr>
-                                    <td>Provider</td>
-                                    <td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
+                                    <td>
+                                        <a href="{{ member.device.get_absolute_url }}">{{ member.device }}</a>
+                                    </td>
+                                    <td>
+                                        <a href="{{ member.get_absolute_url }}">{{ member }}</a>
+                                    </td>
+                                    <td>
+                                        {{ member.get_type_display }}
+                                    </td>
                                 </tr>
+                            {% empty %}
                                 <tr>
-                                    <td>Circuit</td>
-                                    <td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
+                                    <td colspan="3" class="text-muted">No member interfaces</td>
                                 </tr>
-                                <tr>
-                                    <td>Side</td>
-                                    <td>{{ ct.term_side }}</td>
-                                </tr>
-                            {% endwith %}
-                        {% endif %}
-                        <tr>
-                            <td>Cable</td>
-                            <td>
-                                <a href="{{ interface.cable.get_absolute_url }}">{{ interface.cable }}</a>
-                                <a href="{% url 'dcim:interface_trace' pk=interface.pk %}" class="btn btn-primary btn-xs" title="Trace">
-                                    <i class="fa fa-share-alt" aria-hidden="true"></i>
-                                </a>
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>Connection Status</td>
-                            <td>
-                                {% if interface.connection_status %}
-                                    <span class="label label-success">{{ interface.get_connection_status_display }}</span>
-                                {% else %}
-                                    <span class="label label-info">{{ interface.get_connection_status_display }}</span>
-                                {% endif %}
-                            </td>
-                        </tr>
+                            {% endfor %}
+                        </tbody>
                     </table>
-                {% else %}
-                    <div class="panel-body text-muted">
-                        Not connected
-                    </div>
-                {% endif %}
-            </div>
-        {% endif %}
-        {% if interface.is_lag %}
-            <div class="panel panel-default">
-                <div class="panel-heading"><strong>LAG Members</strong></div>
-                <table class="table table-hover table-headings panel-body">
-                    <thead>
-                        <tr>
-                            <th>Parent</th>
-                            <th>Interface</th>
-                            <th>Type</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        {% for member in interface.member_interfaces.all %}
-                            <tr>
-                                <td>
-                                    <a href="{{ member.device.get_absolute_url }}">{{ member.device }}</a>
-                                </td>
-                                <td>
-                                    <a href="{{ member.get_absolute_url }}">{{ member }}</a>
-                                </td>
-                                <td>
-                                    {{ member.get_type_display }}
-                                </td>
-                            </tr>
-                        {% empty %}
-                            <tr>
-                                <td colspan="3" class="text-muted">No member interfaces</td>
-                            </tr>
-                        {% endfor %}
-                    </tbody>
-                </table>
-            </div>
-        {% endif %}
+                </div>
+            {% endif %}
+            {% plugin_right_page instance %}
+        </div>
     </div>
-</div>
-<div class="row">
-    <div class="col-md-12">
-        {% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
+    <div class="row">
+        <div class="col-md-12">
+            {% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
+        </div>
     </div>
-</div>
-<div class="row">
-    <div class="col-md-12">
-        {% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
+    <div class="row">
+        <div class="col-md-12">
+            {% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
     </div>
-</div>
 {% endblock %}

+ 111 - 0
netbox/templates/dcim/poweroutlet.html

@@ -0,0 +1,111 @@
+{% extends 'dcim/device_component.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Power Outlet</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Type</td>
+                        <td>{{ instance.get_type_display }}</td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Power Port</td>
+                        <td>{{ instance.power_port }}</td>
+                    </tr>
+                    <tr>
+                        <td>Feed Leg</td>
+                        <td>{{ instance.get_feed_leg_display }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Connection</strong>
+                </div>
+                {% if instance.cable %}
+                    <table class="table table-hover panel-body attr-table">
+                        {% if instance.connected_endpoint %}
+                            <tr>
+                                <td>Device</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Name</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Type</td>
+                                <td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
+                            </tr>
+                            <tr>
+                                <td>Description</td>
+                                <td>{{ instance.connected_endpoint.description|placeholder }}</td>
+                            </tr>
+                        {% endif %}
+                        <tr>
+                            <td>Cable</td>
+                            <td>
+                                <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
+                                <a href="{% url 'dcim:poweroutlet_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                    <i class="fa fa-share-alt" aria-hidden="true"></i>
+                                </a>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Connection Status</td>
+                            <td>
+                                {% if instance.connection_status %}
+                                    <span class="label label-success">{{ instance.get_connection_status_display }}</span>
+                                {% else %}
+                                    <span class="label label-info">{{ instance.get_connection_status_display }}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    </table>
+                {% else %}
+                    <div class="panel-body text-muted">
+                        Not connected
+                    </div>
+                {% endif %}
+            </div>
+            {% plugin_right_page instance %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
+    </div>
+{% endblock %}

+ 111 - 0
netbox/templates/dcim/powerport.html

@@ -0,0 +1,111 @@
+{% extends 'dcim/device_component.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Power Port</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Type</td>
+                        <td>{{ instance.get_type_display }}</td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Maximum Draw</td>
+                        <td>{{ instance.maximum_draw|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Allocated Draw</td>
+                        <td>{{ instance.allocated_draw|placeholder }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Connection</strong>
+                </div>
+                {% if instance.cable %}
+                    <table class="table table-hover panel-body attr-table">
+                        {% if instance.connected_endpoint %}
+                            <tr>
+                                <td>Device</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Name</td>
+                                <td>
+                                    <a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Type</td>
+                                <td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
+                            </tr>
+                            <tr>
+                                <td>Description</td>
+                                <td>{{ instance.connected_endpoint.description|placeholder }}</td>
+                            </tr>
+                        {% endif %}
+                        <tr>
+                            <td>Cable</td>
+                            <td>
+                                <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
+                                <a href="{% url 'dcim:powerport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                    <i class="fa fa-share-alt" aria-hidden="true"></i>
+                                </a>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Connection Status</td>
+                            <td>
+                                {% if instance.connection_status %}
+                                    <span class="label label-success">{{ instance.get_connection_status_display }}</span>
+                                {% else %}
+                                    <span class="label label-info">{{ instance.get_connection_status_display }}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    </table>
+                {% else %}
+                    <div class="panel-body text-muted">
+                        Not connected
+                    </div>
+                {% endif %}
+            </div>
+            {% plugin_right_page instance %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
+    </div>
+{% endblock %}

+ 85 - 0
netbox/templates/dcim/rearport.html

@@ -0,0 +1,85 @@
+{% extends 'dcim/device_component.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Rear Port</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Device</td>
+                        <td>
+                            <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Name</td>
+                        <td>{{ instance.name }}</td>
+                    </tr>
+                    <tr>
+                        <td>Label</td>
+                        <td>{{ instance.label|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Type</td>
+                        <td>{{ instance.get_type_display }}</td>
+                    </tr>
+                    <tr>
+                        <td>Positions</td>
+                        <td>{{ instance.positions }}</td>
+                    </tr>
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ instance.description|placeholder }}</td>
+                    </tr>
+                </table>
+            </div>
+            {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
+            {% plugin_left_page instance %}
+        </div>
+        <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Connection</strong>
+                </div>
+                {% if instance.cable %}
+                    <table class="table table-hover panel-body attr-table">
+                        <tr>
+                            <td>Cable</td>
+                            <td>
+                                <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
+                                <a href="{% url 'dcim:rearport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                    <i class="fa fa-share-alt" aria-hidden="true"></i>
+                                </a>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Connection Status</td>
+                            <td>
+                                {% if instance.cable.status %}
+                                    <span class="label label-success">{{ instance.cable.get_status_display }}</span>
+                                {% else %}
+                                    <span class="label label-info">{{ instance.cable.get_status_display }}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    </table>
+                {% else %}
+                    <div class="panel-body text-muted">
+                        Not connected
+                    </div>
+                {% endif %}
+            </div>
+            {% plugin_right_page instance %}
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            {% plugin_full_width_page instance %}
+        </div>
+    </div>
+{% endblock %}

+ 1 - 9
netbox/utilities/templatetags/helpers.py

@@ -5,7 +5,6 @@ import re
 import yaml
 from django import template
 from django.conf import settings
-from django.urls import NoReverseMatch, reverse
 from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
 from markdown import markdown
@@ -79,14 +78,7 @@ def url_name(model, action):
     """
     Return the URL name for the given model and action, or None if invalid.
     """
-    url_name = '{}:{}_{}'.format(model._meta.app_label, model._meta.model_name, action)
-    try:
-        # Validate and return the URL name. We don't return the actual URL yet because many of the templates
-        # are written to pass a name to {% url %}.
-        reverse(url_name)
-        return url_name
-    except NoReverseMatch:
-        return None
+    return '{}:{}_{}'.format(model._meta.app_label, model._meta.model_name, action)
 
 
 @register.filter()

+ 30 - 0
netbox/utilities/templatetags/perms.py

@@ -0,0 +1,30 @@
+from django import template
+
+register = template.Library()
+
+
+def _check_permission(user, instance, action):
+    return user.has_perm(
+        perm=f'{instance._meta.app_label}.{action}_{instance._meta.model_name}',
+        obj=instance
+    )
+
+
+@register.filter()
+def can_view(user, instance):
+    return _check_permission(user, instance, 'view')
+
+
+@register.filter()
+def can_add(user, instance):
+    return _check_permission(user, instance, 'add')
+
+
+@register.filter()
+def can_change(user, instance):
+    return _check_permission(user, instance, 'change')
+
+
+@register.filter()
+def can_delete(user, instance):
+    return _check_permission(user, instance, 'delete')

+ 1 - 0
netbox/utilities/testing/views.py

@@ -917,6 +917,7 @@ class ViewTestCases:
         maxDiff = None
 
     class DeviceComponentViewTestCase(
+        GetObjectViewTestCase,
         EditObjectViewTestCase,
         DeleteObjectViewTestCase,
         ListObjectsViewTestCase,

+ 19 - 0
netbox/utilities/views.py

@@ -127,6 +127,25 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'view')
 
+    def get_template_name(self):
+        """
+        Return self.template_name if set. Otherwise, resolve the template path by model app_label and name.
+        """
+        if hasattr(self, 'template_name'):
+            return self.template_name
+        model_opts = self.queryset.model._meta
+        return f'{model_opts.app_label}/{model_opts.model_name}.html'
+
+    def get(self, request, pk):
+        """
+        Generic GET handler for accessing an object by PK
+        """
+        instance = get_object_or_404(self.queryset, pk=pk)
+
+        return render(request, self.get_template_name(), {
+            'instance': instance,
+        })
+
 
 class ObjectListView(ObjectPermissionRequiredMixin, View):
     """

+ 1 - 4
netbox/virtualization/tests/test_views.py

@@ -189,10 +189,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
 
-class VMInterfaceTestCase(
-    ViewTestCases.GetObjectViewTestCase,
-    ViewTestCases.DeviceComponentViewTestCase,
-):
+class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = VMInterface
 
     @classmethod