Browse Source

Fixes #10247: Allow changing selected device/VM when creating a new component (#10312)

* Initial work on #10247

* Continued work on #10247

* Clean up component creation tests

* Move valdiation of replicated field to form

* Clean up ordering of fields in component creation forms

* Omit fieldset header if none

* Clean up ordering of fields in component template creation forms

* View tests should not move component templates to new device type

* Define replication_fields on VMInterfaceCreateForm

* Clean up expandable field help texts

* Update comments

* Update component bulk update forms & views to support new replication fields

* Fix ModularDeviceComponentForm parent class

* Fix bulk creation of VM interfaces (thanks @kkthxbye-code!)
Jeremy Stretch 3 years ago
parent
commit
c4b7ab067a

+ 13 - 12
netbox/dcim/forms/bulk_create.py

@@ -3,7 +3,7 @@ from django import forms
 from dcim.models import *
 from dcim.models import *
 from extras.forms import CustomFieldsMixin
 from extras.forms import CustomFieldsMixin
 from extras.models import Tag
 from extras.models import Tag
-from utilities.forms import DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
+from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
 from .object_create import ComponentCreateForm
 from .object_create import ComponentCreateForm
 
 
 __all__ = (
 __all__ = (
@@ -24,7 +24,7 @@ __all__ = (
 # Device components
 # Device components
 #
 #
 
 
-class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
+class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
@@ -37,6 +37,7 @@ class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
         queryset=Tag.objects.all(),
         queryset=Tag.objects.all(),
         required=False
         required=False
     )
     )
+    replication_fields = ('name', 'label')
 
 
 
 
 class ConsolePortBulkCreateForm(
 class ConsolePortBulkCreateForm(
@@ -44,7 +45,7 @@ class ConsolePortBulkCreateForm(
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     model = ConsolePort
     model = ConsolePort
-    field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags')
 
 
 
 
 class ConsoleServerPortBulkCreateForm(
 class ConsoleServerPortBulkCreateForm(
@@ -52,7 +53,7 @@ class ConsoleServerPortBulkCreateForm(
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     model = ConsoleServerPort
     model = ConsoleServerPort
-    field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'speed', 'description', 'tags')
 
 
 
 
 class PowerPortBulkCreateForm(
 class PowerPortBulkCreateForm(
@@ -60,7 +61,7 @@ class PowerPortBulkCreateForm(
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     model = PowerPort
     model = PowerPort
-    field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
 
 
 
 
 class PowerOutletBulkCreateForm(
 class PowerOutletBulkCreateForm(
@@ -68,7 +69,7 @@ class PowerOutletBulkCreateForm(
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     model = PowerOutlet
     model = PowerOutlet
-    field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
 
 
 
 
 class InterfaceBulkCreateForm(
 class InterfaceBulkCreateForm(
@@ -79,7 +80,7 @@ class InterfaceBulkCreateForm(
 ):
 ):
     model = Interface
     model = Interface
     field_order = (
     field_order = (
-        'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
+        'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
         'poe_type', 'mark_connected', 'description', 'tags',
         'poe_type', 'mark_connected', 'description', 'tags',
     )
     )
 
 
@@ -96,13 +97,13 @@ class RearPortBulkCreateForm(
     DeviceBulkAddComponentForm
     DeviceBulkAddComponentForm
 ):
 ):
     model = RearPort
     model = RearPort
-    field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags')
 
 
 
 
 class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
 class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
     model = ModuleBay
     model = ModuleBay
-    field_order = ('name_pattern', 'label_pattern', 'position_pattern', 'description', 'tags')
-
+    field_order = ('name', 'label', 'position_pattern', 'description', 'tags')
+    replication_fields = ('name', 'label', 'position')
     position_pattern = ExpandableNameField(
     position_pattern = ExpandableNameField(
         label='Position',
         label='Position',
         required=False,
         required=False,
@@ -112,7 +113,7 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
 
 
 class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
 class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
     model = DeviceBay
     model = DeviceBay
-    field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
+    field_order = ('name', 'label', 'description', 'tags')
 
 
 
 
 class InventoryItemBulkCreateForm(
 class InventoryItemBulkCreateForm(
@@ -121,6 +122,6 @@ class InventoryItemBulkCreateForm(
 ):
 ):
     model = InventoryItem
     model = InventoryItem
     field_order = (
     field_order = (
-        'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
+        'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
         'description', 'tags',
         'description', 'tags',
     )
     )

+ 146 - 101
netbox/dcim/forms/models.py

@@ -986,47 +986,74 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
 # Device component templates
 # Device component templates
 #
 #
 
 
+class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
+    device_type = DynamicModelChoiceField(
+        queryset=DeviceType.objects.all()
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Disable reassignment of DeviceType when editing an existing instance
+        if self.instance.pk:
+            self.fields['device_type'].disabled = True
+
+
+class ModularComponentTemplateForm(ComponentTemplateForm):
+    module_type = DynamicModelChoiceField(
+        queryset=ModuleType.objects.all(),
+        required=False
+    )
+
+
+class ConsolePortTemplateForm(ModularComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
+    )
 
 
-class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = ConsolePortTemplate
         model = ConsolePortTemplate
         fields = [
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'description',
             'device_type', 'module_type', 'name', 'label', 'type', 'description',
         ]
         ]
         widgets = {
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect,
             'type': StaticSelect,
         }
         }
 
 
 
 
-class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
+    )
+
     class Meta:
     class Meta:
         model = ConsoleServerPortTemplate
         model = ConsoleServerPortTemplate
         fields = [
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'description',
             'device_type', 'module_type', 'name', 'label', 'type', 'description',
         ]
         ]
         widgets = {
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect,
             'type': StaticSelect,
         }
         }
 
 
 
 
-class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class PowerPortTemplateForm(ModularComponentTemplateForm):
+    fieldsets = (
+        (None, (
+            'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
+        )),
+    )
+
     class Meta:
     class Meta:
         model = PowerPortTemplate
         model = PowerPortTemplate
         fields = [
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
             'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
         ]
         ]
         widgets = {
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
         }
         }
 
 
 
 
-class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
+class PowerOutletTemplateForm(ModularComponentTemplateForm):
     power_port = DynamicModelChoiceField(
     power_port = DynamicModelChoiceField(
         queryset=PowerPortTemplate.objects.all(),
         queryset=PowerPortTemplate.objects.all(),
         required=False,
         required=False,
@@ -1035,35 +1062,40 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
         }
         }
     )
     )
 
 
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
+    )
+
     class Meta:
     class Meta:
         model = PowerOutletTemplate
         model = PowerOutletTemplate
         fields = [
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
             'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
         ]
         ]
         widgets = {
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
             'feed_leg': StaticSelect(),
             'feed_leg': StaticSelect(),
         }
         }
 
 
 
 
-class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
+class InterfaceTemplateForm(ModularComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description')),
+        ('PoE', ('poe_mode', 'poe_type'))
+    )
+
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
         fields = [
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
             'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
         ]
         ]
         widgets = {
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
             'poe_mode': StaticSelect(),
             'poe_mode': StaticSelect(),
             'poe_type': StaticSelect(),
             'poe_type': StaticSelect(),
         }
         }
 
 
 
 
-class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class FrontPortTemplateForm(ModularComponentTemplateForm):
     rear_port = DynamicModelChoiceField(
     rear_port = DynamicModelChoiceField(
         queryset=RearPortTemplate.objects.all(),
         queryset=RearPortTemplate.objects.all(),
         required=False,
         required=False,
@@ -1073,6 +1105,13 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
         }
         }
     )
     )
 
 
+    fieldsets = (
+        (None, (
+            'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
+            'description',
+        )),
+    )
+
     class Meta:
     class Meta:
         model = FrontPortTemplate
         model = FrontPortTemplate
         fields = [
         fields = [
@@ -1080,48 +1119,50 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
             'description',
             'description',
         ]
         ]
         widgets = {
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
         }
         }
 
 
 
 
-class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class RearPortTemplateForm(ModularComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
+    )
+
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
         fields = [
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
             'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
         ]
         ]
         widgets = {
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
         }
         }
 
 
 
 
-class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
+class ModuleBayTemplateForm(ComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'name', 'label', 'position', 'description')),
+    )
+
     class Meta:
     class Meta:
         model = ModuleBayTemplate
         model = ModuleBayTemplate
         fields = [
         fields = [
             'device_type', 'name', 'label', 'position', 'description',
             'device_type', 'name', 'label', 'position', 'description',
         ]
         ]
-        widgets = {
-            'device_type': forms.HiddenInput(),
-        }
 
 
 
 
-class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
+class DeviceBayTemplateForm(ComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'name', 'label', 'description')),
+    )
+
     class Meta:
     class Meta:
         model = DeviceBayTemplate
         model = DeviceBayTemplate
         fields = [
         fields = [
             'device_type', 'name', 'label', 'description',
             'device_type', 'name', 'label', 'description',
         ]
         ]
-        widgets = {
-            'device_type': forms.HiddenInput(),
-        }
 
 
 
 
-class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
+class InventoryItemTemplateForm(ComponentTemplateForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=InventoryItemTemplate.objects.all(),
         queryset=InventoryItemTemplate.objects.all(),
         required=False,
         required=False,
@@ -1148,22 +1189,39 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
         widget=forms.HiddenInput
         widget=forms.HiddenInput
     )
     )
 
 
+    fieldsets = (
+        (None, (
+            'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
+            'component_type', 'component_id',
+        )),
+    )
+
     class Meta:
     class Meta:
         model = InventoryItemTemplate
         model = InventoryItemTemplate
         fields = [
         fields = [
             'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
             'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
             'component_type', 'component_id',
             'component_type', 'component_id',
         ]
         ]
-        widgets = {
-            'device_type': forms.HiddenInput(),
-        }
 
 
 
 
 #
 #
 # Device components
 # Device components
 #
 #
 
 
-class ConsolePortForm(NetBoxModelForm):
+class DeviceComponentForm(NetBoxModelForm):
+    device = DynamicModelChoiceField(
+        queryset=Device.objects.all()
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Disable reassignment of Device when editing an existing instance
+        if self.instance.pk:
+            self.fields['device'].disabled = True
+
+
+class ModularDeviceComponentForm(DeviceComponentForm):
     module = DynamicModelChoiceField(
     module = DynamicModelChoiceField(
         queryset=Module.objects.all(),
         queryset=Module.objects.all(),
         required=False,
         required=False,
@@ -1172,25 +1230,31 @@ class ConsolePortForm(NetBoxModelForm):
         }
         }
     )
     )
 
 
+
+class ConsolePortForm(ModularDeviceComponentForm):
+    fieldsets = (
+        (None, (
+            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
+        )),
+    )
+
     class Meta:
     class Meta:
         model = ConsolePort
         model = ConsolePort
         fields = [
         fields = [
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
             'speed': StaticSelect(),
             'speed': StaticSelect(),
         }
         }
 
 
 
 
-class ConsoleServerPortForm(NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
+class ConsoleServerPortForm(ModularDeviceComponentForm):
+
+    fieldsets = (
+        (None, (
+            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
+        )),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1199,42 +1263,32 @@ class ConsoleServerPortForm(NetBoxModelForm):
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
             'speed': StaticSelect(),
             'speed': StaticSelect(),
         }
         }
 
 
 
 
-class PowerPortForm(NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
+class PowerPortForm(ModularDeviceComponentForm):
+
+    fieldsets = (
+        (None, (
+            'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
+            'description', 'tags',
+        )),
     )
     )
 
 
     class Meta:
     class Meta:
         model = PowerPort
         model = PowerPort
         fields = [
         fields = [
             'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
             'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
-            'description',
-            'tags',
+            'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
         }
         }
 
 
 
 
-class PowerOutletForm(NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
-    )
+class PowerOutletForm(ModularDeviceComponentForm):
     power_port = DynamicModelChoiceField(
     power_port = DynamicModelChoiceField(
         queryset=PowerPort.objects.all(),
         queryset=PowerPort.objects.all(),
         required=False,
         required=False,
@@ -1243,6 +1297,13 @@ class PowerOutletForm(NetBoxModelForm):
         }
         }
     )
     )
 
 
+    fieldsets = (
+        (None, (
+            'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
+            'tags',
+        )),
+    )
+
     class Meta:
     class Meta:
         model = PowerOutlet
         model = PowerOutlet
         fields = [
         fields = [
@@ -1250,20 +1311,12 @@ class PowerOutletForm(NetBoxModelForm):
             'tags',
             'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
             'feed_leg': StaticSelect(),
             'feed_leg': StaticSelect(),
         }
         }
 
 
 
 
-class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
-    )
+class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         required=False,
         required=False,
@@ -1338,7 +1391,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        ('Interface', ('device', 'module', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')),
+        ('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
         ('Addressing', ('vrf', 'mac_address', 'wwn')),
         ('Addressing', ('vrf', 'mac_address', 'wwn')),
         ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
         ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
@@ -1358,7 +1411,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
             'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
             'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
             'speed': SelectSpeedWidget(),
             'speed': SelectSpeedWidget(),
             'poe_mode': StaticSelect(),
             'poe_mode': StaticSelect(),
@@ -1388,14 +1440,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
             self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
             self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
 
 
 
 
-class FrontPortForm(NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
-    )
+class FrontPortForm(ModularDeviceComponentForm):
     rear_port = DynamicModelChoiceField(
     rear_port = DynamicModelChoiceField(
         queryset=RearPort.objects.all(),
         queryset=RearPort.objects.all(),
         query_params={
         query_params={
@@ -1403,6 +1448,13 @@ class FrontPortForm(NetBoxModelForm):
         }
         }
     )
     )
 
 
+    fieldsets = (
+        (None, (
+            'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
+            'description', 'tags',
+        )),
+    )
+
     class Meta:
     class Meta:
         model = FrontPort
         model = FrontPort
         fields = [
         fields = [
@@ -1410,18 +1462,15 @@ class FrontPortForm(NetBoxModelForm):
             'description', 'tags',
             'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
         }
         }
 
 
 
 
-class RearPortForm(NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
+class RearPortForm(ModularDeviceComponentForm):
+    fieldsets = (
+        (None, (
+            'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
+        )),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1430,33 +1479,32 @@ class RearPortForm(NetBoxModelForm):
             'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
             'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'type': StaticSelect(),
         }
         }
 
 
 
 
-class ModuleBayForm(NetBoxModelForm):
+class ModuleBayForm(DeviceComponentForm):
+    fieldsets = (
+        (None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
+    )
 
 
     class Meta:
     class Meta:
         model = ModuleBay
         model = ModuleBay
         fields = [
         fields = [
             'device', 'name', 'label', 'position', 'description', 'tags',
             'device', 'name', 'label', 'position', 'description', 'tags',
         ]
         ]
-        widgets = {
-            'device': forms.HiddenInput(),
-        }
 
 
 
 
-class DeviceBayForm(NetBoxModelForm):
+class DeviceBayForm(DeviceComponentForm):
+    fieldsets = (
+        (None, ('device', 'name', 'label', 'description', 'tags',)),
+    )
 
 
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
         fields = [
         fields = [
             'device', 'name', 'label', 'description', 'tags',
             'device', 'name', 'label', 'description', 'tags',
         ]
         ]
-        widgets = {
-            'device': forms.HiddenInput(),
-        }
 
 
 
 
 class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
 class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
@@ -1479,10 +1527,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
         ).exclude(pk=device_bay.device.pk)
         ).exclude(pk=device_bay.device.pk)
 
 
 
 
-class InventoryItemForm(NetBoxModelForm):
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all()
-    )
+class InventoryItemForm(DeviceComponentForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=InventoryItem.objects.all(),
         queryset=InventoryItem.objects.all(),
         required=False,
         required=False,

+ 178 - 80
netbox/dcim/forms/object_create.py

@@ -2,46 +2,56 @@ from django import forms
 
 
 from dcim.models import *
 from dcim.models import *
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
-from utilities.forms import (
-    BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
-)
+from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
+from . import models as model_forms
 
 
 __all__ = (
 __all__ = (
-    'ComponentTemplateCreateForm',
-    'DeviceComponentCreateForm',
+    'ComponentCreateForm',
+    'ConsolePortCreateForm',
+    'ConsolePortTemplateCreateForm',
+    'ConsoleServerPortCreateForm',
+    'ConsoleServerPortTemplateCreateForm',
+    'DeviceBayCreateForm',
+    'DeviceBayTemplateCreateForm',
     'FrontPortCreateForm',
     'FrontPortCreateForm',
     'FrontPortTemplateCreateForm',
     'FrontPortTemplateCreateForm',
+    'InterfaceCreateForm',
+    'InterfaceTemplateCreateForm',
     'InventoryItemCreateForm',
     'InventoryItemCreateForm',
-    'ModularComponentTemplateCreateForm',
+    'InventoryItemTemplateCreateForm',
     'ModuleBayCreateForm',
     'ModuleBayCreateForm',
     'ModuleBayTemplateCreateForm',
     'ModuleBayTemplateCreateForm',
+    'PowerOutletCreateForm',
+    'PowerOutletTemplateCreateForm',
+    'PowerPortCreateForm',
+    'PowerPortTemplateCreateForm',
+    'RearPortCreateForm',
+    'RearPortTemplateCreateForm',
     'VirtualChassisCreateForm',
     'VirtualChassisCreateForm',
 )
 )
 
 
 
 
-class ComponentCreateForm(BootstrapMixin, forms.Form):
+class ComponentCreateForm(forms.Form):
     """
     """
-    Subclass this form when facilitating the creation of one or more device component or component templates based on
+    Subclass this form when facilitating the creation of one or more component or component template objects based on
     a name pattern.
     a name pattern.
     """
     """
-    name_pattern = ExpandableNameField(
-        label='Name'
-    )
-    label_pattern = ExpandableNameField(
-        label='Label',
+    name = ExpandableNameField()
+    label = ExpandableNameField(
         required=False,
         required=False,
-        help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
+        help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
     )
     )
 
 
+    # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
+    # ComponentCreateView when creating objects.
+    replication_fields = ('name', 'label')
+
     def clean(self):
     def clean(self):
         super().clean()
         super().clean()
 
 
-        # Validate that all patterned fields generate an equal number of values
-        patterned_fields = [
-            field_name for field_name in self.fields if field_name.endswith('_pattern')
-        ]
-        pattern_count = len(self.cleaned_data['name_pattern'])
-        for field_name in patterned_fields:
+        # Validate that all replication fields generate an equal number of values
+        pattern_count = len(self.cleaned_data[self.replication_fields[0]])
+        for field_name in self.replication_fields:
             value_count = len(self.cleaned_data[field_name])
             value_count = len(self.cleaned_data[field_name])
             if self.cleaned_data[field_name] and value_count != pattern_count:
             if self.cleaned_data[field_name] and value_count != pattern_count:
                 raise forms.ValidationError({
                 raise forms.ValidationError({
@@ -50,56 +60,55 @@ class ComponentCreateForm(BootstrapMixin, forms.Form):
                 }, code='label_pattern_mismatch')
                 }, code='label_pattern_mismatch')
 
 
 
 
-class ComponentTemplateCreateForm(ComponentCreateForm):
-    """
-    Creation form for component templates that can be assigned only to a DeviceType.
-    """
-    device_type = DynamicModelChoiceField(
-        queryset=DeviceType.objects.all(),
-    )
-    field_order = ('device_type', 'name_pattern', 'label_pattern')
+#
+# Device component templates
+#
 
 
+class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
 
 
-class ModularComponentTemplateCreateForm(ComponentCreateForm):
-    """
-    Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
-    """
-    name_pattern = ExpandableNameField(
-        label='Name',
-        help_text="""
-                Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
-                are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>.  {module} is accepted as a substitution for
-                the module bay position.
-                """
-    )
-    device_type = DynamicModelChoiceField(
-        queryset=DeviceType.objects.all(),
-        required=False
-    )
-    module_type = DynamicModelChoiceField(
-        queryset=ModuleType.objects.all(),
-        required=False
-    )
-    field_order = ('device_type', 'module_type', 'name_pattern', 'label_pattern')
+    class Meta(model_forms.ConsolePortTemplateForm.Meta):
+        exclude = ('name', 'label')
 
 
 
 
-class DeviceComponentCreateForm(ComponentCreateForm):
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all()
-    )
-    field_order = ('device', 'name_pattern', 'label_pattern')
+class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
+
+    class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
+        exclude = ('name', 'label')
+
+
+class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
+
+    class Meta(model_forms.PowerPortTemplateForm.Meta):
+        exclude = ('name', 'label')
+
 
 
+class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
 
 
-class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
-    rear_port_set = forms.MultipleChoiceField(
+    class Meta(model_forms.PowerOutletTemplateForm.Meta):
+        exclude = ('name', 'label')
+
+
+class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
+
+    class Meta(model_forms.InterfaceTemplateForm.Meta):
+        exclude = ('name', 'label')
+
+
+class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
+    rear_port = forms.MultipleChoiceField(
         choices=[],
         choices=[],
         label='Rear ports',
         label='Rear ports',
         help_text='Select one rear port assignment for each front port being created.',
         help_text='Select one rear port assignment for each front port being created.',
     )
     )
-    field_order = (
-        'device_type', 'name_pattern', 'label_pattern', 'rear_port_set',
+
+    # Override fieldsets from FrontPortTemplateForm to omit rear_port_position
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description')),
     )
     )
 
 
+    class Meta(model_forms.FrontPortTemplateForm.Meta):
+        exclude = ('name', 'label', 'rear_port', 'rear_port_position')
+
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
@@ -130,12 +139,12 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
                     choices.append(
                     choices.append(
                         ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
                         ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
                     )
                     )
-        self.fields['rear_port_set'].choices = choices
+        self.fields['rear_port'].choices = choices
 
 
     def get_iterative_data(self, iteration):
     def get_iterative_data(self, iteration):
 
 
         # Assign rear port and position from selected set
         # Assign rear port and position from selected set
-        rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
+        rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
 
 
         return {
         return {
             'rear_port': int(rear_port),
             'rear_port': int(rear_port),
@@ -143,16 +152,94 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
         }
         }
 
 
 
 
-class FrontPortCreateForm(DeviceComponentCreateForm):
-    rear_port_set = forms.MultipleChoiceField(
+class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
+
+    class Meta(model_forms.RearPortTemplateForm.Meta):
+        exclude = ('name', 'label')
+
+
+class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
+
+    class Meta(model_forms.DeviceBayTemplateForm.Meta):
+        exclude = ('name', 'label')
+
+
+class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
+    position = ExpandableNameField(
+        label='Position',
+        required=False,
+        help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
+    )
+    replication_fields = ('name', 'label', 'position')
+
+    class Meta(model_forms.ModuleBayTemplateForm.Meta):
+        exclude = ('name', 'label', 'position')
+
+
+class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
+
+    class Meta(model_forms.InventoryItemTemplateForm.Meta):
+        exclude = ('name', 'label')
+
+
+#
+# Device components
+#
+
+class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
+
+    class Meta(model_forms.ConsolePortForm.Meta):
+        exclude = ('name', 'label')
+
+
+class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
+
+    class Meta(model_forms.ConsoleServerPortForm.Meta):
+        exclude = ('name', 'label')
+
+
+class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
+
+    class Meta(model_forms.PowerPortForm.Meta):
+        exclude = ('name', 'label')
+
+
+class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
+
+    class Meta(model_forms.PowerOutletForm.Meta):
+        exclude = ('name', 'label')
+
+
+class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
+
+    class Meta(model_forms.InterfaceForm.Meta):
+        exclude = ('name', 'label')
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        if 'module' in self.fields:
+            self.fields['name'].help_text += ' The string <code>{module}</code> will be replaced with the position ' \
+                                             'of the assigned module, if any'
+
+
+class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
+    rear_port = forms.MultipleChoiceField(
         choices=[],
         choices=[],
         label='Rear ports',
         label='Rear ports',
         help_text='Select one rear port assignment for each front port being created.',
         help_text='Select one rear port assignment for each front port being created.',
     )
     )
-    field_order = (
-        'device', 'name_pattern', 'label_pattern', 'rear_port_set',
+
+    # Override fieldsets from FrontPortForm to omit rear_port_position
+    fieldsets = (
+        (None, (
+            'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
+        )),
     )
     )
 
 
+    class Meta(model_forms.FrontPortForm.Meta):
+        exclude = ('name', 'label', 'rear_port', 'rear_port_position')
+
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
@@ -176,12 +263,12 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
                     choices.append(
                     choices.append(
                         ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
                         ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
                     )
                     )
-        self.fields['rear_port_set'].choices = choices
+        self.fields['rear_port'].choices = choices
 
 
     def get_iterative_data(self, iteration):
     def get_iterative_data(self, iteration):
 
 
         # Assign rear port and position from selected set
         # Assign rear port and position from selected set
-        rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
+        rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
 
 
         return {
         return {
             'rear_port': int(rear_port),
             'rear_port': int(rear_port),
@@ -189,28 +276,39 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
         }
         }
 
 
 
 
-class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
-    position_pattern = ExpandableNameField(
-        label='Position',
-        required=False,
-        help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
-    )
-    field_order = ('device_type', 'name_pattern', 'label_pattern', 'position_pattern')
+class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
+
+    class Meta(model_forms.RearPortForm.Meta):
+        exclude = ('name', 'label')
+
+
+class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
 
 
+    class Meta(model_forms.DeviceBayForm.Meta):
+        exclude = ('name', 'label')
 
 
-class ModuleBayCreateForm(DeviceComponentCreateForm):
-    position_pattern = ExpandableNameField(
+
+class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
+    position = ExpandableNameField(
         label='Position',
         label='Position',
         required=False,
         required=False,
-        help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
+        help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
     )
     )
-    field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
+    replication_fields = ('name', 'label', 'position')
+
+    class Meta(model_forms.ModuleBayForm.Meta):
+        exclude = ('name', 'label', 'position')
+
+
+class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
 
 
+    class Meta(model_forms.InventoryItemForm.Meta):
+        exclude = ('name', 'label')
 
 
-class InventoryItemCreateForm(ComponentCreateForm):
-    # Device is assigned by the model form
-    field_order = ('name_pattern', 'label_pattern')
 
 
+#
+# Virtual chassis
+#
 
 
 class VirtualChassisCreateForm(NetBoxModelForm):
 class VirtualChassisCreateForm(NetBoxModelForm):
     region = DynamicModelChoiceField(
     region = DynamicModelChoiceField(

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

@@ -908,18 +908,20 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
     def clean(self):
     def clean(self):
         super().clean()
         super().clean()
 
 
-        # Validate rear port assignment
-        if self.rear_port.device != self.device:
-            raise ValidationError({
-                "rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
-            })
+        if hasattr(self, 'rear_port'):
 
 
-        # Validate rear port position assignment
-        if self.rear_port_position > self.rear_port.positions:
-            raise ValidationError({
-                "rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
-                                      f"{self.rear_port.name} has only {self.rear_port.positions} positions"
-            })
+            # Validate rear port assignment
+            if self.rear_port.device != self.device:
+                raise ValidationError({
+                    "rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
+                })
+
+            # Validate rear port position assignment
+            if self.rear_port_position > self.rear_port.positions:
+                raise ValidationError({
+                    "rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
+                                          f"{self.rear_port.name} has only {self.rear_port.positions} positions"
+                })
 
 
 
 
 class RearPort(ModularComponentModel, CabledObjectModel):
 class RearPort(ModularComponentModel, CabledObjectModel):

+ 1 - 1
netbox/dcim/tables/template_code.py

@@ -239,7 +239,7 @@ INTERFACE_BUTTONS = """
         <li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
         <li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
       {% endif %}
       {% endif %}
       {% if perms.dcim.add_interface %}
       {% if perms.dcim.add_interface %}
-        <li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name_pattern={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li>
+        <li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li>
       {% endif %}
       {% endif %}
       {% if perms.ipam.add_l2vpntermination %}
       {% if perms.ipam.add_l2vpntermination %}
         <li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
         <li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>

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

@@ -1,6 +1,6 @@
 from django.test import TestCase
 from django.test import TestCase
 
 
-from dcim.choices import DeviceFaceChoices, DeviceStatusChoices
+from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
 from dcim.forms import *
 from dcim.forms import *
 from dcim.models import *
 from dcim.models import *
 from utilities.testing import create_test_device
 from utilities.testing import create_test_device
@@ -129,10 +129,11 @@ class LabelTestCase(TestCase):
         """
         """
         interface_data = {
         interface_data = {
             'device': self.device.pk,
             'device': self.device.pk,
-            'name_pattern': 'eth[0-9]',
-            'label_pattern': 'Interface[0-9]',
+            'name': 'eth[0-9]',
+            'label': 'Interface[0-9]',
+            'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
         }
         }
-        form = DeviceComponentCreateForm(interface_data)
+        form = InterfaceCreateForm(interface_data)
 
 
         self.assertTrue(form.is_valid())
         self.assertTrue(form.is_valid())
 
 
@@ -142,10 +143,11 @@ class LabelTestCase(TestCase):
         """
         """
         bad_interface_data = {
         bad_interface_data = {
             'device': self.device.pk,
             'device': self.device.pk,
-            'name_pattern': 'eth[0-9]',
-            'label_pattern': 'Interface[0-1]',
+            'name': 'eth[0-9]',
+            'label': 'Interface[0-1]',
+            'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
         }
         }
-        form = DeviceComponentCreateForm(bad_interface_data)
+        form = InterfaceCreateForm(bad_interface_data)
 
 
         self.assertFalse(form.is_valid())
         self.assertFalse(form.is_valid())
-        self.assertIn('label_pattern', form.errors)
+        self.assertIn('label', form.errors)

+ 92 - 109
netbox/dcim/tests/test_views.py

@@ -1082,31 +1082,28 @@ front-ports:
 
 
 class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = ConsolePortTemplate
     model = ConsolePortTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetypes = (
-            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
 
 
         ConsolePortTemplate.objects.bulk_create((
         ConsolePortTemplate.objects.bulk_create((
-            ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'),
-            ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'),
-            ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'),
+            ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
+            ConsolePortTemplate(device_type=devicetype, name='Console Port Template 2'),
+            ConsolePortTemplate(device_type=devicetype, name='Console Port Template 3'),
         ))
         ))
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Console Port Template X',
             'name': 'Console Port Template X',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Console Port Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Console Port Template [4-6]',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
         }
         }
 
 
@@ -1117,31 +1114,28 @@ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
 
 
 class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = ConsoleServerPortTemplate
     model = ConsoleServerPortTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetypes = (
-            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
 
 
         ConsoleServerPortTemplate.objects.bulk_create((
         ConsoleServerPortTemplate.objects.bulk_create((
-            ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'),
-            ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'),
-            ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 3'),
+            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
+            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 2'),
+            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 3'),
         ))
         ))
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Console Server Port Template X',
             'name': 'Console Server Port Template X',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Console Server Port Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Console Server Port Template [4-6]',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
         }
         }
 
 
@@ -1152,24 +1146,21 @@ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateVie
 
 
 class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = PowerPortTemplate
     model = PowerPortTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetypes = (
-            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
 
 
         PowerPortTemplate.objects.bulk_create((
         PowerPortTemplate.objects.bulk_create((
-            PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'),
-            PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'),
-            PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'),
+            PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
+            PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
+            PowerPortTemplate(device_type=devicetype, name='Power Port Template 3'),
         ))
         ))
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Power Port Template X',
             'name': 'Power Port Template X',
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'maximum_draw': 100,
             'maximum_draw': 100,
@@ -1177,8 +1168,8 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Power Port Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Power Port Template [4-6]',
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'maximum_draw': 100,
             'maximum_draw': 100,
             'allocated_draw': 50,
             'allocated_draw': 50,
@@ -1193,6 +1184,7 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 
 class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = PowerOutletTemplate
     model = PowerOutletTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1220,7 +1212,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device_type': devicetype.pk,
             'device_type': devicetype.pk,
-            'name_pattern': 'Power Outlet Template [4-6]',
+            'name': 'Power Outlet Template [4-6]',
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
             'power_port': powerports[0].pk,
             'power_port': powerports[0].pk,
             'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
             'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -1234,34 +1226,31 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
 
 
 class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = InterfaceTemplate
     model = InterfaceTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetypes = (
-            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
 
 
         InterfaceTemplate.objects.bulk_create((
         InterfaceTemplate.objects.bulk_create((
-            InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'),
-            InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'),
-            InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'),
+            InterfaceTemplate(device_type=devicetype, name='Interface Template 1'),
+            InterfaceTemplate(device_type=devicetype, name='Interface Template 2'),
+            InterfaceTemplate(device_type=devicetype, name='Interface Template 3'),
         ))
         ))
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Interface Template X',
             'name': 'Interface Template X',
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'mgmt_only': True,
             'mgmt_only': True,
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Interface Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Interface Template [4-6]',
             # Test that a label can be applied to each generated interface templates
             # Test that a label can be applied to each generated interface templates
-            'label_pattern': 'Interface Template Label [3-5]',
+            'label': 'Interface Template Label [3-5]',
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'mgmt_only': True,
             'mgmt_only': True,
         }
         }
@@ -1274,6 +1263,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 
 class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = FrontPortTemplate
     model = FrontPortTemplate
+    validation_excluded_fields = ('name', 'label', 'rear_port')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1306,11 +1296,9 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device_type': devicetype.pk,
             'device_type': devicetype.pk,
-            'name_pattern': 'Front Port [4-6]',
+            'name': 'Front Port [4-6]',
             'type': PortTypeChoices.TYPE_8P8C,
             'type': PortTypeChoices.TYPE_8P8C,
-            'rear_port_set': [
-                '{}:1'.format(rp.pk) for rp in rearports[3:6]
-            ],
+            'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
         }
         }
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
@@ -1320,32 +1308,29 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 
 class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = RearPortTemplate
     model = RearPortTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetypes = (
-            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
 
 
         RearPortTemplate.objects.bulk_create((
         RearPortTemplate.objects.bulk_create((
-            RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'),
-            RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'),
-            RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'),
+            RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
+            RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
+            RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
         ))
         ))
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Rear Port Template X',
             'name': 'Rear Port Template X',
             'type': PortTypeChoices.TYPE_8P8C,
             'type': PortTypeChoices.TYPE_8P8C,
             'positions': 2,
             'positions': 2,
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Rear Port Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Rear Port Template [4-6]',
             'type': PortTypeChoices.TYPE_8P8C,
             'type': PortTypeChoices.TYPE_8P8C,
             'positions': 2,
             'positions': 2,
         }
         }
@@ -1357,30 +1342,27 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
 
 
 class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = ModuleBayTemplate
     model = ModuleBayTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetypes = (
-            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
 
 
         ModuleBayTemplate.objects.bulk_create((
         ModuleBayTemplate.objects.bulk_create((
-            ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 1'),
-            ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 2'),
-            ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 3'),
+            ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'),
+            ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'),
+            ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'),
         ))
         ))
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Module Bay Template X',
             'name': 'Module Bay Template X',
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Module Bay Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Module Bay Template [4-6]',
         }
         }
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
@@ -1390,30 +1372,27 @@ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 
 class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = DeviceBayTemplate
     model = DeviceBayTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetypes = (
-            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
 
 
         DeviceBayTemplate.objects.bulk_create((
         DeviceBayTemplate.objects.bulk_create((
-            DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'),
-            DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'),
-            DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'),
+            DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1'),
+            DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2'),
+            DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3'),
         ))
         ))
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Device Bay Template X',
             'name': 'Device Bay Template X',
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Device Bay Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Device Bay Template [4-6]',
         }
         }
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
@@ -1423,6 +1402,7 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 
 class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = InventoryItemTemplate
     model = InventoryItemTemplate
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1431,30 +1411,25 @@ class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
             Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
             Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
         )
         )
         Manufacturer.objects.bulk_create(manufacturers)
         Manufacturer.objects.bulk_create(manufacturers)
-
-        devicetypes = (
-            DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturers[0], model='Device Type 2', slug='device-type-2'),
-        )
-        DeviceType.objects.bulk_create(devicetypes)
+        devicetype = DeviceType.objects.create(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1')
 
 
         inventory_item_templates = (
         inventory_item_templates = (
-            InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 1', manufacturer=manufacturers[0]),
-            InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 2', manufacturer=manufacturers[0]),
-            InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 3', manufacturer=manufacturers[0]),
+            InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturers[0]),
+            InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 2', manufacturer=manufacturers[0]),
+            InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 3', manufacturer=manufacturers[0]),
         )
         )
         for item in inventory_item_templates:
         for item in inventory_item_templates:
             item.save()
             item.save()
 
 
         cls.form_data = {
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Inventory Item Template X',
             'name': 'Inventory Item Template X',
             'manufacturer': manufacturers[1].pk,
             'manufacturer': manufacturers[1].pk,
         }
         }
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
-            'device_type': devicetypes[1].pk,
-            'name_pattern': 'Inventory Item Template [4-6]',
+            'device_type': devicetype.pk,
+            'name': 'Inventory Item Template [4-6]',
             'manufacturer': manufacturers[1].pk,
             'manufacturer': manufacturers[1].pk,
         }
         }
 
 
@@ -1912,6 +1887,7 @@ class ModuleTestCase(
 
 
 class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = ConsolePort
     model = ConsolePort
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1935,9 +1911,9 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Console Port [4-6]',
+            'name': 'Console Port [4-6]',
             # Test that a label can be applied to each generated console ports
             # Test that a label can be applied to each generated console ports
-            'label_pattern': 'Serial[3-5]',
+            'label': 'Serial[3-5]',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'description': 'A console port',
             'description': 'A console port',
             'tags': sorted([t.pk for t in tags]),
             'tags': sorted([t.pk for t in tags]),
@@ -1970,6 +1946,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = ConsoleServerPort
     model = ConsoleServerPort
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1993,7 +1970,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Console Server Port [4-6]',
+            'name': 'Console Server Port [4-6]',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'description': 'A console server port',
             'description': 'A console server port',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
@@ -2026,6 +2003,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = PowerPort
     model = PowerPort
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2051,7 +2029,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Power Port [4-6]]',
+            'name': 'Power Port [4-6]]',
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'maximum_draw': 100,
             'maximum_draw': 100,
             'allocated_draw': 50,
             'allocated_draw': 50,
@@ -2088,6 +2066,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = PowerOutlet
     model = PowerOutlet
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2119,7 +2098,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Power Outlet [4-6]',
+            'name': 'Power Outlet [4-6]',
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
             'power_port': powerports[1].pk,
             'power_port': powerports[1].pk,
             'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
             'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -2153,6 +2132,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = Interface
     model = Interface
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2217,7 +2197,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Interface [4-6]',
+            'name': 'Interface [4-6]',
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'enabled': False,
             'enabled': False,
             'bridge': interfaces[4].pk,
             'bridge': interfaces[4].pk,
@@ -2277,6 +2257,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = FrontPort
     model = FrontPort
+    validation_excluded_fields = ('name', 'label', 'rear_port')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2312,11 +2293,9 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Front Port [4-6]',
+            'name': 'Front Port [4-6]',
             'type': PortTypeChoices.TYPE_8P8C,
             'type': PortTypeChoices.TYPE_8P8C,
-            'rear_port_set': [
-                '{}:1'.format(rp.pk) for rp in rearports[3:6]
-            ],
+            'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
             'description': 'New description',
             'description': 'New description',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
@@ -2348,6 +2327,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = RearPort
     model = RearPort
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2372,7 +2352,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Rear Port [4-6]',
+            'name': 'Rear Port [4-6]',
             'type': PortTypeChoices.TYPE_8P8C,
             'type': PortTypeChoices.TYPE_8P8C,
             'positions': 3,
             'positions': 3,
             'description': 'A rear port',
             'description': 'A rear port',
@@ -2406,6 +2386,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = ModuleBay
     model = ModuleBay
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2428,7 +2409,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Module Bay [4-6]',
+            'name': 'Module Bay [4-6]',
             'description': 'A module bay',
             'description': 'A module bay',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
@@ -2447,6 +2428,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = DeviceBay
     model = DeviceBay
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2472,7 +2454,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Device Bay [4-6]',
+            'name': 'Device Bay [4-6]',
             'description': 'A device bay',
             'description': 'A device bay',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
@@ -2491,6 +2473,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
 class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = InventoryItem
     model = InventoryItem
+    validation_excluded_fields = ('name', 'label')
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2525,7 +2508,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'device': device.pk,
             'device': device.pk,
-            'name_pattern': 'Inventory Item [4-6]',
+            'name': 'Inventory Item [4-6]',
             'role': roles[1].pk,
             'role': roles[1].pk,
             'manufacturer': manufacturer.pk,
             'manufacturer': manufacturer.pk,
             'parent': None,
             'parent': None,

+ 15 - 82
netbox/dcim/views.py

@@ -1120,9 +1120,8 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
 
 
 class ConsolePortTemplateCreateView(generic.ComponentCreateView):
 class ConsolePortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsolePortTemplate.objects.all()
     queryset = ConsolePortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.ConsolePortTemplateCreateForm
     model_form = forms.ConsolePortTemplateForm
     model_form = forms.ConsolePortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 
 
 class ConsolePortTemplateEditView(generic.ObjectEditView):
 class ConsolePortTemplateEditView(generic.ObjectEditView):
@@ -1155,9 +1154,8 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 
 class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
 class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsoleServerPortTemplate.objects.all()
     queryset = ConsoleServerPortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.ConsoleServerPortTemplateCreateForm
     model_form = forms.ConsoleServerPortTemplateForm
     model_form = forms.ConsoleServerPortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 
 
 class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
 class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
@@ -1190,9 +1188,8 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 
 class PowerPortTemplateCreateView(generic.ComponentCreateView):
 class PowerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerPortTemplate.objects.all()
     queryset = PowerPortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.PowerPortTemplateCreateForm
     model_form = forms.PowerPortTemplateForm
     model_form = forms.PowerPortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 
 
 class PowerPortTemplateEditView(generic.ObjectEditView):
 class PowerPortTemplateEditView(generic.ObjectEditView):
@@ -1225,9 +1222,8 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 
 class PowerOutletTemplateCreateView(generic.ComponentCreateView):
 class PowerOutletTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerOutletTemplate.objects.all()
     queryset = PowerOutletTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.PowerOutletTemplateCreateForm
     model_form = forms.PowerOutletTemplateForm
     model_form = forms.PowerOutletTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 
 
 class PowerOutletTemplateEditView(generic.ObjectEditView):
 class PowerOutletTemplateEditView(generic.ObjectEditView):
@@ -1260,9 +1256,8 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
 
 
 class InterfaceTemplateCreateView(generic.ComponentCreateView):
 class InterfaceTemplateCreateView(generic.ComponentCreateView):
     queryset = InterfaceTemplate.objects.all()
     queryset = InterfaceTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.InterfaceTemplateCreateForm
     model_form = forms.InterfaceTemplateForm
     model_form = forms.InterfaceTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 
 
 class InterfaceTemplateEditView(generic.ObjectEditView):
 class InterfaceTemplateEditView(generic.ObjectEditView):
@@ -1297,15 +1292,6 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
     queryset = FrontPortTemplate.objects.all()
     queryset = FrontPortTemplate.objects.all()
     form = forms.FrontPortTemplateCreateForm
     form = forms.FrontPortTemplateCreateForm
     model_form = forms.FrontPortTemplateForm
     model_form = forms.FrontPortTemplateForm
-    template_name = 'dcim/frontporttemplate_create.html'
-
-    def initialize_forms(self, request):
-        form, model_form = super().initialize_forms(request)
-
-        model_form.fields.pop('rear_port')
-        model_form.fields.pop('rear_port_position')
-
-        return form, model_form
 
 
 
 
 class FrontPortTemplateEditView(generic.ObjectEditView):
 class FrontPortTemplateEditView(generic.ObjectEditView):
@@ -1338,9 +1324,8 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 
 class RearPortTemplateCreateView(generic.ComponentCreateView):
 class RearPortTemplateCreateView(generic.ComponentCreateView):
     queryset = RearPortTemplate.objects.all()
     queryset = RearPortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.RearPortTemplateCreateForm
     model_form = forms.RearPortTemplateForm
     model_form = forms.RearPortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 
 
 class RearPortTemplateEditView(generic.ObjectEditView):
 class RearPortTemplateEditView(generic.ObjectEditView):
@@ -1375,8 +1360,6 @@ class ModuleBayTemplateCreateView(generic.ComponentCreateView):
     queryset = ModuleBayTemplate.objects.all()
     queryset = ModuleBayTemplate.objects.all()
     form = forms.ModuleBayTemplateCreateForm
     form = forms.ModuleBayTemplateCreateForm
     model_form = forms.ModuleBayTemplateForm
     model_form = forms.ModuleBayTemplateForm
-    template_name = 'dcim/modulebaytemplate_create.html'
-    patterned_fields = ('name', 'label', 'position')
 
 
 
 
 class ModuleBayTemplateEditView(generic.ObjectEditView):
 class ModuleBayTemplateEditView(generic.ObjectEditView):
@@ -1409,9 +1392,8 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
 
 
 class DeviceBayTemplateCreateView(generic.ComponentCreateView):
 class DeviceBayTemplateCreateView(generic.ComponentCreateView):
     queryset = DeviceBayTemplate.objects.all()
     queryset = DeviceBayTemplate.objects.all()
-    form = forms.ComponentTemplateCreateForm
+    form = forms.DeviceBayTemplateCreateForm
     model_form = forms.DeviceBayTemplateForm
     model_form = forms.DeviceBayTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 
 
 class DeviceBayTemplateEditView(generic.ObjectEditView):
 class DeviceBayTemplateEditView(generic.ObjectEditView):
@@ -1444,9 +1426,8 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
 
 
 class InventoryItemTemplateCreateView(generic.ComponentCreateView):
 class InventoryItemTemplateCreateView(generic.ComponentCreateView):
     queryset = InventoryItemTemplate.objects.all()
     queryset = InventoryItemTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.InventoryItemTemplateCreateForm
     model_form = forms.InventoryItemTemplateForm
     model_form = forms.InventoryItemTemplateForm
-    template_name = 'dcim/inventoryitemtemplate_create.html'
 
 
     def alter_object(self, instance, request):
     def alter_object(self, instance, request):
         # Set component (if any)
         # Set component (if any)
@@ -1874,14 +1855,13 @@ class ConsolePortView(generic.ObjectView):
 
 
 class ConsolePortCreateView(generic.ComponentCreateView):
 class ConsolePortCreateView(generic.ComponentCreateView):
     queryset = ConsolePort.objects.all()
     queryset = ConsolePort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.ConsolePortCreateForm
     model_form = forms.ConsolePortForm
     model_form = forms.ConsolePortForm
 
 
 
 
 class ConsolePortEditView(generic.ObjectEditView):
 class ConsolePortEditView(generic.ObjectEditView):
     queryset = ConsolePort.objects.all()
     queryset = ConsolePort.objects.all()
     form = forms.ConsolePortForm
     form = forms.ConsolePortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class ConsolePortDeleteView(generic.ObjectDeleteView):
 class ConsolePortDeleteView(generic.ObjectDeleteView):
@@ -1933,14 +1913,13 @@ class ConsoleServerPortView(generic.ObjectView):
 
 
 class ConsoleServerPortCreateView(generic.ComponentCreateView):
 class ConsoleServerPortCreateView(generic.ComponentCreateView):
     queryset = ConsoleServerPort.objects.all()
     queryset = ConsoleServerPort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.ConsoleServerPortCreateForm
     model_form = forms.ConsoleServerPortForm
     model_form = forms.ConsoleServerPortForm
 
 
 
 
 class ConsoleServerPortEditView(generic.ObjectEditView):
 class ConsoleServerPortEditView(generic.ObjectEditView):
     queryset = ConsoleServerPort.objects.all()
     queryset = ConsoleServerPort.objects.all()
     form = forms.ConsoleServerPortForm
     form = forms.ConsoleServerPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class ConsoleServerPortDeleteView(generic.ObjectDeleteView):
 class ConsoleServerPortDeleteView(generic.ObjectDeleteView):
@@ -1992,14 +1971,13 @@ class PowerPortView(generic.ObjectView):
 
 
 class PowerPortCreateView(generic.ComponentCreateView):
 class PowerPortCreateView(generic.ComponentCreateView):
     queryset = PowerPort.objects.all()
     queryset = PowerPort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.PowerPortCreateForm
     model_form = forms.PowerPortForm
     model_form = forms.PowerPortForm
 
 
 
 
 class PowerPortEditView(generic.ObjectEditView):
 class PowerPortEditView(generic.ObjectEditView):
     queryset = PowerPort.objects.all()
     queryset = PowerPort.objects.all()
     form = forms.PowerPortForm
     form = forms.PowerPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class PowerPortDeleteView(generic.ObjectDeleteView):
 class PowerPortDeleteView(generic.ObjectDeleteView):
@@ -2051,14 +2029,13 @@ class PowerOutletView(generic.ObjectView):
 
 
 class PowerOutletCreateView(generic.ComponentCreateView):
 class PowerOutletCreateView(generic.ComponentCreateView):
     queryset = PowerOutlet.objects.all()
     queryset = PowerOutlet.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.PowerOutletCreateForm
     model_form = forms.PowerOutletForm
     model_form = forms.PowerOutletForm
 
 
 
 
 class PowerOutletEditView(generic.ObjectEditView):
 class PowerOutletEditView(generic.ObjectEditView):
     queryset = PowerOutlet.objects.all()
     queryset = PowerOutlet.objects.all()
     form = forms.PowerOutletForm
     form = forms.PowerOutletForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class PowerOutletDeleteView(generic.ObjectDeleteView):
 class PowerOutletDeleteView(generic.ObjectDeleteView):
@@ -2154,42 +2131,13 @@ class InterfaceView(generic.ObjectView):
 
 
 class InterfaceCreateView(generic.ComponentCreateView):
 class InterfaceCreateView(generic.ComponentCreateView):
     queryset = Interface.objects.all()
     queryset = Interface.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.InterfaceCreateForm
     model_form = forms.InterfaceForm
     model_form = forms.InterfaceForm
-    # template_name = 'dcim/interface_create.html'
-
-    # TODO: Figure out what to do with this
-    # def post(self, request):
-    #     """
-    #     Override inherited post() method to handle request to assign newly created
-    #     interface objects (first object) to an IP Address object.
-    #     """
-    #     form = self.form(request.POST, initial=request.GET)
-    #     new_objs = self.validate_form(request, form)
-    #
-    #     if form.is_valid() and not form.errors:
-    #         if '_addanother' in request.POST:
-    #             return redirect(request.get_full_path())
-    #         elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and \
-    #                 request.user.has_perm('ipam.add_ipaddress'):
-    #             first_obj = new_objs[0].pk
-    #             return redirect(
-    #                 f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}'
-    #             )
-    #         else:
-    #             return redirect(self.get_return_url(request))
-    #
-    #     return render(request, self.template_name, {
-    #         'obj_type': self.queryset.model._meta.verbose_name,
-    #         'form': form,
-    #         'return_url': self.get_return_url(request),
-    #     })
 
 
 
 
 class InterfaceEditView(generic.ObjectEditView):
 class InterfaceEditView(generic.ObjectEditView):
     queryset = Interface.objects.all()
     queryset = Interface.objects.all()
     form = forms.InterfaceForm
     form = forms.InterfaceForm
-    template_name = 'dcim/interface_edit.html'
 
 
 
 
 class InterfaceDeleteView(generic.ObjectDeleteView):
 class InterfaceDeleteView(generic.ObjectDeleteView):
@@ -2244,19 +2192,10 @@ class FrontPortCreateView(generic.ComponentCreateView):
     form = forms.FrontPortCreateForm
     form = forms.FrontPortCreateForm
     model_form = forms.FrontPortForm
     model_form = forms.FrontPortForm
 
 
-    def initialize_forms(self, request):
-        form, model_form = super().initialize_forms(request)
-
-        model_form.fields.pop('rear_port')
-        model_form.fields.pop('rear_port_position')
-
-        return form, model_form
-
 
 
 class FrontPortEditView(generic.ObjectEditView):
 class FrontPortEditView(generic.ObjectEditView):
     queryset = FrontPort.objects.all()
     queryset = FrontPort.objects.all()
     form = forms.FrontPortForm
     form = forms.FrontPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class FrontPortDeleteView(generic.ObjectDeleteView):
 class FrontPortDeleteView(generic.ObjectDeleteView):
@@ -2308,14 +2247,13 @@ class RearPortView(generic.ObjectView):
 
 
 class RearPortCreateView(generic.ComponentCreateView):
 class RearPortCreateView(generic.ComponentCreateView):
     queryset = RearPort.objects.all()
     queryset = RearPort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.RearPortCreateForm
     model_form = forms.RearPortForm
     model_form = forms.RearPortForm
 
 
 
 
 class RearPortEditView(generic.ObjectEditView):
 class RearPortEditView(generic.ObjectEditView):
     queryset = RearPort.objects.all()
     queryset = RearPort.objects.all()
     form = forms.RearPortForm
     form = forms.RearPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class RearPortDeleteView(generic.ObjectDeleteView):
 class RearPortDeleteView(generic.ObjectDeleteView):
@@ -2369,13 +2307,11 @@ class ModuleBayCreateView(generic.ComponentCreateView):
     queryset = ModuleBay.objects.all()
     queryset = ModuleBay.objects.all()
     form = forms.ModuleBayCreateForm
     form = forms.ModuleBayCreateForm
     model_form = forms.ModuleBayForm
     model_form = forms.ModuleBayForm
-    patterned_fields = ('name', 'label', 'position')
 
 
 
 
 class ModuleBayEditView(generic.ObjectEditView):
 class ModuleBayEditView(generic.ObjectEditView):
     queryset = ModuleBay.objects.all()
     queryset = ModuleBay.objects.all()
     form = forms.ModuleBayForm
     form = forms.ModuleBayForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class ModuleBayDeleteView(generic.ObjectDeleteView):
 class ModuleBayDeleteView(generic.ObjectDeleteView):
@@ -2423,14 +2359,13 @@ class DeviceBayView(generic.ObjectView):
 
 
 class DeviceBayCreateView(generic.ComponentCreateView):
 class DeviceBayCreateView(generic.ComponentCreateView):
     queryset = DeviceBay.objects.all()
     queryset = DeviceBay.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.DeviceBayCreateForm
     model_form = forms.DeviceBayForm
     model_form = forms.DeviceBayForm
 
 
 
 
 class DeviceBayEditView(generic.ObjectEditView):
 class DeviceBayEditView(generic.ObjectEditView):
     queryset = DeviceBay.objects.all()
     queryset = DeviceBay.objects.all()
     form = forms.DeviceBayForm
     form = forms.DeviceBayForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 
 
 class DeviceBayDeleteView(generic.ObjectDeleteView):
 class DeviceBayDeleteView(generic.ObjectDeleteView):
@@ -2552,7 +2487,6 @@ class InventoryItemCreateView(generic.ComponentCreateView):
     queryset = InventoryItem.objects.all()
     queryset = InventoryItem.objects.all()
     form = forms.InventoryItemCreateForm
     form = forms.InventoryItemCreateForm
     model_form = forms.InventoryItemForm
     model_form = forms.InventoryItemForm
-    template_name = 'dcim/inventoryitem_create.html'
 
 
     def alter_object(self, instance, request):
     def alter_object(self, instance, request):
         # Set component (if any)
         # Set component (if any)
@@ -2736,7 +2670,6 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
     filterset = filtersets.DeviceFilterSet
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
-    patterned_fields = ('name', 'label', 'position')
 
 
 
 
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):

+ 9 - 8
netbox/netbox/views/generic/bulk_views.py

@@ -774,7 +774,6 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
     model_form = None
     model_form = None
     filterset = None
     filterset = None
     table = None
     table = None
-    patterned_fields = ('name', 'label')
 
 
     def get_required_permission(self):
     def get_required_permission(self):
         return f'dcim.add_{self.queryset.model._meta.model_name}'
         return f'dcim.add_{self.queryset.model._meta.model_name}'
@@ -804,23 +803,25 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
 
 
                 new_components = []
                 new_components = []
                 data = deepcopy(form.cleaned_data)
                 data = deepcopy(form.cleaned_data)
+                replication_data = {
+                    field: data.pop(field) for field in form.replication_fields
+                }
 
 
                 try:
                 try:
                     with transaction.atomic():
                     with transaction.atomic():
 
 
                         for obj in data['pk']:
                         for obj in data['pk']:
 
 
-                            pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
+                            pattern_count = len(replication_data[form.replication_fields[0]])
                             for i in range(pattern_count):
                             for i in range(pattern_count):
                                 component_data = {
                                 component_data = {
                                     self.parent_field: obj.pk
                                     self.parent_field: obj.pk
                                 }
                                 }
-
-                                for field_name in self.patterned_fields:
-                                    if data.get(f'{field_name}_pattern'):
-                                        component_data[field_name] = data[f'{field_name}_pattern'][i]
-
                                 component_data.update(data)
                                 component_data.update(data)
+                                for field, values in replication_data.items():
+                                    if values:
+                                        component_data[field] = values[i]
+
                                 component_form = self.model_form(component_data)
                                 component_form = self.model_form(component_data)
                                 if component_form.is_valid():
                                 if component_form.is_valid():
                                     instance = component_form.save()
                                     instance = component_form.save()
@@ -829,7 +830,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
                                 else:
                                 else:
                                     for field, errors in component_form.errors.as_data().items():
                                     for field, errors in component_form.errors.as_data().items():
                                         for e in errors:
                                         for e in errors:
-                                            form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e)))
+                                            form.add_error(field, '{}: {}'.format(obj, ', '.join(e)))
 
 
                         # Enforce object-level permissions
                         # Enforce object-level permissions
                         if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components):
                         if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components):

+ 12 - 20
netbox/netbox/views/generic/object_views.py

@@ -538,10 +538,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
     """
     """
     Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
     Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
     """
     """
-    template_name = 'dcim/component_create.html'
+    template_name = 'generic/object_edit.html'
     form = None
     form = None
     model_form = None
     model_form = None
-    patterned_fields = ('name', 'label')
 
 
     def get_required_permission(self):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'add')
         return get_permission_for_model(self.queryset.model, 'add')
@@ -549,44 +548,38 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
     def alter_object(self, instance, request):
     def alter_object(self, instance, request):
         return instance
         return instance
 
 
-    def initialize_forms(self, request):
+    def initialize_form(self, request):
         data = request.POST if request.method == 'POST' else None
         data = request.POST if request.method == 'POST' else None
         initial_data = normalize_querydict(request.GET)
         initial_data = normalize_querydict(request.GET)
 
 
-        form = self.form(data=data, initial=request.GET)
-        model_form = self.model_form(data=data, initial=initial_data)
-
-        # These fields will be set from the pattern values
-        for field_name in self.patterned_fields:
-            model_form.fields[field_name].widget = HiddenInput()
+        form = self.form(data=data, initial=initial_data)
 
 
-        return form, model_form
+        return form
 
 
     def get(self, request):
     def get(self, request):
-        form, model_form = self.initialize_forms(request)
+        form = self.initialize_form(request)
         instance = self.alter_object(self.queryset.model(), request)
         instance = self.alter_object(self.queryset.model(), request)
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'object': instance,
             'object': instance,
-            'replication_form': form,
-            'form': model_form,
+            'form': form,
             'return_url': self.get_return_url(request),
             'return_url': self.get_return_url(request),
         })
         })
 
 
     def post(self, request):
     def post(self, request):
         logger = logging.getLogger('netbox.views.ComponentCreateView')
         logger = logging.getLogger('netbox.views.ComponentCreateView')
-        form, model_form = self.initialize_forms(request)
+        form = self.initialize_form(request)
         instance = self.alter_object(self.queryset.model(), request)
         instance = self.alter_object(self.queryset.model(), request)
 
 
         if form.is_valid():
         if form.is_valid():
             new_components = []
             new_components = []
             data = deepcopy(request.POST)
             data = deepcopy(request.POST)
-            pattern_count = len(form.cleaned_data[f'{self.patterned_fields[0]}_pattern'])
+            pattern_count = len(form.cleaned_data[self.form.replication_fields[0]])
 
 
             for i in range(pattern_count):
             for i in range(pattern_count):
-                for field_name in self.patterned_fields:
-                    if form.cleaned_data.get(f'{field_name}_pattern'):
-                        data[field_name] = form.cleaned_data[f'{field_name}_pattern'][i]
+                for field_name in self.form.replication_fields:
+                    if form.cleaned_data.get(field_name):
+                        data[field_name] = form.cleaned_data[field_name][i]
 
 
                 if hasattr(form, 'get_iterative_data'):
                 if hasattr(form, 'get_iterative_data'):
                     data.update(form.get_iterative_data(i))
                     data.update(form.get_iterative_data(i))
@@ -626,7 +619,6 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'object': instance,
             'object': instance,
-            'replication_form': form,
-            'form': model_form,
+            'form': form,
             'return_url': self.get_return_url(request),
             'return_url': self.get_return_url(request),
         })
         })

+ 0 - 38
netbox/templates/dcim/component_template_create.html

@@ -1,38 +0,0 @@
-{% extends 'generic/object_edit.html' %}
-{% load form_helpers %}
-
-{% block form %}
-  {% if form.module_type %}
-    <div class="row mb-2">
-      <div class="offset-sm-3">
-        <ul class="nav nav-pills" role="tablist">
-          <li role="presentation" class="nav-item">
-            <button role="tab" type="button" id="devicetype_tab" data-bs-toggle="tab" aria-controls="devicetype" data-bs-target="#devicetype" class="nav-link {% if not form.initial.module_type %}active{% endif %}">
-              Device Type
-            </button>
-          </li>
-          <li role="presentation" class="nav-item">
-            <button role="tab" type="button" id="moduletype_tab" data-bs-toggle="tab" aria-controls="moduletype" data-bs-target="#moduletype" class="nav-link {% if form.initial.module_type %}active{% endif %}">
-              Module Type
-            </button>
-          </li>
-        </ul>
-      </div>
-    </div>
-    <div class="tab-content p-0 border-0">
-      <div class="tab-pane {% if not form.initial.module_type %}active{% endif %}" id="devicetype" role="tabpanel">
-        {% render_field replication_form.device_type %}
-      </div>
-      <div class="tab-pane {% if form.initial.module_type %}active{% endif %}" id="moduletype" role="tabpanel">
-        {% render_field replication_form.module_type %}
-      </div>
-    </div>
-  {% else %}
-    {% render_field replication_form.device_type %}
-  {% endif %}
-  {% block replication_fields %}
-    {% render_field replication_form.name_pattern %}
-    {% render_field replication_form.label_pattern %}
-  {% endblock replication_fields %}
-  {{ block.super }}
-{% endblock form %}

+ 0 - 16
netbox/templates/dcim/device_component_edit.html

@@ -1,16 +0,0 @@
-{% extends 'generic/object_edit.html' %}
-{% load form_helpers %}
-
-{% block form %}
-  <div class="field-group mb-5">
-    {% if form.instance.device %}
-      <div class="row mb-3">
-        <label class="col-sm-3 col-form-label text-lg-end">Device</label>
-        <div class="col">
-          <input class="form-control" value="{{ form.instance.device }}" disabled />
-        </div>
-      </div>
-    {% endif %}
-    {% render_form form %}
-  </div>
-{% endblock form %}

+ 0 - 7
netbox/templates/dcim/frontporttemplate_create.html

@@ -1,7 +0,0 @@
-{% extends 'dcim/component_template_create.html' %}
-{% load form_helpers %}
-
-{% block replication_fields %}
-  {{ block.super }}
-  {% render_field replication_form.rear_port_set %}
-{% endblock replication_fields %}

+ 0 - 17
netbox/templates/dcim/inventoryitem_create.html

@@ -1,17 +0,0 @@
-{% extends 'dcim/component_create.html' %}
-{% load helpers %}
-{% load form_helpers %}
-
-{% block replication_fields %}
-  {{ block.super }}
-  {% if object.component %}
-    <div class="row mb-3">
-        <label class="col-sm-3 col-form-label text-lg-end">
-          {{ object.component|meta:"verbose_name"|bettertitle }}
-        </label>
-        <div class="col">
-            <input class="form-control" value="{{ object.component }}" disabled />
-        </div>
-    </div>
-  {% endif %}
-{% endblock replication_fields %}

+ 0 - 17
netbox/templates/dcim/inventoryitemtemplate_create.html

@@ -1,17 +0,0 @@
-{% extends 'dcim/component_template_create.html' %}
-{% load helpers %}
-{% load form_helpers %}
-
-{% block replication_fields %}
-  {{ block.super }}
-  {% if object.component %}
-    <div class="row mb-3">
-        <label class="col-sm-3 col-form-label text-lg-end">
-          {{ object.component|meta:"verbose_name"|bettertitle }}
-        </label>
-        <div class="col">
-            <input class="form-control" value="{{ object.component }}" disabled />
-        </div>
-    </div>
-  {% endif %}
-{% endblock replication_fields %}

+ 0 - 7
netbox/templates/dcim/modulebaytemplate_create.html

@@ -1,7 +0,0 @@
-{% extends 'dcim/component_template_create.html' %}
-{% load form_helpers %}
-
-{% block replication_fields %}
-  {{ block.super }}
-  {% render_field replication_form.position_pattern %}
-{% endblock replication_fields %}

+ 5 - 3
netbox/templates/generic/object_edit.html

@@ -59,9 +59,11 @@ Context:
             {# Render grouped fields according to Form #}
             {# Render grouped fields according to Form #}
             {% for group, fields in form.fieldsets %}
             {% for group, fields in form.fieldsets %}
               <div class="field-group mb-5">
               <div class="field-group mb-5">
-                <div class="row mb-2">
-                  <h5 class="offset-sm-3">{{ group }}</h5>
-                </div>
+                {% if group %}
+                  <div class="row mb-2">
+                    <h5 class="offset-sm-3">{{ group }}</h5>
+                  </div>
+                {% endif %}
                 {% for name in fields %}
                 {% for name in fields %}
                   {% with field=form|getfield:name %}
                   {% with field=form|getfield:name %}
                     {% if not field.field.widget.is_hidden %}
                     {% if not field.field.widget.is_hidden %}

+ 0 - 69
netbox/templates/virtualization/vminterface_edit.html

@@ -1,69 +0,0 @@
-{% extends 'generic/object_edit.html' %}
-{% load form_helpers %}
-
-{% block form %}
-    {# Render hidden fields #}
-    {% for field in form.hidden_fields %}
-      {{ field }}
-    {% endfor %}
-
-    <div class="field-group my-5">
-      <div class="row mb-2">
-        <h5 class="offset-sm-3">Interface</h5>
-      </div>
-      {% if form.instance.virtual_machine %}
-        <div class="row mb-3">
-          <label class="col-sm-3 col-form-label text-lg-end required" for="id_device">Virtual Machine</label>
-          <div class="col">
-            <input class="form-control" value="{{ form.instance.virtual_machine }}" disabled />
-          </div>
-        </div>
-      {% endif %}
-      {% render_field form.name %}
-      {% render_field form.description %}
-      {% render_field form.tags %}
-    </div>
-
-    <div class="field-group my-5">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Addressing</h5>
-        </div>
-      {% render_field form.vrf %}
-      {% render_field form.mac_address %}
-    </div>
-
-    <div class="field-group my-5">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Operation</h5>
-        </div>
-      {% render_field form.mtu %}
-      {% render_field form.enabled %}
-    </div>
-
-    <div class="field-group my-5">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Related Interfaces</h5>
-        </div>
-        {% render_field form.parent %}
-        {% render_field form.bridge %}
-    </div>
-
-    <div class="field-group my-5">
-      <div class="row mb-2">
-        <h5 class="offset-sm-3">802.1Q Switching</h5>
-      </div>
-      {% render_field form.mode %}
-      {% render_field form.vlan_group %}
-      {% render_field form.untagged_vlan %}
-      {% render_field form.tagged_vlans %}
-    </div>
-
-    {% if form.custom_fields %}
-      <div class="field-group my-5">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Custom Fields</h5>
-        </div>
-        {% render_custom_fields form %}
-      </div>
-    {% endif %}
-{% endblock %}

+ 1 - 1
netbox/utilities/forms/fields/expandable.py

@@ -22,7 +22,7 @@ class ExpandableNameField(forms.CharField):
         if not self.help_text:
         if not self.help_text:
             self.help_text = """
             self.help_text = """
                 Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
                 Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
-                are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>
+                are not supported (example: <code>[ge,xe]-0/0/[0-9]</code>).
                 """
                 """
 
 
     def to_python(self, value):
     def to_python(self, value):

+ 3 - 2
netbox/utilities/testing/views.py

@@ -466,6 +466,7 @@ class ViewTestCases:
         """
         """
         bulk_create_count = 3
         bulk_create_count = 3
         bulk_create_data = {}
         bulk_create_data = {}
+        validation_excluded_fields = []
 
 
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         def test_create_multiple_objects_without_permission(self):
         def test_create_multiple_objects_without_permission(self):
@@ -500,7 +501,7 @@ class ViewTestCases:
             self.assertHttpStatus(response, 302)
             self.assertHttpStatus(response, 302)
             self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
             self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
             for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]:
             for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]:
-                self.assertInstanceEqual(instance, self.bulk_create_data)
+                self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields)
 
 
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         def test_create_multiple_objects_with_constrained_permission(self):
         def test_create_multiple_objects_with_constrained_permission(self):
@@ -532,7 +533,7 @@ class ViewTestCases:
             self.assertHttpStatus(response, 302)
             self.assertHttpStatus(response, 302)
             self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
             self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
             for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]:
             for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]:
-                self.assertInstanceEqual(instance, self.bulk_create_data)
+                self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields)
 
 
     class BulkImportObjectsViewTestCase(ModelViewTestCase):
     class BulkImportObjectsViewTestCase(ModelViewTestCase):
         """
         """

+ 2 - 2
netbox/virtualization/forms/bulk_create.py

@@ -13,7 +13,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
         queryset=VirtualMachine.objects.all(),
         queryset=VirtualMachine.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    name_pattern = ExpandableNameField(
+    name = ExpandableNameField(
         label='Name'
         label='Name'
     )
     )
 
 
@@ -27,4 +27,4 @@ class VMInterfaceBulkCreateForm(
     form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
     form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
     VirtualMachineBulkAddComponentForm
     VirtualMachineBulkAddComponentForm
 ):
 ):
-    pass
+    replication_fields = ('name',)

+ 10 - 2
netbox/virtualization/forms/models.py

@@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError
 from dcim.forms.common import InterfaceCommonForm
 from dcim.forms.common import InterfaceCommonForm
 from dcim.forms.models import INTERFACE_MODE_HELP_TEXT
 from dcim.forms.models import INTERFACE_MODE_HELP_TEXT
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
-from extras.models import Tag
 from ipam.models import IPAddress, VLAN, VLANGroup, VRF
 from ipam.models import IPAddress, VLAN, VLANGroup, VRF
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
@@ -278,6 +277,9 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
 
 
 
 
 class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
 class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
+    virtual_machine = DynamicModelChoiceField(
+        queryset=VirtualMachine.objects.all()
+    )
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         queryset=VMInterface.objects.all(),
         required=False,
         required=False,
@@ -338,7 +340,6 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
             'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
             'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'virtual_machine': forms.HiddenInput(),
             'mode': StaticSelect()
             'mode': StaticSelect()
         }
         }
         labels = {
         labels = {
@@ -347,3 +348,10 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
         help_texts = {
         help_texts = {
             'mode': INTERFACE_MODE_HELP_TEXT,
             'mode': INTERFACE_MODE_HELP_TEXT,
         }
         }
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Disable reassignment of VirtualMachine when editing an existing instance
+        if self.instance.pk:
+            self.fields['virtual_machine'].disabled = True

+ 8 - 11
netbox/virtualization/forms/object_create.py

@@ -1,17 +1,14 @@
-from django import forms
-
-from utilities.forms import BootstrapMixin, DynamicModelChoiceField, ExpandableNameField
-from .models import VirtualMachine
+from utilities.forms import ExpandableNameField
+from .models import VMInterfaceForm
 
 
 __all__ = (
 __all__ = (
     'VMInterfaceCreateForm',
     'VMInterfaceCreateForm',
 )
 )
 
 
 
 
-class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
-    virtual_machine = DynamicModelChoiceField(
-        queryset=VirtualMachine.objects.all()
-    )
-    name_pattern = ExpandableNameField(
-        label='Name'
-    )
+class VMInterfaceCreateForm(VMInterfaceForm):
+    name = ExpandableNameField()
+    replication_fields = ('name',)
+
+    class Meta(VMInterfaceForm.Meta):
+        exclude = ('name',)

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

@@ -251,6 +251,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
 
 class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = VMInterface
     model = VMInterface
+    validation_excluded_fields = ('name',)
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -290,10 +291,10 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         tags = create_tags('Alpha', 'Bravo', 'Charlie')
         tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
 
         cls.form_data = {
         cls.form_data = {
-            'virtual_machine': virtualmachines[1].pk,
+            'virtual_machine': virtualmachines[0].pk,
             'name': 'Interface X',
             'name': 'Interface X',
             'enabled': False,
             'enabled': False,
-            'bridge': interfaces[3].pk,
+            'bridge': interfaces[1].pk,
             'mac_address': EUI('01-02-03-04-05-06'),
             'mac_address': EUI('01-02-03-04-05-06'),
             'mtu': 65000,
             'mtu': 65000,
             'description': 'New description',
             'description': 'New description',
@@ -306,7 +307,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         cls.bulk_create_data = {
         cls.bulk_create_data = {
             'virtual_machine': virtualmachines[1].pk,
             'virtual_machine': virtualmachines[1].pk,
-            'name_pattern': 'Interface [4-6]',
+            'name': 'Interface [4-6]',
             'enabled': False,
             'enabled': False,
             'bridge': interfaces[3].pk,
             'bridge': interfaces[3].pk,
             'mac_address': EUI('01-02-03-04-05-06'),
             'mac_address': EUI('01-02-03-04-05-06'),

+ 0 - 2
netbox/virtualization/views.py

@@ -451,13 +451,11 @@ class VMInterfaceCreateView(generic.ComponentCreateView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     form = forms.VMInterfaceCreateForm
     form = forms.VMInterfaceCreateForm
     model_form = forms.VMInterfaceForm
     model_form = forms.VMInterfaceForm
-    patterned_fields = ('name',)
 
 
 
 
 class VMInterfaceEditView(generic.ObjectEditView):
 class VMInterfaceEditView(generic.ObjectEditView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     form = forms.VMInterfaceForm
     form = forms.VMInterfaceForm
-    template_name = 'virtualization/vminterface_edit.html'
 
 
 
 
 class VMInterfaceDeleteView(generic.ObjectDeleteView):
 class VMInterfaceDeleteView(generic.ObjectDeleteView):