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

Merge branch 'develop-2.9' into 2006-scripts-reports-background

John Anderson 5 лет назад
Родитель
Сommit
4a74927fa2
31 измененных файлов с 1118 добавлено и 1084 удалено
  1. 1 0
      docs/release-notes/version-2.9.md
  2. 2 0
      netbox/circuits/tests/test_api.py
  3. 7 5
      netbox/dcim/api/serializers.py
  4. 11 10
      netbox/dcim/filters.py
  5. 46 81
      netbox/dcim/forms.py
  6. 33 10
      netbox/dcim/migrations/0107_component_labels.py
  7. 9 3
      netbox/dcim/migrations/0109_interface_remove_vm.py
  8. 120 0
      netbox/dcim/migrations/0112_standardize_components.py
  9. 26 26
      netbox/dcim/models/__init__.py
  10. 19 136
      netbox/dcim/models/device_component_templates.py
  11. 44 173
      netbox/dcim/models/device_components.py
  12. 27 49
      netbox/dcim/tables.py
  13. 4 0
      netbox/dcim/tests/test_api.py
  14. 10 8
      netbox/dcim/tests/test_views.py
  15. 8 0
      netbox/dcim/urls.py
  16. 42 10
      netbox/dcim/views.py
  17. 2 0
      netbox/secrets/tests/test_views.py
  18. 471 416
      netbox/templates/dcim/device.html
  19. 139 131
      netbox/templates/dcim/devicetype.html
  20. 0 2
      netbox/templates/dcim/inc/consoleport.html
  21. 40 0
      netbox/templates/dcim/inc/device_component_table.html
  22. 11 11
      netbox/templates/dcim/inc/devicetype_component_table.html
  23. 1 1
      netbox/templates/dcim/inc/interface.html
  24. 1 1
      netbox/templates/extras/inc/tags_panel.html
  25. 1 0
      netbox/templates/utilities/templatetags/badge.html
  26. 15 7
      netbox/utilities/tables.py
  27. 11 0
      netbox/utilities/templatetags/helpers.py
  28. 1 1
      netbox/utilities/utils.py
  29. 2 2
      netbox/virtualization/migrations/0016_replicate_interfaces.py
  30. 11 0
      netbox/virtualization/models.py
  31. 3 1
      netbox/virtualization/tests/test_api.py

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

@@ -19,6 +19,7 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
 * [#4793](https://github.com/netbox-community/netbox/issues/4793) - Add `description` field to device component templates
 * [#4793](https://github.com/netbox-community/netbox/issues/4793) - Add `description` field to device component templates
 * [#4795](https://github.com/netbox-community/netbox/issues/4795) - Add bulk disconnect capability for console and power ports
 * [#4795](https://github.com/netbox-community/netbox/issues/4795) - Add bulk disconnect capability for console and power ports
 * [#4807](https://github.com/netbox-community/netbox/issues/4807) - Add bulk edit ability for device bay templates
 * [#4807](https://github.com/netbox-community/netbox/issues/4807) - Add bulk edit ability for device bay templates
+* [#4817](https://github.com/netbox-community/netbox/issues/4817) - Standardize device/VM component `name` field to 64 characters
 
 
 ### Configuration Changes
 ### Configuration Changes
 
 

+ 2 - 0
netbox/circuits/tests/test_api.py

@@ -1,4 +1,5 @@
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 
 
 from circuits.choices import *
 from circuits.choices import *
@@ -45,6 +46,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
         )
         )
         Provider.objects.bulk_create(providers)
         Provider.objects.bulk_create(providers)
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_provider_graphs(self):
     def test_get_provider_graphs(self):
         """
         """
         Test retrieval of Graphs assigned to Providers.
         Test retrieval of Graphs assigned to Providers.

+ 7 - 5
netbox/dcim/api/serializers.py

@@ -311,7 +311,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
-        fields = ['id', 'device_type', 'name', 'type', 'positions', 'description']
+        fields = ['id', 'device_type', 'name', 'label', 'type', 'positions', 'description']
 
 
 
 
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
@@ -321,7 +321,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = FrontPortTemplate
         model = FrontPortTemplate
-        fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
+        fields = ['id', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description']
 
 
 
 
 class DeviceBayTemplateSerializer(ValidatedModelSerializer):
 class DeviceBayTemplateSerializer(ValidatedModelSerializer):
@@ -559,7 +559,7 @@ class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = RearPort
         model = RearPort
-        fields = ['id', 'device', 'name', 'type', 'positions', 'description', 'cable', 'tags']
+        fields = ['id', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags']
 
 
 
 
 class FrontPortRearPortSerializer(WritableNestedSerializer):
 class FrontPortRearPortSerializer(WritableNestedSerializer):
@@ -570,7 +570,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
 
 
     class Meta:
     class Meta:
         model = RearPort
         model = RearPort
-        fields = ['id', 'url', 'name']
+        fields = ['id', 'url', 'name', 'label']
 
 
 
 
 class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
 class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
@@ -581,7 +581,9 @@ class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = FrontPort
         model = FrontPort
-        fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags']
+        fields = [
+            'id', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags',
+        ]
 
 
 
 
 class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer):
 class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer):

+ 11 - 10
netbox/dcim/filters.py

@@ -384,28 +384,28 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
         )
         )
 
 
     def _console_ports(self, queryset, name, value):
     def _console_ports(self, queryset, name, value):
-        return queryset.exclude(consoleport_templates__isnull=value)
+        return queryset.exclude(consoleporttemplates__isnull=value)
 
 
     def _console_server_ports(self, queryset, name, value):
     def _console_server_ports(self, queryset, name, value):
-        return queryset.exclude(consoleserverport_templates__isnull=value)
+        return queryset.exclude(consoleserverporttemplates__isnull=value)
 
 
     def _power_ports(self, queryset, name, value):
     def _power_ports(self, queryset, name, value):
-        return queryset.exclude(powerport_templates__isnull=value)
+        return queryset.exclude(powerporttemplates__isnull=value)
 
 
     def _power_outlets(self, queryset, name, value):
     def _power_outlets(self, queryset, name, value):
-        return queryset.exclude(poweroutlet_templates__isnull=value)
+        return queryset.exclude(poweroutlettemplates__isnull=value)
 
 
     def _interfaces(self, queryset, name, value):
     def _interfaces(self, queryset, name, value):
-        return queryset.exclude(interface_templates__isnull=value)
+        return queryset.exclude(interfacetemplates__isnull=value)
 
 
     def _pass_through_ports(self, queryset, name, value):
     def _pass_through_ports(self, queryset, name, value):
         return queryset.exclude(
         return queryset.exclude(
-            frontport_templates__isnull=value,
-            rearport_templates__isnull=value
+            frontporttemplates__isnull=value,
+            rearporttemplates__isnull=value
         )
         )
 
 
     def _device_bays(self, queryset, name, value):
     def _device_bays(self, queryset, name, value):
-        return queryset.exclude(device_bay_templates__isnull=value)
+        return queryset.exclude(devicebaytemplates__isnull=value)
 
 
 
 
 class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
 class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
@@ -656,7 +656,7 @@ class DeviceFilterSet(
         return queryset.filter(
         return queryset.filter(
             Q(name__icontains=value) |
             Q(name__icontains=value) |
             Q(serial__icontains=value.strip()) |
             Q(serial__icontains=value.strip()) |
-            Q(inventory_items__serial__icontains=value.strip()) |
+            Q(inventoryitems__serial__icontains=value.strip()) |
             Q(asset_tag__icontains=value.strip()) |
             Q(asset_tag__icontains=value.strip()) |
             Q(comments__icontains=value)
             Q(comments__icontains=value)
         ).distinct()
         ).distinct()
@@ -698,7 +698,7 @@ class DeviceFilterSet(
         )
         )
 
 
     def _device_bays(self, queryset, name, value):
     def _device_bays(self, queryset, name, value):
-        return queryset.exclude(device_bays__isnull=value)
+        return queryset.exclude(devicebays__isnull=value)
 
 
 
 
 class DeviceComponentFilterSet(django_filters.FilterSet):
 class DeviceComponentFilterSet(django_filters.FilterSet):
@@ -747,6 +747,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
             return queryset
             return queryset
         return queryset.filter(
         return queryset.filter(
             Q(name__icontains=value) |
             Q(name__icontains=value) |
+            Q(label__icontains=value) |
             Q(description__icontains=value)
             Q(description__icontains=value)
         )
         )
 
 

+ 46 - 81
netbox/dcim/forms.py

@@ -59,7 +59,6 @@ def get_device_by_name_or_pk(name):
 
 
 
 
 class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
 class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
-
     field_order = [
     field_order = [
         'q', 'region', 'site'
         'q', 'region', 'site'
     ]
     ]
@@ -127,7 +126,11 @@ class InterfaceCommonForm:
                 })
                 })
 
 
 
 
-class LabeledComponentForm(BootstrapMixin, forms.Form):
+class ComponentForm(BootstrapMixin, forms.Form):
+    """
+    Subclass this form when facilitating the creation of one or more device component or component templates based on
+    a name pattern.
+    """
     name_pattern = ExpandableNameField(
     name_pattern = ExpandableNameField(
         label='Name'
         label='Name'
     )
     )
@@ -1033,7 +1036,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
 # Device component templates
 # Device component templates
 #
 #
 
 
-class ComponentTemplateCreateForm(LabeledComponentForm):
+class ComponentTemplateCreateForm(ComponentForm):
     """
     """
     Base form for the creation of device component templates.
     Base form for the creation of device component templates.
     """
     """
@@ -1350,7 +1353,7 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = FrontPortTemplate
         model = FrontPortTemplate
         fields = [
         fields = [
-            'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'description',
+            'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
         ]
         ]
         widgets = {
         widgets = {
             'device_type': forms.HiddenInput(),
             'device_type': forms.HiddenInput(),
@@ -1389,7 +1392,7 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
         # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
         # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
         occupied_port_positions = [
         occupied_port_positions = [
             (front_port.rear_port_id, front_port.rear_port_position)
             (front_port.rear_port_id, front_port.rear_port_position)
-            for front_port in device_type.frontport_templates.all()
+            for front_port in device_type.frontporttemplates.all()
         ]
         ]
 
 
         # Populate rear port choices
         # Populate rear port choices
@@ -1430,6 +1433,10 @@ class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
         queryset=FrontPortTemplate.objects.all(),
         queryset=FrontPortTemplate.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
+    label = forms.CharField(
+        max_length=64,
+        required=False
+    )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         choices=add_blank_choice(PortTypeChoices),
         choices=add_blank_choice(PortTypeChoices),
         required=False,
         required=False,
@@ -1448,7 +1455,7 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
         fields = [
         fields = [
-            'device_type', 'name', 'type', 'positions', 'description',
+            'device_type', 'name', 'label', 'type', 'positions', 'description',
         ]
         ]
         widgets = {
         widgets = {
             'device_type': forms.HiddenInput(),
             'device_type': forms.HiddenInput(),
@@ -1474,6 +1481,10 @@ class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
         queryset=RearPortTemplate.objects.all(),
         queryset=RearPortTemplate.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
+    label = forms.CharField(
+        max_length=64,
+        required=False
+    )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         choices=add_blank_choice(PortTypeChoices),
         choices=add_blank_choice(PortTypeChoices),
         required=False,
         required=False,
@@ -2248,7 +2259,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
 # Device components
 # Device components
 #
 #
 
 
-class ComponentCreateForm(LabeledComponentForm):
+class ComponentCreateForm(ComponentForm):
     """
     """
     Base form for the creation of device components.
     Base form for the creation of device components.
     """
     """
@@ -2261,7 +2272,7 @@ class ComponentCreateForm(LabeledComponentForm):
     )
     )
 
 
 
 
-class DeviceBulkAddComponentForm(LabeledComponentForm):
+class DeviceBulkAddComponentForm(ComponentForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
@@ -2967,13 +2978,12 @@ class InterfaceCSVForm(CSVModelForm):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
         # Limit LAG choices to interfaces belonging to this device (or VC master)
         # Limit LAG choices to interfaces belonging to this device (or VC master)
+        device = None
         if self.is_bound and 'device' in self.data:
         if self.is_bound and 'device' in self.data:
             try:
             try:
                 device = self.fields['device'].to_python(self.data['device'])
                 device = self.fields['device'].to_python(self.data['device'])
             except forms.ValidationError:
             except forms.ValidationError:
-                device = None
-        else:
-            device = self.instance.device
+                pass
 
 
         if device:
         if device:
             self.fields['lag'].queryset = Interface.objects.filter(
             self.fields['lag'].queryset = Interface.objects.filter(
@@ -3013,7 +3023,7 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = FrontPort
         model = FrontPort
         fields = [
         fields = [
-            'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'tags',
+            'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'device': forms.HiddenInput(),
             'device': forms.HiddenInput(),
@@ -3094,14 +3104,14 @@ class FrontPortCreateForm(ComponentCreateForm):
 
 
 
 
 # class FrontPortBulkCreateForm(
 # class FrontPortBulkCreateForm(
-#     form_from_model(FrontPort, ['type', 'description', 'tags']),
+#     form_from_model(FrontPort, ['label', 'type', 'description', 'tags']),
 #     DeviceBulkAddComponentForm
 #     DeviceBulkAddComponentForm
 # ):
 # ):
 #     pass
 #     pass
 
 
 
 
 class FrontPortBulkEditForm(
 class FrontPortBulkEditForm(
-    form_from_model(FrontPort, ['type', 'description']),
+    form_from_model(FrontPort, ['label', 'type', 'description']),
     BootstrapMixin,
     BootstrapMixin,
     AddRemoveTagsForm,
     AddRemoveTagsForm,
     BulkEditForm
     BulkEditForm
@@ -3112,9 +3122,7 @@ class FrontPortBulkEditForm(
     )
     )
 
 
     class Meta:
     class Meta:
-        nullable_fields = [
-            'description',
-        ]
+        nullable_fields = ('label', 'description')
 
 
 
 
 class FrontPortCSVForm(CSVModelForm):
 class FrontPortCSVForm(CSVModelForm):
@@ -3185,7 +3193,7 @@ class RearPortForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = RearPort
         model = RearPort
         fields = [
         fields = [
-            'device', 'name', 'type', 'positions', 'description', 'tags',
+            'device', 'name', 'label', 'type', 'positions', 'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'device': forms.HiddenInput(),
             'device': forms.HiddenInput(),
@@ -3210,14 +3218,14 @@ class RearPortCreateForm(ComponentCreateForm):
 
 
 
 
 class RearPortBulkCreateForm(
 class RearPortBulkCreateForm(
-    form_from_model(RearPort, ['type', 'positions', 'description', 'tags']),
+    form_from_model(RearPort, ['label', 'type', 'positions', 'description', 'tags']),
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     pass
     pass
 
 
 
 
 class RearPortBulkEditForm(
 class RearPortBulkEditForm(
-    form_from_model(RearPort, ['type', 'description']),
+    form_from_model(RearPort, ['label', 'type', 'description']),
     BootstrapMixin,
     BootstrapMixin,
     AddRemoveTagsForm,
     AddRemoveTagsForm,
     BulkEditForm
     BulkEditForm
@@ -3228,9 +3236,7 @@ class RearPortBulkEditForm(
     )
     )
 
 
     class Meta:
     class Meta:
-        nullable_fields = [
-            'description',
-        ]
+        nullable_fields = ('label', 'description')
 
 
 
 
 class RearPortCSVForm(CSVModelForm):
 class RearPortCSVForm(CSVModelForm):
@@ -3392,17 +3398,11 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = InventoryItem
         model = InventoryItem
         fields = [
         fields = [
-            'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
+            'name', 'label', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
         ]
         ]
 
 
 
 
-class InventoryItemCreateForm(BootstrapMixin, forms.Form):
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.prefetch_related('device_type__manufacturer')
-    )
-    name_pattern = ExpandableNameField(
-        label='Name'
-    )
+class InventoryItemCreateForm(ComponentCreateForm):
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
         required=False
         required=False
@@ -3443,7 +3443,7 @@ class InventoryItemCSVForm(CSVModelForm):
 
 
 
 
 class InventoryItemBulkCreateForm(
 class InventoryItemBulkCreateForm(
-    form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'tags']),
+    form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'tags']),
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     tags = DynamicModelMultipleChoiceField(
     tags = DynamicModelMultipleChoiceField(
@@ -3452,68 +3452,27 @@ class InventoryItemBulkCreateForm(
     )
     )
 
 
 
 
-class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
+class InventoryItemBulkEditForm(
+    form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
+    BootstrapMixin,
+    AddRemoveTagsForm,
+    BulkEditForm
+):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=InventoryItem.objects.all(),
         queryset=InventoryItem.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all(),
-        required=False
-    )
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
         required=False
         required=False
     )
     )
-    part_id = forms.CharField(
-        max_length=50,
-        required=False,
-        label='Part ID'
-    )
-    description = forms.CharField(
-        max_length=100,
-        required=False
-    )
 
 
     class Meta:
     class Meta:
-        nullable_fields = [
-            'manufacturer', 'part_id', 'description',
-        ]
+        nullable_fields = ('label', 'manufacturer', 'part_id', 'description')
 
 
 
 
-class InventoryItemFilterForm(BootstrapMixin, forms.Form):
+class InventoryItemFilterForm(DeviceComponentFilterForm):
     model = InventoryItem
     model = InventoryItem
-    q = forms.CharField(
-        required=False,
-        label='Search'
-    )
-    region = DynamicModelMultipleChoiceField(
-        queryset=Region.objects.all(),
-        to_field_name='slug',
-        required=False,
-        widget=APISelectMultiple(
-            value_field="slug",
-            filter_for={
-                'site': 'region'
-            }
-        )
-    )
-    site = DynamicModelMultipleChoiceField(
-        queryset=Site.objects.all(),
-        to_field_name='slug',
-        required=False,
-        widget=APISelectMultiple(
-            value_field="slug",
-            filter_for={
-                'device_id': 'site'
-            }
-        )
-    )
-    device_id = DynamicModelMultipleChoiceField(
-        queryset=Device.objects.all(),
-        required=False,
-        label='Device'
-    )
     manufacturer = DynamicModelMultipleChoiceField(
     manufacturer = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -3522,6 +3481,12 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
+    serial = forms.CharField(
+        required=False
+    )
+    asset_tag = forms.CharField(
+        required=False
+    )
     discovered = forms.NullBooleanField(
     discovered = forms.NullBooleanField(
         required=False,
         required=False,
         widget=StaticSelect2(
         widget=StaticSelect2(

+ 33 - 10
netbox/dcim/migrations/0107_component_labels.py

@@ -1,5 +1,3 @@
-# Generated by Django 3.0.7 on 2020-06-04 20:37
-
 from django.db import migrations, models
 from django.db import migrations, models
 
 
 
 
@@ -11,32 +9,57 @@ class Migration(migrations.Migration):
 
 
     operations = [
     operations = [
         migrations.AddField(
         migrations.AddField(
-            model_name='interface',
+            model_name='consoleport',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name='interfacetemplate',
+            model_name='consoleporttemplate',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name='consoleport',
+            model_name='consoleserverport',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name='consoleporttemplate',
+            model_name='consoleserverporttemplate',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name='consoleserverport',
+            model_name='devicebay',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name='consoleserverporttemplate',
+            model_name='devicebaytemplate',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='frontport',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='frontporttemplate',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='interfacetemplate',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
@@ -61,12 +84,12 @@ class Migration(migrations.Migration):
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name='devicebay',
+            model_name='rearport',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name='devicebaytemplate',
+            model_name='rearporttemplate',
             name='label',
             name='label',
             field=models.CharField(blank=True, max_length=64),
             field=models.CharField(blank=True, max_length=64),
         ),
         ),

+ 9 - 3
netbox/dcim/migrations/0109_interface_remove_vm.py

@@ -1,6 +1,5 @@
-# Generated by Django 3.0.6 on 2020-06-22 16:03
-
-from django.db import migrations
+from django.db import migrations, models
+import django.db.models.deletion
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
@@ -15,4 +14,11 @@ class Migration(migrations.Migration):
             model_name='interface',
             model_name='interface',
             name='virtual_machine',
             name='virtual_machine',
         ),
         ),
+        # device is now a required field
+        migrations.AlterField(
+            model_name='interface',
+            name='device',
+            field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
+            preserve_default=False,
+        ),
     ]
     ]

+ 120 - 0
netbox/dcim/migrations/0112_standardize_components.py

@@ -0,0 +1,120 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0111_component_template_description'),
+    ]
+
+    operations = [
+        # Set max_length=64 for all name fields
+        migrations.AlterField(
+            model_name='consoleport',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleporttemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverport',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverporttemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='devicebay',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='devicebaytemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlet',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlettemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='powerport',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='powerporttemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+
+        # Update related_name for necessary component and component template models
+        migrations.AlterField(
+            model_name='consoleporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='devicebay',
+            name='device',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='devicebays', to='dcim.Device'),
+        ),
+        migrations.AlterField(
+            model_name='devicebaytemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='devicebaytemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='frontporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='interfacetemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfacetemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='device',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventoryitems', to='dcim.Device'),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlettemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlettemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='powerporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='powerporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='rearporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearporttemplates', to='dcim.DeviceType'),
+        ),
+    ]

+ 26 - 26
netbox/dcim/models/__init__.py

@@ -678,7 +678,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
                 'device_type__manufacturer',
                 'device_type__manufacturer',
                 'device_role'
                 'device_role'
             ).annotate(
             ).annotate(
-                devicebay_count=Count('device_bays')
+                devicebay_count=Count('devicebays')
             ).exclude(
             ).exclude(
                 pk=exclude
                 pk=exclude
             ).filter(
             ).filter(
@@ -1049,23 +1049,23 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
         ))
         ))
 
 
         # Component templates
         # Component templates
-        if self.consoleport_templates.exists():
+        if self.consoleporttemplates.exists():
             data['console-ports'] = [
             data['console-ports'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
                     'type': c.type,
                     'type': c.type,
                 }
                 }
-                for c in self.consoleport_templates.all()
+                for c in self.consoleporttemplates.all()
             ]
             ]
-        if self.consoleserverport_templates.exists():
+        if self.consoleserverporttemplates.exists():
             data['console-server-ports'] = [
             data['console-server-ports'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
                     'type': c.type,
                     'type': c.type,
                 }
                 }
-                for c in self.consoleserverport_templates.all()
+                for c in self.consoleserverporttemplates.all()
             ]
             ]
-        if self.powerport_templates.exists():
+        if self.powerporttemplates.exists():
             data['power-ports'] = [
             data['power-ports'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
@@ -1073,9 +1073,9 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                     'maximum_draw': c.maximum_draw,
                     'maximum_draw': c.maximum_draw,
                     'allocated_draw': c.allocated_draw,
                     'allocated_draw': c.allocated_draw,
                 }
                 }
-                for c in self.powerport_templates.all()
+                for c in self.powerporttemplates.all()
             ]
             ]
-        if self.poweroutlet_templates.exists():
+        if self.poweroutlettemplates.exists():
             data['power-outlets'] = [
             data['power-outlets'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
@@ -1083,18 +1083,18 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                     'power_port': c.power_port.name if c.power_port else None,
                     'power_port': c.power_port.name if c.power_port else None,
                     'feed_leg': c.feed_leg,
                     'feed_leg': c.feed_leg,
                 }
                 }
-                for c in self.poweroutlet_templates.all()
+                for c in self.poweroutlettemplates.all()
             ]
             ]
-        if self.interface_templates.exists():
+        if self.interfacetemplates.exists():
             data['interfaces'] = [
             data['interfaces'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
                     'type': c.type,
                     'type': c.type,
                     'mgmt_only': c.mgmt_only,
                     'mgmt_only': c.mgmt_only,
                 }
                 }
-                for c in self.interface_templates.all()
+                for c in self.interfacetemplates.all()
             ]
             ]
-        if self.frontport_templates.exists():
+        if self.frontporttemplates.exists():
             data['front-ports'] = [
             data['front-ports'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
@@ -1102,23 +1102,23 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                     'rear_port': c.rear_port.name,
                     'rear_port': c.rear_port.name,
                     'rear_port_position': c.rear_port_position,
                     'rear_port_position': c.rear_port_position,
                 }
                 }
-                for c in self.frontport_templates.all()
+                for c in self.frontporttemplates.all()
             ]
             ]
-        if self.rearport_templates.exists():
+        if self.rearporttemplates.exists():
             data['rear-ports'] = [
             data['rear-ports'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
                     'type': c.type,
                     'type': c.type,
                     'positions': c.positions,
                     'positions': c.positions,
                 }
                 }
-                for c in self.rearport_templates.all()
+                for c in self.rearporttemplates.all()
             ]
             ]
-        if self.device_bay_templates.exists():
+        if self.devicebaytemplates.exists():
             data['device-bays'] = [
             data['device-bays'] = [
                 {
                 {
                     'name': c.name,
                     'name': c.name,
                 }
                 }
-                for c in self.device_bay_templates.all()
+                for c in self.devicebaytemplates.all()
             ]
             ]
 
 
         return yaml.dump(dict(data), sort_keys=False)
         return yaml.dump(dict(data), sort_keys=False)
@@ -1159,7 +1159,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
 
 
         if (
         if (
                 self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
                 self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
-        ) and self.device_bay_templates.count():
+        ) and self.devicebaytemplates.count():
             raise ValidationError({
             raise ValidationError({
                 'subdevice_role': "Must delete all device bay templates associated with this device before "
                 'subdevice_role': "Must delete all device bay templates associated with this device before "
                                   "declassifying it as a parent device."
                                   "declassifying it as a parent device."
@@ -1634,28 +1634,28 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         # If this is a new Device, instantiate all of the related components per the DeviceType definition
         # If this is a new Device, instantiate all of the related components per the DeviceType definition
         if is_new:
         if is_new:
             ConsolePort.objects.bulk_create(
             ConsolePort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.consoleport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.consoleporttemplates.unrestricted()]
             )
             )
             ConsoleServerPort.objects.bulk_create(
             ConsoleServerPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.consoleserverport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.consoleserverporttemplates.unrestricted()]
             )
             )
             PowerPort.objects.bulk_create(
             PowerPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.powerport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.powerporttemplates.unrestricted()]
             )
             )
             PowerOutlet.objects.bulk_create(
             PowerOutlet.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.poweroutlet_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.poweroutlettemplates.unrestricted()]
             )
             )
             Interface.objects.bulk_create(
             Interface.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.interface_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.interfacetemplates.unrestricted()]
             )
             )
             RearPort.objects.bulk_create(
             RearPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.rearport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.rearporttemplates.unrestricted()]
             )
             )
             FrontPort.objects.bulk_create(
             FrontPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.frontport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.frontporttemplates.unrestricted()]
             )
             )
             DeviceBay.objects.bulk_create(
             DeviceBay.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.device_bay_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.devicebaytemplates.unrestricted()]
             )
             )
 
 
         # Update Site and Rack assignment for any child Devices
         # Update Site and Rack assignment for any child Devices

+ 19 - 136
netbox/dcim/models/device_component_templates.py

@@ -27,6 +27,24 @@ __all__ = (
 
 
 
 
 class ComponentTemplateModel(models.Model):
 class ComponentTemplateModel(models.Model):
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='%(class)ss'
+    )
+    name = models.CharField(
+        max_length=64
+    )
+    _name = NaturalOrderingField(
+        target_field='name',
+        max_length=100,
+        blank=True
+    )
+    label = models.CharField(
+        max_length=64,
+        blank=True,
+        help_text="Physical label"
+    )
     description = models.CharField(
     description = models.CharField(
         max_length=200,
         max_length=200,
         blank=True
         blank=True
@@ -68,24 +86,6 @@ class ConsolePortTemplate(ComponentTemplateModel):
     """
     """
     A template for a ConsolePort to be created for a new Device.
     A template for a ConsolePort to be created for a new Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='consoleport_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
@@ -108,24 +108,6 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
     """
     """
     A template for a ConsoleServerPort to be created for a new Device.
     A template for a ConsoleServerPort to be created for a new Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='consoleserverport_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
@@ -148,24 +130,6 @@ class PowerPortTemplate(ComponentTemplateModel):
     """
     """
     A template for a PowerPort to be created for a new Device.
     A template for a PowerPort to be created for a new Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='powerport_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PowerPortTypeChoices,
         choices=PowerPortTypeChoices,
@@ -202,24 +166,6 @@ class PowerOutletTemplate(ComponentTemplateModel):
     """
     """
     A template for a PowerOutlet to be created for a new Device.
     A template for a PowerOutlet to be created for a new Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='poweroutlet_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PowerOutletTypeChoices,
         choices=PowerOutletTypeChoices,
@@ -269,25 +215,13 @@ class InterfaceTemplate(ComponentTemplateModel):
     """
     """
     A template for a physical data interface on a new Device.
     A template for a physical data interface on a new Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='interface_templates'
-    )
-    name = models.CharField(
-        max_length=64
-    )
+    # Override ComponentTemplateModel._name to specify naturalize_interface function
     _name = NaturalOrderingField(
     _name = NaturalOrderingField(
         target_field='name',
         target_field='name',
         naturalize_function=naturalize_interface,
         naturalize_function=naturalize_interface,
         max_length=100,
         max_length=100,
         blank=True
         blank=True
     )
     )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=InterfaceTypeChoices
         choices=InterfaceTypeChoices
@@ -314,19 +248,6 @@ class FrontPortTemplate(ComponentTemplateModel):
     """
     """
     Template for a pass-through port on the front of a new Device.
     Template for a pass-through port on the front of a new Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='frontport_templates'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PortTypeChoices
         choices=PortTypeChoices
@@ -348,9 +269,6 @@ class FrontPortTemplate(ComponentTemplateModel):
             ('rear_port', 'rear_port_position'),
             ('rear_port', 'rear_port_position'),
         )
         )
 
 
-    def __str__(self):
-        return self.name
-
     def clean(self):
     def clean(self):
 
 
         # Validate rear port assignment
         # Validate rear port assignment
@@ -385,19 +303,6 @@ class RearPortTemplate(ComponentTemplateModel):
     """
     """
     Template for a pass-through port on the rear of a new Device.
     Template for a pass-through port on the rear of a new Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='rearport_templates'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PortTypeChoices
         choices=PortTypeChoices
@@ -411,9 +316,6 @@ class RearPortTemplate(ComponentTemplateModel):
         ordering = ('device_type', '_name')
         ordering = ('device_type', '_name')
         unique_together = ('device_type', 'name')
         unique_together = ('device_type', 'name')
 
 
-    def __str__(self):
-        return self.name
-
     def instantiate(self, device):
     def instantiate(self, device):
         return RearPort(
         return RearPort(
             device=device,
             device=device,
@@ -427,25 +329,6 @@ class DeviceBayTemplate(ComponentTemplateModel):
     """
     """
     A template for a DeviceBay to be created for a new parent Device.
     A template for a DeviceBay to be created for a new parent Device.
     """
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='device_bay_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
-
     class Meta:
     class Meta:
         ordering = ('device_type', '_name')
         ordering = ('device_type', '_name')
         unique_together = ('device_type', 'name')
         unique_together = ('device_type', 'name')

+ 44 - 173
netbox/dcim/models/device_components.py

@@ -36,6 +36,24 @@ __all__ = (
 
 
 
 
 class ComponentModel(models.Model):
 class ComponentModel(models.Model):
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='%(class)ss'
+    )
+    name = models.CharField(
+        max_length=64
+    )
+    _name = NaturalOrderingField(
+        target_field='name',
+        max_length=100,
+        blank=True
+    )
+    label = models.CharField(
+        max_length=64,
+        blank=True,
+        help_text="Physical label"
+    )
     description = models.CharField(
     description = models.CharField(
         max_length=200,
         max_length=200,
         blank=True
         blank=True
@@ -233,24 +251,6 @@ class ConsolePort(CableTermination, ComponentModel):
     """
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='consoleports'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
@@ -270,7 +270,7 @@ class ConsolePort(CableTermination, ComponentModel):
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
-    csv_headers = ['device', 'name', 'type', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'description']
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -283,6 +283,7 @@ class ConsolePort(CableTermination, ComponentModel):
         return (
         return (
             self.device.identifier,
             self.device.identifier,
             self.name,
             self.name,
+            self.label,
             self.type,
             self.type,
             self.description,
             self.description,
         )
         )
@@ -297,24 +298,6 @@ class ConsoleServerPort(CableTermination, ComponentModel):
     """
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='consoleserverports'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
@@ -327,7 +310,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
-    csv_headers = ['device', 'name', 'type', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'description']
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -340,6 +323,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
         return (
         return (
             self.device.identifier,
             self.device.identifier,
             self.name,
             self.name,
+            self.label,
             self.type,
             self.type,
             self.description,
             self.description,
         )
         )
@@ -354,24 +338,6 @@ class PowerPort(CableTermination, ComponentModel):
     """
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='powerports'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PowerPortTypeChoices,
         choices=PowerPortTypeChoices,
@@ -410,7 +376,7 @@ class PowerPort(CableTermination, ComponentModel):
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
-    csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -423,6 +389,7 @@ class PowerPort(CableTermination, ComponentModel):
         return (
         return (
             self.device.identifier,
             self.device.identifier,
             self.name,
             self.name,
+            self.label,
             self.get_type_display(),
             self.get_type_display(),
             self.maximum_draw,
             self.maximum_draw,
             self.allocated_draw,
             self.allocated_draw,
@@ -519,24 +486,6 @@ class PowerOutlet(CableTermination, ComponentModel):
     """
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='poweroutlets'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PowerOutletTypeChoices,
         choices=PowerOutletTypeChoices,
@@ -562,7 +511,7 @@ class PowerOutlet(CableTermination, ComponentModel):
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
-    csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -575,6 +524,7 @@ class PowerOutlet(CableTermination, ComponentModel):
         return (
         return (
             self.device.identifier,
             self.device.identifier,
             self.name,
             self.name,
+            self.label,
             self.get_type_display(),
             self.get_type_display(),
             self.power_port.name if self.power_port else None,
             self.power_port.name if self.power_port else None,
             self.get_feed_leg_display(),
             self.get_feed_leg_display(),
@@ -595,15 +545,9 @@ class PowerOutlet(CableTermination, ComponentModel):
 #
 #
 
 
 class BaseInterface(models.Model):
 class BaseInterface(models.Model):
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        naturalize_function=naturalize_interface,
-        max_length=100,
-        blank=True
-    )
+    """
+    Abstract base class for fields shared by dcim.Interface and virtualization.VMInterface.
+    """
     enabled = models.BooleanField(
     enabled = models.BooleanField(
         default=True
         default=True
     )
     )
@@ -633,18 +577,13 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
     """
     """
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
     """
     """
-    device = models.ForeignKey(
-        to='Device',
-        on_delete=models.CASCADE,
-        related_name='interfaces',
-        null=True,
+    # Override ComponentModel._name to specify naturalize_interface function
+    _name = NaturalOrderingField(
+        target_field='name',
+        naturalize_function=naturalize_interface,
+        max_length=100,
         blank=True
         blank=True
     )
     )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     _connected_interface = models.OneToOneField(
     _connected_interface = models.OneToOneField(
         to='self',
         to='self',
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
@@ -703,7 +642,7 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
-        'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
+        'device', 'name', 'label', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
     ]
     ]
 
 
     class Meta:
     class Meta:
@@ -717,6 +656,7 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
         return (
         return (
             self.device.identifier if self.device else None,
             self.device.identifier if self.device else None,
             self.name,
             self.name,
+            self.label,
             self.lag.name if self.lag else None,
             self.lag.name if self.lag else None,
             self.get_type_display(),
             self.get_type_display(),
             self.enabled,
             self.enabled,
@@ -849,19 +789,6 @@ class FrontPort(CableTermination, ComponentModel):
     """
     """
     A pass-through port on the front of a Device.
     A pass-through port on the front of a Device.
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='frontports'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PortTypeChoices
         choices=PortTypeChoices
@@ -877,7 +804,7 @@ class FrontPort(CableTermination, ComponentModel):
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
-    csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description']
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -886,9 +813,6 @@ class FrontPort(CableTermination, ComponentModel):
             ('rear_port', 'rear_port_position'),
             ('rear_port', 'rear_port_position'),
         )
         )
 
 
-    def __str__(self):
-        return self.name
-
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('dcim:frontport', kwargs={'pk': self.pk})
         return reverse('dcim:frontport', kwargs={'pk': self.pk})
 
 
@@ -896,6 +820,7 @@ class FrontPort(CableTermination, ComponentModel):
         return (
         return (
             self.device.identifier,
             self.device.identifier,
             self.name,
             self.name,
+            self.label,
             self.get_type_display(),
             self.get_type_display(),
             self.rear_port.name,
             self.rear_port.name,
             self.rear_port_position,
             self.rear_port_position,
@@ -924,19 +849,6 @@ class RearPort(CableTermination, ComponentModel):
     """
     """
     A pass-through port on the rear of a Device.
     A pass-through port on the rear of a Device.
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='rearports'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=PortTypeChoices
         choices=PortTypeChoices
@@ -947,15 +859,12 @@ class RearPort(CableTermination, ComponentModel):
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
-    csv_headers = ['device', 'name', 'type', 'positions', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'positions', 'description']
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
         unique_together = ('device', 'name')
         unique_together = ('device', 'name')
 
 
-    def __str__(self):
-        return self.name
-
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('dcim:rearport', kwargs={'pk': self.pk})
         return reverse('dcim:rearport', kwargs={'pk': self.pk})
 
 
@@ -963,6 +872,7 @@ class RearPort(CableTermination, ComponentModel):
         return (
         return (
             self.device.identifier,
             self.device.identifier,
             self.name,
             self.name,
+            self.label,
             self.get_type_display(),
             self.get_type_display(),
             self.positions,
             self.positions,
             self.description,
             self.description,
@@ -978,25 +888,6 @@ class DeviceBay(ComponentModel):
     """
     """
     An empty space within a Device which can house a child device
     An empty space within a Device which can house a child device
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='device_bays'
-    )
-    name = models.CharField(
-        max_length=50,
-        verbose_name='Name'
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     installed_device = models.OneToOneField(
     installed_device = models.OneToOneField(
         to='dcim.Device',
         to='dcim.Device',
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
@@ -1006,17 +897,12 @@ class DeviceBay(ComponentModel):
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
-    csv_headers = ['device', 'name', 'installed_device', 'description']
+    csv_headers = ['device', 'name', 'label', 'installed_device', 'description']
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
         unique_together = ('device', 'name')
         unique_together = ('device', 'name')
 
 
-    def __str__(self):
-        if self.label:
-            return '{} - {} ({})'.format(self.device.name, self.name, self.label)
-        return '{} - {}'.format(self.device.name, self.name)
-
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('dcim:devicebay', kwargs={'pk': self.pk})
         return reverse('dcim:devicebay', kwargs={'pk': self.pk})
 
 
@@ -1024,6 +910,7 @@ class DeviceBay(ComponentModel):
         return (
         return (
             self.device.identifier,
             self.device.identifier,
             self.name,
             self.name,
+            self.label,
             self.installed_device.identifier if self.installed_device else None,
             self.installed_device.identifier if self.installed_device else None,
             self.description,
             self.description,
         )
         )
@@ -1061,11 +948,6 @@ class InventoryItem(ComponentModel):
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
     InventoryItems are used only for inventory purposes.
     InventoryItems are used only for inventory purposes.
     """
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='inventory_items'
-    )
     parent = models.ForeignKey(
     parent = models.ForeignKey(
         to='self',
         to='self',
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
@@ -1073,15 +955,6 @@ class InventoryItem(ComponentModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    name = models.CharField(
-        max_length=50,
-        verbose_name='Name'
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     manufacturer = models.ForeignKey(
     manufacturer = models.ForeignKey(
         to='dcim.Manufacturer',
         to='dcim.Manufacturer',
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
@@ -1116,16 +989,13 @@ class InventoryItem(ComponentModel):
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
-        'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
+        'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
     ]
     ]
 
 
     class Meta:
     class Meta:
         ordering = ('device__id', 'parent__id', '_name')
         ordering = ('device__id', 'parent__id', '_name')
         unique_together = ('device', 'parent', 'name')
         unique_together = ('device', 'parent', 'name')
 
 
-    def __str__(self):
-        return self.name
-
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
         return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
 
 
@@ -1133,6 +1003,7 @@ class InventoryItem(ComponentModel):
         return (
         return (
             self.device.name or '{{{}}}'.format(self.device.pk),
             self.device.name or '{{{}}}'.format(self.device.pk),
             self.name,
             self.name,
+            self.label,
             self.manufacturer.name if self.manufacturer else None,
             self.manufacturer.name if self.manufacturer else None,
             self.part_id,
             self.part_id,
             self.serial,
             self.serial,

+ 27 - 49
netbox/dcim/tables.py

@@ -110,21 +110,6 @@ POWERPANEL_POWERFEED_COUNT = """
 """
 """
 
 
 
 
-def get_component_template_actions(model_name):
-    return """
-        {{% if perms.dcim.change_{model_name} %}}
-            <a href="{{% url 'dcim:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning">
-                <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
-            </a>
-        {{% endif %}}
-        {{% if perms.dcim.delete_{model_name} %}}
-            <a href="{{% url 'dcim:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger">
-                <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
-            </a>
-        {{% endif %}}
-    """.format(model_name=model_name).strip()
-
-
 #
 #
 # Regions
 # Regions
 #
 #
@@ -401,10 +386,9 @@ class ComponentTemplateTable(BaseTable):
 
 
 
 
 class ConsolePortTemplateTable(ComponentTemplateTable):
 class ConsolePortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('consoleporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=ConsolePortTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -414,10 +398,9 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
 
 
 
 
 class ConsoleServerPortTemplateTable(ComponentTemplateTable):
 class ConsoleServerPortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('consoleserverporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=ConsoleServerPortTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -427,10 +410,9 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
 
 
 
 
 class PowerPortTemplateTable(ComponentTemplateTable):
 class PowerPortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('powerporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=PowerPortTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -440,10 +422,9 @@ class PowerPortTemplateTable(ComponentTemplateTable):
 
 
 
 
 class PowerOutletTemplateTable(ComponentTemplateTable):
 class PowerOutletTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('poweroutlettemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=PowerOutletTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -456,10 +437,9 @@ class InterfaceTemplateTable(ComponentTemplateTable):
     mgmt_only = BooleanColumn(
     mgmt_only = BooleanColumn(
         verbose_name='Management Only'
         verbose_name='Management Only'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('interfacetemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=InterfaceTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -472,10 +452,9 @@ class FrontPortTemplateTable(ComponentTemplateTable):
     rear_port_position = tables.Column(
     rear_port_position = tables.Column(
         verbose_name='Position'
         verbose_name='Position'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('frontporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=FrontPortTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -485,10 +464,9 @@ class FrontPortTemplateTable(ComponentTemplateTable):
 
 
 
 
 class RearPortTemplateTable(ComponentTemplateTable):
 class RearPortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('rearporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=RearPortTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -498,10 +476,9 @@ class RearPortTemplateTable(ComponentTemplateTable):
 
 
 
 
 class DeviceBayTemplateTable(ComponentTemplateTable):
 class DeviceBayTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('devicebaytemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=DeviceBayTemplate,
+        buttons=('edit', 'delete')
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -784,9 +761,10 @@ class InventoryItemTable(DeviceComponentTable):
     class Meta(DeviceComponentTable.Meta):
     class Meta(DeviceComponentTable.Meta):
         model = InventoryItem
         model = InventoryItem
         fields = (
         fields = (
-            'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
+            'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
+            'discovered',
         )
         )
-        default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
+        default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
 
 
 
 
 #
 #

+ 4 - 0
netbox/dcim/tests/test_api.py

@@ -1,5 +1,6 @@
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
@@ -131,6 +132,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
             },
             },
         ]
         ]
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_site_graphs(self):
     def test_get_site_graphs(self):
         """
         """
         Test retrieval of Graphs assigned to Sites.
         Test retrieval of Graphs assigned to Sites.
@@ -900,6 +902,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
             },
             },
         ]
         ]
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_device_graphs(self):
     def test_get_device_graphs(self):
         """
         """
         Test retrieval of Graphs assigned to Devices.
         Test retrieval of Graphs assigned to Devices.
@@ -1156,6 +1159,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
             },
             },
         ]
         ]
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_interface_graphs(self):
     def test_get_interface_graphs(self):
         """
         """
         Test retrieval of Graphs assigned to Devices.
         Test retrieval of Graphs assigned to Devices.

+ 10 - 8
netbox/dcim/tests/test_views.py

@@ -4,6 +4,7 @@ import pytz
 import yaml
 import yaml
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from netaddr import EUI
 from netaddr import EUI
 
 
@@ -376,6 +377,7 @@ class DeviceTypeTestCase(
             'is_full_depth': False,
             'is_full_depth': False,
         }
         }
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_import_objects(self):
     def test_import_objects(self):
         """
         """
         Custom import test for YAML-based imports (versus CSV)
         Custom import test for YAML-based imports (versus CSV)
@@ -479,45 +481,45 @@ device-bays:
         self.assertEqual(dt.comments, 'test comment')
         self.assertEqual(dt.comments, 'test comment')
 
 
         # Verify all of the components were created
         # Verify all of the components were created
-        self.assertEqual(dt.consoleport_templates.count(), 3)
+        self.assertEqual(dt.consoleporttemplates.count(), 3)
         cp1 = ConsolePortTemplate.objects.first()
         cp1 = ConsolePortTemplate.objects.first()
         self.assertEqual(cp1.name, 'Console Port 1')
         self.assertEqual(cp1.name, 'Console Port 1')
         self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
         self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
 
 
-        self.assertEqual(dt.consoleserverport_templates.count(), 3)
+        self.assertEqual(dt.consoleserverporttemplates.count(), 3)
         csp1 = ConsoleServerPortTemplate.objects.first()
         csp1 = ConsoleServerPortTemplate.objects.first()
         self.assertEqual(csp1.name, 'Console Server Port 1')
         self.assertEqual(csp1.name, 'Console Server Port 1')
         self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
         self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
 
 
-        self.assertEqual(dt.powerport_templates.count(), 3)
+        self.assertEqual(dt.powerporttemplates.count(), 3)
         pp1 = PowerPortTemplate.objects.first()
         pp1 = PowerPortTemplate.objects.first()
         self.assertEqual(pp1.name, 'Power Port 1')
         self.assertEqual(pp1.name, 'Power Port 1')
         self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
         self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
 
 
-        self.assertEqual(dt.poweroutlet_templates.count(), 3)
+        self.assertEqual(dt.poweroutlettemplates.count(), 3)
         po1 = PowerOutletTemplate.objects.first()
         po1 = PowerOutletTemplate.objects.first()
         self.assertEqual(po1.name, 'Power Outlet 1')
         self.assertEqual(po1.name, 'Power Outlet 1')
         self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
         self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
         self.assertEqual(po1.power_port, pp1)
         self.assertEqual(po1.power_port, pp1)
         self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
         self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
 
 
-        self.assertEqual(dt.interface_templates.count(), 3)
+        self.assertEqual(dt.interfacetemplates.count(), 3)
         iface1 = InterfaceTemplate.objects.first()
         iface1 = InterfaceTemplate.objects.first()
         self.assertEqual(iface1.name, 'Interface 1')
         self.assertEqual(iface1.name, 'Interface 1')
         self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
         self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
         self.assertTrue(iface1.mgmt_only)
         self.assertTrue(iface1.mgmt_only)
 
 
-        self.assertEqual(dt.rearport_templates.count(), 3)
+        self.assertEqual(dt.rearporttemplates.count(), 3)
         rp1 = RearPortTemplate.objects.first()
         rp1 = RearPortTemplate.objects.first()
         self.assertEqual(rp1.name, 'Rear Port 1')
         self.assertEqual(rp1.name, 'Rear Port 1')
 
 
-        self.assertEqual(dt.frontport_templates.count(), 3)
+        self.assertEqual(dt.frontporttemplates.count(), 3)
         fp1 = FrontPortTemplate.objects.first()
         fp1 = FrontPortTemplate.objects.first()
         self.assertEqual(fp1.name, 'Front Port 1')
         self.assertEqual(fp1.name, 'Front Port 1')
         self.assertEqual(fp1.rear_port, rp1)
         self.assertEqual(fp1.rear_port, rp1)
         self.assertEqual(fp1.rear_port_position, 1)
         self.assertEqual(fp1.rear_port_position, 1)
 
 
-        self.assertEqual(dt.device_bay_templates.count(), 3)
+        self.assertEqual(dt.devicebaytemplates.count(), 3)
         db1 = DeviceBayTemplate.objects.first()
         db1 = DeviceBayTemplate.objects.first()
         self.assertEqual(db1.name, 'Device Bay 1')
         self.assertEqual(db1.name, 'Device Bay 1')
 
 

+ 8 - 0
netbox/dcim/urls.py

@@ -98,6 +98,7 @@ urlpatterns = [
     # Console port templates
     # Console port templates
     path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
     path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
     path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
     path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
+    path('console-port-templates/rename/', views.ConsolePortTemplateBulkRenameView.as_view(), name='consoleporttemplate_bulk_rename'),
     path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
     path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
     path('console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
     path('console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
     path('console-port-templates/<int:pk>/delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
     path('console-port-templates/<int:pk>/delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
@@ -105,6 +106,7 @@ urlpatterns = [
     # Console server port templates
     # Console server port templates
     path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
     path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
     path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
     path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
+    path('console-server-port-templates/rename/', views.ConsoleServerPortTemplateBulkRenameView.as_view(), name='consoleserverporttemplate_bulk_rename'),
     path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
     path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
     path('console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
     path('console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
     path('console-server-port-templates/<int:pk>/delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
     path('console-server-port-templates/<int:pk>/delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
@@ -112,6 +114,7 @@ urlpatterns = [
     # Power port templates
     # Power port templates
     path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
     path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
     path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
     path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
+    path('power-port-templates/rename/', views.PowerPortTemplateBulkRenameView.as_view(), name='powerporttemplate_bulk_rename'),
     path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
     path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
     path('power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
     path('power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
     path('power-port-templates/<int:pk>/delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
     path('power-port-templates/<int:pk>/delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
@@ -119,6 +122,7 @@ urlpatterns = [
     # Power outlet templates
     # Power outlet templates
     path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
     path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
     path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
     path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
+    path('power-outlet-templates/rename/', views.PowerOutletTemplateBulkRenameView.as_view(), name='poweroutlettemplate_bulk_rename'),
     path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
     path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
     path('power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
     path('power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
     path('power-outlet-templates/<int:pk>/delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
     path('power-outlet-templates/<int:pk>/delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
@@ -126,6 +130,7 @@ urlpatterns = [
     # Interface templates
     # Interface templates
     path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
     path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
     path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
     path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
+    path('interface-templates/rename/', views.InterfaceTemplateBulkRenameView.as_view(), name='interfacetemplate_bulk_rename'),
     path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
     path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
     path('interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
     path('interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
     path('interface-templates/<int:pk>/delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
     path('interface-templates/<int:pk>/delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
@@ -133,6 +138,7 @@ urlpatterns = [
     # Front port templates
     # Front port templates
     path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
     path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
     path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
     path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
+    path('front-port-templates/rename/', views.FrontPortTemplateBulkRenameView.as_view(), name='frontporttemplate_bulk_rename'),
     path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
     path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
     path('front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
     path('front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
     path('front-port-templates/<int:pk>/delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
     path('front-port-templates/<int:pk>/delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
@@ -140,6 +146,7 @@ urlpatterns = [
     # Rear port templates
     # Rear port templates
     path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
     path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
     path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
     path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
+    path('rear-port-templates/rename/', views.RearPortTemplateBulkRenameView.as_view(), name='rearporttemplate_bulk_rename'),
     path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
     path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
     path('rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
     path('rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
     path('rear-port-templates/<int:pk>/delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
     path('rear-port-templates/<int:pk>/delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
@@ -147,6 +154,7 @@ urlpatterns = [
     # Device bay templates
     # Device bay templates
     path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
     path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
     path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
     path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
+    path('device-bay-templates/rename/', views.DeviceBayTemplateBulkRenameView.as_view(), name='devicebaytemplate_bulk_rename'),
     path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
     path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
     path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
     path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
     path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
     path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),

+ 42 - 10
netbox/dcim/views.py

@@ -640,6 +640,10 @@ class ConsolePortTemplateBulkEditView(BulkEditView):
     form = forms.ConsolePortTemplateBulkEditForm
     form = forms.ConsolePortTemplateBulkEditForm
 
 
 
 
+class ConsolePortTemplateBulkRenameView(BulkRenameView):
+    queryset = ConsolePortTemplate.objects.all()
+
+
 class ConsolePortTemplateBulkDeleteView(BulkDeleteView):
 class ConsolePortTemplateBulkDeleteView(BulkDeleteView):
     queryset = ConsolePortTemplate.objects.all()
     queryset = ConsolePortTemplate.objects.all()
     table = tables.ConsolePortTemplateTable
     table = tables.ConsolePortTemplateTable
@@ -671,6 +675,10 @@ class ConsoleServerPortTemplateBulkEditView(BulkEditView):
     form = forms.ConsoleServerPortTemplateBulkEditForm
     form = forms.ConsoleServerPortTemplateBulkEditForm
 
 
 
 
+class ConsoleServerPortTemplateBulkRenameView(BulkRenameView):
+    queryset = ConsoleServerPortTemplate.objects.all()
+
+
 class ConsoleServerPortTemplateBulkDeleteView(BulkDeleteView):
 class ConsoleServerPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = ConsoleServerPortTemplate.objects.all()
     queryset = ConsoleServerPortTemplate.objects.all()
     table = tables.ConsoleServerPortTemplateTable
     table = tables.ConsoleServerPortTemplateTable
@@ -702,6 +710,10 @@ class PowerPortTemplateBulkEditView(BulkEditView):
     form = forms.PowerPortTemplateBulkEditForm
     form = forms.PowerPortTemplateBulkEditForm
 
 
 
 
+class PowerPortTemplateBulkRenameView(BulkRenameView):
+    queryset = PowerPortTemplate.objects.all()
+
+
 class PowerPortTemplateBulkDeleteView(BulkDeleteView):
 class PowerPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = PowerPortTemplate.objects.all()
     queryset = PowerPortTemplate.objects.all()
     table = tables.PowerPortTemplateTable
     table = tables.PowerPortTemplateTable
@@ -733,6 +745,10 @@ class PowerOutletTemplateBulkEditView(BulkEditView):
     form = forms.PowerOutletTemplateBulkEditForm
     form = forms.PowerOutletTemplateBulkEditForm
 
 
 
 
+class PowerOutletTemplateBulkRenameView(BulkRenameView):
+    queryset = PowerOutletTemplate.objects.all()
+
+
 class PowerOutletTemplateBulkDeleteView(BulkDeleteView):
 class PowerOutletTemplateBulkDeleteView(BulkDeleteView):
     queryset = PowerOutletTemplate.objects.all()
     queryset = PowerOutletTemplate.objects.all()
     table = tables.PowerOutletTemplateTable
     table = tables.PowerOutletTemplateTable
@@ -764,6 +780,10 @@ class InterfaceTemplateBulkEditView(BulkEditView):
     form = forms.InterfaceTemplateBulkEditForm
     form = forms.InterfaceTemplateBulkEditForm
 
 
 
 
+class InterfaceTemplateBulkRenameView(BulkRenameView):
+    queryset = InterfaceTemplate.objects.all()
+
+
 class InterfaceTemplateBulkDeleteView(BulkDeleteView):
 class InterfaceTemplateBulkDeleteView(BulkDeleteView):
     queryset = InterfaceTemplate.objects.all()
     queryset = InterfaceTemplate.objects.all()
     table = tables.InterfaceTemplateTable
     table = tables.InterfaceTemplateTable
@@ -795,6 +815,10 @@ class FrontPortTemplateBulkEditView(BulkEditView):
     form = forms.FrontPortTemplateBulkEditForm
     form = forms.FrontPortTemplateBulkEditForm
 
 
 
 
+class FrontPortTemplateBulkRenameView(BulkRenameView):
+    queryset = FrontPortTemplate.objects.all()
+
+
 class FrontPortTemplateBulkDeleteView(BulkDeleteView):
 class FrontPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = FrontPortTemplate.objects.all()
     queryset = FrontPortTemplate.objects.all()
     table = tables.FrontPortTemplateTable
     table = tables.FrontPortTemplateTable
@@ -826,6 +850,10 @@ class RearPortTemplateBulkEditView(BulkEditView):
     form = forms.RearPortTemplateBulkEditForm
     form = forms.RearPortTemplateBulkEditForm
 
 
 
 
+class RearPortTemplateBulkRenameView(BulkRenameView):
+    queryset = RearPortTemplate.objects.all()
+
+
 class RearPortTemplateBulkDeleteView(BulkDeleteView):
 class RearPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = RearPortTemplate.objects.all()
     queryset = RearPortTemplate.objects.all()
     table = tables.RearPortTemplateTable
     table = tables.RearPortTemplateTable
@@ -857,6 +885,10 @@ class DeviceBayTemplateBulkEditView(BulkEditView):
     form = forms.DeviceBayTemplateBulkEditForm
     form = forms.DeviceBayTemplateBulkEditForm
 
 
 
 
+class DeviceBayTemplateBulkRenameView(BulkRenameView):
+    queryset = DeviceBayTemplate.objects.all()
+
+
 class DeviceBayTemplateBulkDeleteView(BulkDeleteView):
 class DeviceBayTemplateBulkDeleteView(BulkDeleteView):
     queryset = DeviceBayTemplate.objects.all()
     queryset = DeviceBayTemplate.objects.all()
     table = tables.DeviceBayTemplateTable
     table = tables.DeviceBayTemplateTable
@@ -952,7 +984,7 @@ class DeviceView(ObjectView):
             vc_members = []
             vc_members = []
 
 
         # Console ports
         # Console ports
-        console_ports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'connected_endpoint__device', 'cable',
             'connected_endpoint__device', 'cable',
         )
         )
 
 
@@ -964,7 +996,7 @@ class DeviceView(ObjectView):
         )
         )
 
 
         # Power ports
         # Power ports
-        power_ports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             '_connected_poweroutlet__device', 'cable',
             '_connected_poweroutlet__device', 'cable',
         )
         )
 
 
@@ -982,15 +1014,15 @@ class DeviceView(ObjectView):
         )
         )
 
 
         # Front ports
         # Front ports
-        front_ports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'rear_port', 'cable',
             'rear_port', 'cable',
         )
         )
 
 
         # Rear ports
         # Rear ports
-        rear_ports = RearPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related('cable')
+        rearports = RearPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related('cable')
 
 
         # Device bays
         # Device bays
-        device_bays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'installed_device__device_type__manufacturer',
             'installed_device__device_type__manufacturer',
         )
         )
 
 
@@ -1011,14 +1043,14 @@ class DeviceView(ObjectView):
 
 
         return render(request, 'dcim/device.html', {
         return render(request, 'dcim/device.html', {
             'device': device,
             'device': device,
-            'console_ports': console_ports,
+            'consoleports': consoleports,
             'consoleserverports': consoleserverports,
             'consoleserverports': consoleserverports,
-            'power_ports': power_ports,
+            'powerports': powerports,
             'poweroutlets': poweroutlets,
             'poweroutlets': poweroutlets,
             'interfaces': interfaces,
             'interfaces': interfaces,
-            'device_bays': device_bays,
-            'front_ports': front_ports,
-            'rear_ports': rear_ports,
+            'devicebays': devicebays,
+            'frontports': frontports,
+            'rearports': rearports,
             'services': services,
             'services': services,
             'secrets': secrets,
             'secrets': secrets,
             'vc_members': vc_members,
             'vc_members': vc_members,

+ 2 - 0
netbox/secrets/tests/test_views.py

@@ -1,5 +1,6 @@
 import base64
 import base64
 
 
+from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
@@ -96,6 +97,7 @@ class SecretTestCase(
         self.session_key = SessionKey(userkey=userkey)
         self.session_key = SessionKey(userkey=userkey)
         self.session_key.save(master_key)
         self.session_key.save(master_key)
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_import_objects(self):
     def test_import_objects(self):
         self.add_permissions('secrets.add_secret')
         self.add_permissions('secrets.add_secret')
 
 

+ 471 - 416
netbox/templates/dcim/device.html

@@ -101,7 +101,7 @@
         </li>
         </li>
         <li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
         <li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
             <a href="{% url 'dcim:device_inventory' pk=device.pk %}">
             <a href="{% url 'dcim:device_inventory' pk=device.pk %}">
-                Inventory <span class="badge">{{ device.inventory_items.count }}</span>
+                Inventory <span class="badge">{{ device.inventoryitems.unrestricted.count }}</span>
             </a>
             </a>
         </li>
         </li>
         {% if perms.dcim.napalm_read_device %}
         {% if perms.dcim.napalm_read_device %}
@@ -329,86 +329,6 @@
             {% plugin_left_page device %}
             {% plugin_left_page device %}
         </div>
         </div>
         <div class="col-md-6">
         <div class="col-md-6">
-            {% if console_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Console Ports</strong>
-                        </div>
-                        <table class="table table-hover panel-body component-list">
-                            {% for cp in console_ports %}
-                                {% include 'dcim/inc/consoleport.html' %}
-                            {% endfor %}
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if console_ports and perms.dcim.change_consoleport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if console_ports and perms.dcim.delete_consoleport %}
-                                <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if console_ports and perms.dcim.add_consoleport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
-                                    </a>
-                                </div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if power_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Power Ports</strong>
-                        </div>
-                        <table class="table table-hover panel-body component-list">
-                            {% for pp in power_ports %}
-                                {% include 'dcim/inc/powerport.html' %}
-                            {% endfor %}
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if power_ports and perms.dcim.change_powerport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if power_ports and perms.dcim.delete_powerport %}
-                                <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if power_ports and perms.dcim.add_powerport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
-                                    </a>
-                                </div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
             {% if power_ports and poweroutlets %}
             {% if power_ports and poweroutlets %}
                 <div class="panel panel-default">
                 <div class="panel panel-default">
                     <div class="panel-heading">
                     <div class="panel-heading">
@@ -554,355 +474,490 @@
     </div>
     </div>
     <div class="row">
     <div class="row">
         <div class="col-md-12">
         <div class="col-md-12">
-            {% if device_bays or device.device_type.is_parent_device %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Device Bays</strong>
-                        </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>Status</th>
-                                    <th>Description</th>
-                                    <th colspan="2">Installed Device</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for devicebay in device_bays %}
-                                    {% include 'dcim/inc/devicebay.html' %}
-                                {% empty %}
+            <ul class="nav nav-tabs" role="tablist">
+                <li role="presentation" class="active">
+                    <a href="#interfaces" role="tab" data-toggle="tab">Interfaces {% badge interfaces|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#frontports" role="tab" data-toggle="tab">Front Ports {% badge frontports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#rearports" role="tab" data-toggle="tab">Rear Ports {% badge rearports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleports" role="tab" data-toggle="tab">Console Ports {% badge consoleports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleserverports" role="tab" data-toggle="tab">Console Server Ports {% badge consoleserverports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#powerports" role="tab" data-toggle="tab">Power Ports {% badge powerports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#poweroutlets" role="tab" data-toggle="tab">Power Outlets {% badge poweroutlets|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#devicebays" role="tab" data-toggle="tab">Device Bays {% badge devicebays|length %}</a>
+                </li>
+            </ul>
+            <div class="tab-content">
+                <div role="tabpanel" class="tab-pane active" id="interfaces">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Interfaces</strong>
+                                <div class="pull-right noprint">
+                                    <button class="btn btn-default btn-xs toggle-ips" selected="selected">
+                                        <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
+                                    </button>
+                                </div>
+                                <div class="col-md-2 pull-right noprint">
+                                    <input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
+                                </div>
+                            </div>
+                            <table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
+                                <thead>
                                     <tr>
                                     <tr>
-                                        <td colspan="5" class="text-center text-muted">&mdash; No device bays defined &mdash;</td>
+                                        {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>LAG</th>
+                                        <th>Description</th>
+                                        <th>MTU</th>
+                                        <th>Mode</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
                                     </tr>
                                     </tr>
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if device_bays and perms.dcim.change_devicebay %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                            {% endif %}
-                            {% if device_bays and perms.dcim.delete_devicebay %}
-                                <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_devicebay %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                         </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if interfaces %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Interfaces</strong>
-                            <div class="pull-right noprint">
-                                <button class="btn btn-default btn-xs toggle-ips" selected="selected">
-                                    <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
-                                </button>
+                                </thead>
+                                <tbody>
+                                    {% for iface in interfaces %}
+                                        {% include 'dcim/inc/interface.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if interfaces and perms.dcim.change_interface %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                {% endif %}
+                                {% if interfaces and perms.dcim.change_interface %}
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if interfaces and perms.dcim.delete_interface %}
+                                    <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_interface %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                             </div>
+                        </div>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="frontports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Front Ports</strong>
                             </div>
                             </div>
-                            <div class="col-md-2 pull-right noprint">
-                                <input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Rear Port</th>
+                                        <th>Position</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for frontport in frontports %}
+                                        {% include 'dcim/inc/frontport.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if frontports and perms.dcim.change_frontport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if frontports and perms.dcim.delete_frontport %}
+                                    <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_frontport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
                             </div>
                             </div>
                         </div>
                         </div>
-                        <table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>LAG</th>
-                                    <th>Description</th>
-                                    <th>MTU</th>
-                                    <th>Mode</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for iface in interfaces %}
-                                    {% include 'dcim/inc/interface.html' %}
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if interfaces and perms.dcim.change_interface %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                            {% endif %}
-                            {% if interfaces and perms.dcim.change_interface %}
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if interfaces and perms.dcim.delete_interface %}
-                                <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_interface %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                         </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if consoleserverports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Console Server Ports</strong>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="rearports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Rear Ports</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Positions</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for rearport in rearports %}
+                                        {% include 'dcim/inc/rearport.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if rearports and perms.dcim.change_rearport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if rearports and perms.dcim.delete_rearport %}
+                                    <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_rearport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                            </div>
                         </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>Type</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for csp in consoleserverports %}
-                                    {% include 'dcim/inc/consoleserverport.html' %}
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Console Ports</strong>
+                            </div>
+                            <table class="table table-hover panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                {% for cp in consoleports %}
+                                    {% include 'dcim/inc/consoleport.html' %}
                                 {% endfor %}
                                 {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if consoleserverports and perms.dcim.change_consoleport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if consoleserverports and perms.dcim.delete_consoleserverport %}
-                                <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_consoleserverport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if consoleports and perms.dcim.change_consoleport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if consoleports and perms.dcim.delete_consoleport %}
+                                    <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if consoleports and perms.dcim.add_consoleport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
+                                        </a>
+                                    </div>
+                                {% endif %}
+                            </div>
                         </div>
                         </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if poweroutlets %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Power Outlets</strong>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleserverports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Console Server Ports</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for csp in consoleserverports %}
+                                        {% include 'dcim/inc/consoleserverport.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if consoleserverports and perms.dcim.change_consoleport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if consoleserverports and perms.dcim.delete_consoleserverport %}
+                                    <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_consoleserverport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                            </div>
                         </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                <th>Type</th>
-                                    <th>Input/Leg</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="3">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for po in poweroutlets %}
-                                    {% include 'dcim/inc/poweroutlet.html' %}
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="powerports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Power Ports</strong>
+                            </div>
+                            <table class="table table-hover panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Draw</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                {% for pp in powerports %}
+                                    {% include 'dcim/inc/powerport.html' %}
                                 {% endfor %}
                                 {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if poweroutlets and perms.dcim.change_powerport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if poweroutlets and perms.dcim.delete_poweroutlet %}
-                                <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_poweroutlet %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if front_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Front Ports</strong>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if powerports and perms.dcim.change_powerport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if powerports and perms.dcim.delete_powerport %}
+                                    <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if powerports and perms.dcim.add_powerport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
+                                        </a>
+                                    </div>
+                                {% endif %}
+                            </div>
                         </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="poweroutlets">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Power Outlets</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
                                     <th>Type</th>
                                     <th>Type</th>
-                                    <th>Rear Port</th>
-                                    <th>Position</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for frontport in front_ports %}
-                                    {% include 'dcim/inc/frontport.html' %}
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if front_ports and perms.dcim.change_frontport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if front_ports and perms.dcim.delete_frontport %}
-                                <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_frontport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if rear_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Rear Ports</strong>
+                                        <th>Input/Leg</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="3">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for po in poweroutlets %}
+                                        {% include 'dcim/inc/poweroutlet.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if poweroutlets and perms.dcim.change_powerport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if poweroutlets and perms.dcim.delete_poweroutlet %}
+                                    <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_poweroutlet %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                            </div>
                         </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>Type</th>
-                                    <th>Positions</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for rearport in rear_ports %}
-                                    {% include 'dcim/inc/rearport.html' %}
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if rear_ports and perms.dcim.change_rearport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if rear_ports and perms.dcim.delete_rearport %}
-                                <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_rearport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="devicebays">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Device Bays</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Status</th>
+                                        <th>Description</th>
+                                        <th colspan="2">Installed Device</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for devicebay in devicebays %}
+                                        {% include 'dcim/inc/devicebay.html' %}
+                                    {% empty %}
+                                        <tr>
+                                            <td colspan="5" class="text-center text-muted">&mdash; No device bays defined &mdash;</td>
+                                        </tr>
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if devicebays and perms.dcim.change_devicebay %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                {% endif %}
+                                {% if devicebays and perms.dcim.delete_devicebay %}
+                                    <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_devicebay %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                             </div>
                         </div>
                         </div>
-                    </div>
-                </form>
-            {% endif %}
+                    </form>
+                </div>
+            </div>
         </div>
         </div>
     </div>
     </div>
 {% include 'inc/modal.html' with name='graphs' title='Graphs' %}
 {% include 'inc/modal.html' with name='graphs' title='Graphs' %}

+ 139 - 131
netbox/templates/dcim/devicetype.html

@@ -63,149 +63,157 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
-<div class="row">
-    <div class="col-md-6">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Chassis</strong>
-            </div>
-            <table class="table table-hover panel-body attr-table">
-                <tr>
-                    <td>Manufacturer</td>
-                    <td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
-                </tr>
-                <tr>
-                    <td>Model Name</td>
-                    <td>
-                        {{ devicetype.model }}<br/>
-                        <small class="text-muted">{{ devicetype.slug }}</small>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Part Number</td>
-                    <td>{{ devicetype.part_number|placeholder }}</td>
-                </tr>
-                <tr>
-                    <td>Height (U)</td>
-                    <td>{{ devicetype.u_height }}</td>
-                </tr>
-                <tr>
-                    <td>Full Depth</td>
-                    <td>
-                        {% if devicetype.is_full_depth %}
-                            <i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
-                        {% else %}
-                            <i class="glyphicon glyphicon-remove text-danger" title="No"></i>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Parent/Child</td>
-                    <td>
-                        {{ devicetype.get_subdevice_role_display|placeholder }}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Front Image</td>
-                    <td>
-                        {% if devicetype.front_image %}
-                            <a href="{{ devicetype.front_image.url }}">
-                                <img src="{{ devicetype.front_image.url }}" alt="{{ devicetype.front_image.name }}" class="img-responsive" />
-                            </a>
-                        {% else %}
-                            <span class="text-muted">&mdash;</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Rear Image</td>
-                    <td>
-                        {% if devicetype.rear_image %}
-                            <a href="{{ devicetype.rear_image.url }}">
-                                <img src="{{ devicetype.rear_image.url }}" alt="{{ devicetype.rear_image.name }}" class="img-responsive" />
-                            </a>
-                        {% else %}
-                            <span class="text-muted">&mdash;</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Instances</td>
-                    <td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ instance_count }}</a></td>
-                </tr>
-            </table>
-        </div>
-        {% plugin_left_page devicetype %}
-    </div>
-    <div class="col-md-6">
-        {% include 'inc/custom_fields_panel.html' with obj=devicetype %}
-        {% include 'extras/inc/tags_panel.html' with tags=devicetype.tags.all url='dcim:devicetype_list' %}
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Comments</strong>
-            </div>
-            <div class="panel-body rendered-markdown">
-                {% if devicetype.comments %}
-                    {{ devicetype.comments|render_markdown }}
-                {% else %}
-                    <span class="text-muted">None</span>
-                {% endif %}
-            </div>
-        </div>
-        {% plugin_right_page devicetype %}
-    </div>
-</div>
-{% if devicetype.consoleport_templates.exists or devicetype.powerport_templates.exists %}
     <div class="row">
     <div class="row">
         <div class="col-md-6">
         <div class="col-md-6">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:consoleporttemplate_add' edit_url='dcim:consoleporttemplate_bulk_edit' delete_url='dcim:consoleporttemplate_bulk_delete' %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Chassis</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Manufacturer</td>
+                        <td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
+                    </tr>
+                    <tr>
+                        <td>Model Name</td>
+                        <td>
+                            {{ devicetype.model }}<br/>
+                            <small class="text-muted">{{ devicetype.slug }}</small>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Part Number</td>
+                        <td>{{ devicetype.part_number|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Height (U)</td>
+                        <td>{{ devicetype.u_height }}</td>
+                    </tr>
+                    <tr>
+                        <td>Full Depth</td>
+                        <td>
+                            {% if devicetype.is_full_depth %}
+                                <i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
+                            {% else %}
+                                <i class="glyphicon glyphicon-remove text-danger" title="No"></i>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Parent/Child</td>
+                        <td>
+                            {{ devicetype.get_subdevice_role_display|placeholder }}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Front Image</td>
+                        <td>
+                            {% if devicetype.front_image %}
+                                <a href="{{ devicetype.front_image.url }}">
+                                    <img src="{{ devicetype.front_image.url }}" alt="{{ devicetype.front_image.name }}" class="img-responsive" />
+                                </a>
+                            {% else %}
+                                <span class="text-muted">&mdash;</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Rear Image</td>
+                        <td>
+                            {% if devicetype.rear_image %}
+                                <a href="{{ devicetype.rear_image.url }}">
+                                    <img src="{{ devicetype.rear_image.url }}" alt="{{ devicetype.rear_image.name }}" class="img-responsive" />
+                                </a>
+                            {% else %}
+                                <span class="text-muted">&mdash;</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Instances</td>
+                        <td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ instance_count }}</a></td>
+                    </tr>
+                </table>
+            </div>
+            {% plugin_left_page devicetype %}
         </div>
         </div>
         <div class="col-md-6">
         <div class="col-md-6">
-             {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:powerporttemplate_add' edit_url='dcim:powerporttemplate_bulk_edit' delete_url='dcim:powerporttemplate_bulk_delete' %}
-        </div>
-    </div>
-{% endif %}
-<div class="row">
-    <div class="col-md-12">
-        {% plugin_full_width_page devicetype %}
-    </div>
-</div>
-{% if devicetype.is_parent_device or devicebay_table.rows %}
-    <div class="row">
-        <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicebaytemplate_add' edit_url='dcim:devicebaytemplate_bulk_edit' delete_url='dcim:devicebaytemplate_bulk_delete' %}
-        </div>
-    </div>
-{% endif %}
-{% if devicetype.consoleserverport_templates.exists %}
-    <div class="row">
-        <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:consoleserverporttemplate_add' edit_url='dcim:consoleserverporttemplate_bulk_edit' delete_url='dcim:consoleserverporttemplate_bulk_delete' %}
+            {% include 'inc/custom_fields_panel.html' with obj=devicetype %}
+            {% include 'extras/inc/tags_panel.html' with tags=devicetype.tags.all url='dcim:devicetype_list' %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Comments</strong>
+                </div>
+                <div class="panel-body rendered-markdown">
+                    {% if devicetype.comments %}
+                        {{ devicetype.comments|render_markdown }}
+                    {% else %}
+                        <span class="text-muted">None</span>
+                    {% endif %}
+                </div>
+            </div>
+            {% plugin_right_page devicetype %}
         </div>
         </div>
     </div>
     </div>
-{% endif %}
-{% if devicetype.poweroutlet_templates.exists %}
     <div class="row">
     <div class="row">
         <div class="col-md-12">
         <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:poweroutlettemplate_add' edit_url='dcim:poweroutlettemplate_bulk_edit' delete_url='dcim:poweroutlettemplate_bulk_delete' %}
+            {% plugin_full_width_page devicetype %}
         </div>
         </div>
     </div>
     </div>
-{% endif %}
-{% if devicetype.interface_templates.exists %}
     <div class="row">
     <div class="row">
         <div class="col-md-12">
         <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:interfacetemplate_add' edit_url='dcim:interfacetemplate_bulk_edit' delete_url='dcim:interfacetemplate_bulk_delete' %}
-        </div>
-    </div>
-{% endif %}
-{% if devicetype.frontport_templates.exists or devicetype.rearport_templates.exists %}
-    <div class="row">
-        <div class="col-md-6">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' add_url='dcim:frontporttemplate_add' edit_url='dcim:frontporttemplate_bulk_edit' delete_url='dcim:frontporttemplate_bulk_delete' %}
-        </div>
-        <div class="col-md-6">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' add_url='dcim:rearporttemplate_add' edit_url='dcim:rearporttemplate_bulk_edit' delete_url='dcim:rearporttemplate_bulk_delete' %}
+            <ul class="nav nav-tabs" role="tablist">
+                <li role="presentation" class="active">
+                    <a href="#interfaces" role="tab" data-toggle="tab">Interfaces {% badge interface_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#frontports" role="tab" data-toggle="tab">Front Ports {% badge front_port_table_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#rearports" role="tab" data-toggle="tab">Rear Ports {% badge rear_port_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleports" role="tab" data-toggle="tab">Console Ports {% badge consoleport_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleserverports" role="tab" data-toggle="tab">Console Server Ports {% badge consoleserverport_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#powerports" role="tab" data-toggle="tab">Power Ports {% badge powerport_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#poweroutlets" role="tab" data-toggle="tab">Power Outlets {% badge poweroutlet_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#devicebays" role="tab" data-toggle="tab">Device Bays {% badge devicebay_table.rows|length %}</a>
+                </li>
+            </ul>
+            <div class="tab-content">
+                <div role="tabpanel" class="tab-pane active" id="interfaces">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="frontports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="rearports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleserverports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="powerports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="poweroutlets">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="devicebays">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' %}
+                </div>
+            </div>
         </div>
         </div>
     </div>
     </div>
-{% endif %}
 {% endblock %}
 {% endblock %}

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

@@ -18,8 +18,6 @@
         {% if cp.type %}{{ cp.get_type_display }}{% else %}&mdash;{% endif %}
         {% if cp.type %}{{ cp.get_type_display }}{% else %}&mdash;{% endif %}
     </td>
     </td>
 
 
-    <td></td>
-
     {# Description #}
     {# Description #}
     <td>
     <td>
         {{ cp.description }}
         {{ cp.description }}

+ 40 - 0
netbox/templates/dcim/inc/device_component_table.html

@@ -0,0 +1,40 @@
+{% load helpers %}
+{% load perms %}
+<form method="post">
+    {% csrf_token %}
+    <div class="panel panel-default">
+        <div class="panel-heading">
+            <strong>{{ title }}</strong>
+        </div>
+        <table class="table table-hover panel-body component-list">
+            {% for obj in components %}
+                {% include component_template %}
+            {% endfor %}
+        </table>
+        <div class="panel-footer noprint">
+            {% if components and perms.dcim.change_consoleport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if components and perms.dcim.delete_consoleport %}
+                <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                </button>
+            {% endif %}
+            {% if components and perms.dcim.add_consoleport %}
+                <div class="pull-right">
+                    <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
+                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
+                    </a>
+                </div>
+            {% endif %}
+        </div>
+    </div>
+</form>

+ 11 - 11
netbox/templates/dcim/inc/devicetype_component_table.html

@@ -1,3 +1,4 @@
+{% load helpers %}
 {% if perms.dcim.change_devicetype %}
 {% if perms.dcim.change_devicetype %}
     <form method="post">
     <form method="post">
         {% csrf_token %}
         {% csrf_token %}
@@ -8,19 +9,18 @@
             {% include 'responsive_table.html' %}
             {% include 'responsive_table.html' %}
             <div class="panel-footer noprint">
             <div class="panel-footer noprint">
                 {% if table.rows %}
                 {% if table.rows %}
-                    {% if edit_url %}
-                        <button type="submit" name="_edit" formaction="{% url edit_url %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
-                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
-                        </button>
-                    {% endif %}
-                    {% if delete_url %}
-                        <button type="submit" name="_delete" formaction="{% url delete_url %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
-                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
-                        </button>
-                    {% endif %}
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
+                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                    </button>
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
+                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                    </button>
+                    <button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
+                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                    </button>
                 {% endif %}
                 {% endif %}
                 <div class="pull-right">
                 <div class="pull-right">
-                    <a href="{% url add_url %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-primary btn-xs">
+                    <a href="{% url table.Meta.model|viewname:"add" %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-primary btn-xs">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                         Add {{ title }}
                         Add {{ title }}
                     </a>
                     </a>

+ 1 - 1
netbox/templates/dcim/inc/interface.html

@@ -31,7 +31,7 @@
         {% if iface.description %}
         {% if iface.description %}
             {{ iface.description }}<br/>
             {{ iface.description }}<br/>
         {% endif %}
         {% endif %}
-        {% for tag in iface.tags.all %}
+        {% for tag in iface.tags.all.unrestricted %}
             {% tag tag %}
             {% tag tag %}
         {% empty %}
         {% empty %}
             {% if not iface.description %}&mdash;{% endif %}
             {% if not iface.description %}&mdash;{% endif %}

+ 1 - 1
netbox/templates/extras/inc/tags_panel.html

@@ -4,7 +4,7 @@
         <strong>Tags</strong>
         <strong>Tags</strong>
     </div>
     </div>
     <div class="panel-body">
     <div class="panel-body">
-        {% for tag in tags.all %}
+        {% for tag in tags.all.unrestricted %}
             {% tag tag url %}
             {% tag tag url %}
         {% empty %}
         {% empty %}
             <span class="text-muted">No tags assigned</span>
             <span class="text-muted">No tags assigned</span>

+ 1 - 0
netbox/templates/utilities/templatetags/badge.html

@@ -0,0 +1 @@
+{% if value or show_empty %}<span class="badge">{{ value }}</span>{% endif %}

+ 15 - 7
netbox/utilities/tables.py

@@ -130,25 +130,28 @@ class ButtonsColumn(tables.TemplateColumn):
     :param model: Model class to use for calculating URL view names
     :param model: Model class to use for calculating URL view names
     :param prepend_content: Additional template content to render in the column (optional)
     :param prepend_content: Additional template content to render in the column (optional)
     """
     """
+    buttons = ('changelog', 'edit', 'delete')
     attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
     attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
     # Note that braces are escaped to allow for string formatting prior to template rendering
     # Note that braces are escaped to allow for string formatting prior to template rendering
     template_code = """
     template_code = """
-    <a href="{{% url '{app_label}:{model_name}_changelog' {pk_field}=record.{pk_field} %}}" class="btn btn-default btn-xs" title="Change log">
-        <i class="fa fa-history"></i>
-    </a>
-    {{% if perms.{app_label}.change_{model_name} %}}
+    {{% if "changelog" in buttons %}}
+        <a href="{{% url '{app_label}:{model_name}_changelog' {pk_field}=record.{pk_field} %}}" class="btn btn-default btn-xs" title="Change log">
+            <i class="fa fa-history"></i>
+        </a>
+    {{% endif %}}
+    {{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
         <a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning" title="Edit">
         <a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning" title="Edit">
             <i class="fa fa-pencil"></i>
             <i class="fa fa-pencil"></i>
         </a>
         </a>
     {{% endif %}}
     {{% endif %}}
-    {{% if perms.{app_label}.delete_{model_name} %}}
+    {{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
         <a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger" title="Delete">
         <a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger" title="Delete">
             <i class="fa fa-trash"></i>
             <i class="fa fa-trash"></i>
         </a>
         </a>
     {{% endif %}}
     {{% endif %}}
     """
     """
 
 
-    def __init__(self, model, *args, pk_field='pk', prepend_template=None, **kwargs):
+    def __init__(self, model, *args, pk_field='pk', buttons=None, prepend_template=None, **kwargs):
         if prepend_template:
         if prepend_template:
             prepend_template = prepend_template.replace('{', '{{')
             prepend_template = prepend_template.replace('{', '{{')
             prepend_template = prepend_template.replace('}', '}}')
             prepend_template = prepend_template.replace('}', '}}')
@@ -157,11 +160,16 @@ class ButtonsColumn(tables.TemplateColumn):
         template_code = self.template_code.format(
         template_code = self.template_code.format(
             app_label=model._meta.app_label,
             app_label=model._meta.app_label,
             model_name=model._meta.model_name,
             model_name=model._meta.model_name,
-            pk_field=pk_field
+            pk_field=pk_field,
+            buttons=buttons
         )
         )
 
 
         super().__init__(template_code=template_code, *args, **kwargs)
         super().__init__(template_code=template_code, *args, **kwargs)
 
 
+        self.extra_context.update({
+            'buttons': buttons or self.buttons,
+        })
+
     def header(self):
     def header(self):
         return ''
         return ''
 
 

+ 11 - 0
netbox/utilities/templatetags/helpers.py

@@ -242,3 +242,14 @@ def tag(tag, url_name=None):
         'tag': tag,
         'tag': tag,
         'url_name': url_name,
         'url_name': url_name,
     }
     }
+
+
+@register.inclusion_tag('utilities/templatetags/badge.html')
+def badge(value, show_empty=False):
+    """
+    Display the specified number as a badge.
+    """
+    return {
+        'value': value,
+        'show_empty': show_empty,
+    }

+ 1 - 1
netbox/utilities/utils.py

@@ -217,7 +217,7 @@ def prepare_cloned_fields(instance):
 
 
     # Copy tags
     # Copy tags
     if is_taggable(instance):
     if is_taggable(instance):
-        params['tags'] = ','.join([t.name for t in instance.tags.all()])
+        params['tags'] = ','.join([t.name for t in instance.tags.all().unrestricted()])
 
 
     # Concatenate parameters into a URL query string
     # Concatenate parameters into a URL query string
     param_string = '&'.join(
     param_string = '&'.join(

+ 2 - 2
netbox/virtualization/migrations/0016_replicate_interfaces.py

@@ -51,8 +51,8 @@ def replicate_interfaces(apps, schema_editor):
     # Verify that all interfaces have been replicated
     # Verify that all interfaces have been replicated
     assert replicated_count == original_interfaces.count(), "Replicated interfaces count does not match original count!"
     assert replicated_count == original_interfaces.count(), "Replicated interfaces count does not match original count!"
 
 
-    # Delete original VM interfaces
-    original_interfaces.delete()
+    # Delete all interfaces not assigned to a Device
+    Interface.objects.filter(device__isnull=True).delete()
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 11 - 0
netbox/virtualization/models.py

@@ -9,7 +9,9 @@ from dcim.choices import InterfaceModeChoices
 from dcim.models import BaseInterface, Device
 from dcim.models import BaseInterface, Device
 from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
 from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
 from extras.utils import extras_features
 from extras.utils import extras_features
+from utilities.fields import NaturalOrderingField
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
+from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
 from utilities.utils import serialize_object
 from utilities.utils import serialize_object
@@ -387,6 +389,15 @@ class VMInterface(BaseInterface):
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
         related_name='interfaces'
         related_name='interfaces'
     )
     )
+    name = models.CharField(
+        max_length=64
+    )
+    _name = NaturalOrderingField(
+        target_field='name',
+        naturalize_function=naturalize_interface,
+        max_length=100,
+        blank=True
+    )
     description = models.CharField(
     description = models.CharField(
         max_length=200,
         max_length=200,
         blank=True
         blank=True

+ 3 - 1
netbox/virtualization/tests/test_api.py

@@ -1,4 +1,5 @@
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
@@ -244,7 +245,8 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
             },
             },
         ]
         ]
 
 
-    def test_get_interface_graphs(self):
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_get_vminterface_graphs(self):
         """
         """
         Test retrieval of Graphs assigned to VM interfaces.
         Test retrieval of Graphs assigned to VM interfaces.
         """
         """