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

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 лет назад
Родитель
Сommit
c4b7ab067a

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

@@ -3,7 +3,7 @@ from django import forms
 from dcim.models import *
 from extras.forms import CustomFieldsMixin
 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
 
 __all__ = (
@@ -24,7 +24,7 @@ __all__ = (
 # Device components
 #
 
-class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
+class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm):
     pk = forms.ModelMultipleChoiceField(
         queryset=Device.objects.all(),
         widget=forms.MultipleHiddenInput()
@@ -37,6 +37,7 @@ class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
         queryset=Tag.objects.all(),
         required=False
     )
+    replication_fields = ('name', 'label')
 
 
 class ConsolePortBulkCreateForm(
@@ -44,7 +45,7 @@ class ConsolePortBulkCreateForm(
     DeviceBulkAddComponentForm
 ):
     model = ConsolePort
-    field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags')
 
 
 class ConsoleServerPortBulkCreateForm(
@@ -52,7 +53,7 @@ class ConsoleServerPortBulkCreateForm(
     DeviceBulkAddComponentForm
 ):
     model = ConsoleServerPort
-    field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'speed', 'description', 'tags')
 
 
 class PowerPortBulkCreateForm(
@@ -60,7 +61,7 @@ class PowerPortBulkCreateForm(
     DeviceBulkAddComponentForm
 ):
     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(
@@ -68,7 +69,7 @@ class PowerOutletBulkCreateForm(
     DeviceBulkAddComponentForm
 ):
     model = PowerOutlet
-    field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags')
+    field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
 
 
 class InterfaceBulkCreateForm(
@@ -79,7 +80,7 @@ class InterfaceBulkCreateForm(
 ):
     model = Interface
     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',
     )
 
@@ -96,13 +97,13 @@ class RearPortBulkCreateForm(
     DeviceBulkAddComponentForm
 ):
     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):
     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(
         label='Position',
         required=False,
@@ -112,7 +113,7 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
 
 class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
     model = DeviceBay
-    field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
+    field_order = ('name', 'label', 'description', 'tags')
 
 
 class InventoryItemBulkCreateForm(
@@ -121,6 +122,6 @@ class InventoryItemBulkCreateForm(
 ):
     model = InventoryItem
     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',
     )

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

@@ -986,47 +986,74 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
 # 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:
         model = ConsolePortTemplate
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'description',
         ]
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect,
         }
 
 
-class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
+    )
+
     class Meta:
         model = ConsoleServerPortTemplate
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'description',
         ]
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             '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:
         model = PowerPortTemplate
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
         ]
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
         }
 
 
-class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
+class PowerOutletTemplateForm(ModularComponentTemplateForm):
     power_port = DynamicModelChoiceField(
         queryset=PowerPortTemplate.objects.all(),
         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:
         model = PowerOutletTemplate
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
         ]
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': 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:
         model = InterfaceTemplate
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
         ]
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
             'poe_mode': StaticSelect(),
             'poe_type': StaticSelect(),
         }
 
 
-class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class FrontPortTemplateForm(ModularComponentTemplateForm):
     rear_port = DynamicModelChoiceField(
         queryset=RearPortTemplate.objects.all(),
         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:
         model = FrontPortTemplate
         fields = [
@@ -1080,48 +1119,50 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
             'description',
         ]
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
         }
 
 
-class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class RearPortTemplateForm(ModularComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
+    )
+
     class Meta:
         model = RearPortTemplate
         fields = [
             'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
         ]
         widgets = {
-            'device_type': forms.HiddenInput(),
-            'module_type': forms.HiddenInput(),
             'type': StaticSelect(),
         }
 
 
-class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
+class ModuleBayTemplateForm(ComponentTemplateForm):
+    fieldsets = (
+        (None, ('device_type', 'name', 'label', 'position', 'description')),
+    )
+
     class Meta:
         model = ModuleBayTemplate
         fields = [
             '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:
         model = DeviceBayTemplate
         fields = [
             'device_type', 'name', 'label', 'description',
         ]
-        widgets = {
-            'device_type': forms.HiddenInput(),
-        }
 
 
-class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
+class InventoryItemTemplateForm(ComponentTemplateForm):
     parent = DynamicModelChoiceField(
         queryset=InventoryItemTemplate.objects.all(),
         required=False,
@@ -1148,22 +1189,39 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
         widget=forms.HiddenInput
     )
 
+    fieldsets = (
+        (None, (
+            'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
+            'component_type', 'component_id',
+        )),
+    )
+
     class Meta:
         model = InventoryItemTemplate
         fields = [
             'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
             'component_type', 'component_id',
         ]
-        widgets = {
-            'device_type': forms.HiddenInput(),
-        }
 
 
 #
 # 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(
         queryset=Module.objects.all(),
         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:
         model = ConsolePort
         fields = [
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
         ]
         widgets = {
-            'device': forms.HiddenInput(),
             'type': 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:
@@ -1199,42 +1263,32 @@ class ConsoleServerPortForm(NetBoxModelForm):
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
         ]
         widgets = {
-            'device': forms.HiddenInput(),
             'type': 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:
         model = PowerPort
         fields = [
             'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
-            'description',
-            'tags',
+            'description', 'tags',
         ]
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
         }
 
 
-class PowerOutletForm(NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
-    )
+class PowerOutletForm(ModularDeviceComponentForm):
     power_port = DynamicModelChoiceField(
         queryset=PowerPort.objects.all(),
         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:
         model = PowerOutlet
         fields = [
@@ -1250,20 +1311,12 @@ class PowerOutletForm(NetBoxModelForm):
             'tags',
         ]
         widgets = {
-            'device': forms.HiddenInput(),
             'type': 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(
         queryset=Interface.objects.all(),
         required=False,
@@ -1338,7 +1391,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     )
 
     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')),
         ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
@@ -1358,7 +1411,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
             'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
         ]
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
             'speed': SelectSpeedWidget(),
             '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)
 
 
-class FrontPortForm(NetBoxModelForm):
-    module = DynamicModelChoiceField(
-        queryset=Module.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
-    )
+class FrontPortForm(ModularDeviceComponentForm):
     rear_port = DynamicModelChoiceField(
         queryset=RearPort.objects.all(),
         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:
         model = FrontPort
         fields = [
@@ -1410,18 +1462,15 @@ class FrontPortForm(NetBoxModelForm):
             'description', 'tags',
         ]
         widgets = {
-            'device': forms.HiddenInput(),
             '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:
@@ -1430,33 +1479,32 @@ class RearPortForm(NetBoxModelForm):
             'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
         ]
         widgets = {
-            'device': forms.HiddenInput(),
             'type': StaticSelect(),
         }
 
 
-class ModuleBayForm(NetBoxModelForm):
+class ModuleBayForm(DeviceComponentForm):
+    fieldsets = (
+        (None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
+    )
 
     class Meta:
         model = ModuleBay
         fields = [
             '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:
         model = DeviceBay
         fields = [
             'device', 'name', 'label', 'description', 'tags',
         ]
-        widgets = {
-            'device': forms.HiddenInput(),
-        }
 
 
 class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
@@ -1479,10 +1527,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
         ).exclude(pk=device_bay.device.pk)
 
 
-class InventoryItemForm(NetBoxModelForm):
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all()
-    )
+class InventoryItemForm(DeviceComponentForm):
     parent = DynamicModelChoiceField(
         queryset=InventoryItem.objects.all(),
         required=False,

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

@@ -2,46 +2,56 @@ from django import forms
 
 from dcim.models import *
 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__ = (
-    'ComponentTemplateCreateForm',
-    'DeviceComponentCreateForm',
+    'ComponentCreateForm',
+    'ConsolePortCreateForm',
+    'ConsolePortTemplateCreateForm',
+    'ConsoleServerPortCreateForm',
+    'ConsoleServerPortTemplateCreateForm',
+    'DeviceBayCreateForm',
+    'DeviceBayTemplateCreateForm',
     'FrontPortCreateForm',
     'FrontPortTemplateCreateForm',
+    'InterfaceCreateForm',
+    'InterfaceTemplateCreateForm',
     'InventoryItemCreateForm',
-    'ModularComponentTemplateCreateForm',
+    'InventoryItemTemplateCreateForm',
     'ModuleBayCreateForm',
     'ModuleBayTemplateCreateForm',
+    'PowerOutletCreateForm',
+    'PowerOutletTemplateCreateForm',
+    'PowerPortCreateForm',
+    'PowerPortTemplateCreateForm',
+    'RearPortCreateForm',
+    'RearPortTemplateCreateForm',
     '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.
     """
-    name_pattern = ExpandableNameField(
-        label='Name'
-    )
-    label_pattern = ExpandableNameField(
-        label='Label',
+    name = ExpandableNameField()
+    label = ExpandableNameField(
         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):
         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])
             if self.cleaned_data[field_name] and value_count != pattern_count:
                 raise forms.ValidationError({
@@ -50,56 +60,55 @@ class ComponentCreateForm(BootstrapMixin, forms.Form):
                 }, 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=[],
         label='Rear ports',
         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):
         super().__init__(*args, **kwargs)
 
@@ -130,12 +139,12 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
                     choices.append(
                         ('{}:{}'.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):
 
         # 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 {
             '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=[],
         label='Rear ports',
         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):
         super().__init__(*args, **kwargs)
 
@@ -176,12 +263,12 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
                     choices.append(
                         ('{}:{}'.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):
 
         # 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 {
             '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',
         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):
     region = DynamicModelChoiceField(

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

@@ -908,18 +908,20 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
     def clean(self):
         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):

+ 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>
       {% endif %}
       {% 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 %}
       {% 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>

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

@@ -1,6 +1,6 @@
 from django.test import TestCase
 
-from dcim.choices import DeviceFaceChoices, DeviceStatusChoices
+from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
 from dcim.forms import *
 from dcim.models import *
 from utilities.testing import create_test_device
@@ -129,10 +129,11 @@ class LabelTestCase(TestCase):
         """
         interface_data = {
             '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())
 
@@ -142,10 +143,11 @@ class LabelTestCase(TestCase):
         """
         bad_interface_data = {
             '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.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):
     model = ConsolePortTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
         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(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 = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Console Port Template X',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
         }
 
         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,
         }
 
@@ -1117,31 +1114,28 @@ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
 
 class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = ConsoleServerPortTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
         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(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 = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Console Server Port Template X',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
         }
 
         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,
         }
 
@@ -1152,24 +1146,21 @@ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateVie
 
 class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = PowerPortTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
         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(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 = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Power Port Template X',
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'maximum_draw': 100,
@@ -1177,8 +1168,8 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
         }
 
         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,
             'maximum_draw': 100,
             'allocated_draw': 50,
@@ -1193,6 +1184,7 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = PowerOutletTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -1220,7 +1212,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
 
         cls.bulk_create_data = {
             'device_type': devicetype.pk,
-            'name_pattern': 'Power Outlet Template [4-6]',
+            'name': 'Power Outlet Template [4-6]',
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
             'power_port': powerports[0].pk,
             'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -1234,34 +1226,31 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
 
 class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = InterfaceTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
         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(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 = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Interface Template X',
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'mgmt_only': True,
         }
 
         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
-            'label_pattern': 'Interface Template Label [3-5]',
+            'label': 'Interface Template Label [3-5]',
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'mgmt_only': True,
         }
@@ -1274,6 +1263,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = FrontPortTemplate
+    validation_excluded_fields = ('name', 'label', 'rear_port')
 
     @classmethod
     def setUpTestData(cls):
@@ -1306,11 +1296,9 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
         cls.bulk_create_data = {
             'device_type': devicetype.pk,
-            'name_pattern': 'Front Port [4-6]',
+            'name': 'Front Port [4-6]',
             '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 = {
@@ -1320,32 +1308,29 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = RearPortTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
         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(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 = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Rear Port Template X',
             'type': PortTypeChoices.TYPE_8P8C,
             'positions': 2,
         }
 
         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,
             'positions': 2,
         }
@@ -1357,30 +1342,27 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
 
 class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = ModuleBayTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
         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(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 = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Module Bay Template X',
         }
 
         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 = {
@@ -1390,30 +1372,27 @@ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = DeviceBayTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
         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(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 = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Device Bay Template X',
         }
 
         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 = {
@@ -1423,6 +1402,7 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
 
 class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = InventoryItemTemplate
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -1431,30 +1411,25 @@ class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
             Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
         )
         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 = (
-            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:
             item.save()
 
         cls.form_data = {
-            'device_type': devicetypes[1].pk,
+            'device_type': devicetype.pk,
             'name': 'Inventory Item Template X',
             'manufacturer': manufacturers[1].pk,
         }
 
         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,
         }
 
@@ -1912,6 +1887,7 @@ class ModuleTestCase(
 
 class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = ConsolePort
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -1935,9 +1911,9 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             '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
-            'label_pattern': 'Serial[3-5]',
+            'label': 'Serial[3-5]',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'description': 'A console port',
             'tags': sorted([t.pk for t in tags]),
@@ -1970,6 +1946,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = ConsoleServerPort
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -1993,7 +1970,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Console Server Port [4-6]',
+            'name': 'Console Server Port [4-6]',
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'description': 'A console server port',
             'tags': [t.pk for t in tags],
@@ -2026,6 +2003,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = PowerPort
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -2051,7 +2029,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Power Port [4-6]]',
+            'name': 'Power Port [4-6]]',
             'type': PowerPortTypeChoices.TYPE_IEC_C14,
             'maximum_draw': 100,
             'allocated_draw': 50,
@@ -2088,6 +2066,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = PowerOutlet
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -2119,7 +2098,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Power Outlet [4-6]',
+            'name': 'Power Outlet [4-6]',
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
             'power_port': powerports[1].pk,
             'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@@ -2153,6 +2132,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = Interface
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -2217,7 +2197,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Interface [4-6]',
+            'name': 'Interface [4-6]',
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'enabled': False,
             'bridge': interfaces[4].pk,
@@ -2277,6 +2257,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = FrontPort
+    validation_excluded_fields = ('name', 'label', 'rear_port')
 
     @classmethod
     def setUpTestData(cls):
@@ -2312,11 +2293,9 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Front Port [4-6]',
+            'name': 'Front Port [4-6]',
             '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',
             'tags': [t.pk for t in tags],
         }
@@ -2348,6 +2327,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = RearPort
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -2372,7 +2352,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Rear Port [4-6]',
+            'name': 'Rear Port [4-6]',
             'type': PortTypeChoices.TYPE_8P8C,
             'positions': 3,
             'description': 'A rear port',
@@ -2406,6 +2386,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = ModuleBay
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -2428,7 +2409,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Module Bay [4-6]',
+            'name': 'Module Bay [4-6]',
             'description': 'A module bay',
             'tags': [t.pk for t in tags],
         }
@@ -2447,6 +2428,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = DeviceBay
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -2472,7 +2454,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Device Bay [4-6]',
+            'name': 'Device Bay [4-6]',
             'description': 'A device bay',
             'tags': [t.pk for t in tags],
         }
@@ -2491,6 +2473,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = InventoryItem
+    validation_excluded_fields = ('name', 'label')
 
     @classmethod
     def setUpTestData(cls):
@@ -2525,7 +2508,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'device': device.pk,
-            'name_pattern': 'Inventory Item [4-6]',
+            'name': 'Inventory Item [4-6]',
             'role': roles[1].pk,
             'manufacturer': manufacturer.pk,
             'parent': None,

+ 15 - 82
netbox/dcim/views.py

@@ -1120,9 +1120,8 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
 
 class ConsolePortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsolePortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.ConsolePortTemplateCreateForm
     model_form = forms.ConsolePortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 class ConsolePortTemplateEditView(generic.ObjectEditView):
@@ -1155,9 +1154,8 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsoleServerPortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.ConsoleServerPortTemplateCreateForm
     model_form = forms.ConsoleServerPortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
@@ -1190,9 +1188,8 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class PowerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerPortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.PowerPortTemplateCreateForm
     model_form = forms.PowerPortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 class PowerPortTemplateEditView(generic.ObjectEditView):
@@ -1225,9 +1222,8 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class PowerOutletTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerOutletTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.PowerOutletTemplateCreateForm
     model_form = forms.PowerOutletTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 class PowerOutletTemplateEditView(generic.ObjectEditView):
@@ -1260,9 +1256,8 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class InterfaceTemplateCreateView(generic.ComponentCreateView):
     queryset = InterfaceTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.InterfaceTemplateCreateForm
     model_form = forms.InterfaceTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 class InterfaceTemplateEditView(generic.ObjectEditView):
@@ -1297,15 +1292,6 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
     queryset = FrontPortTemplate.objects.all()
     form = forms.FrontPortTemplateCreateForm
     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):
@@ -1338,9 +1324,8 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class RearPortTemplateCreateView(generic.ComponentCreateView):
     queryset = RearPortTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.RearPortTemplateCreateForm
     model_form = forms.RearPortTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 class RearPortTemplateEditView(generic.ObjectEditView):
@@ -1375,8 +1360,6 @@ class ModuleBayTemplateCreateView(generic.ComponentCreateView):
     queryset = ModuleBayTemplate.objects.all()
     form = forms.ModuleBayTemplateCreateForm
     model_form = forms.ModuleBayTemplateForm
-    template_name = 'dcim/modulebaytemplate_create.html'
-    patterned_fields = ('name', 'label', 'position')
 
 
 class ModuleBayTemplateEditView(generic.ObjectEditView):
@@ -1409,9 +1392,8 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class DeviceBayTemplateCreateView(generic.ComponentCreateView):
     queryset = DeviceBayTemplate.objects.all()
-    form = forms.ComponentTemplateCreateForm
+    form = forms.DeviceBayTemplateCreateForm
     model_form = forms.DeviceBayTemplateForm
-    template_name = 'dcim/component_template_create.html'
 
 
 class DeviceBayTemplateEditView(generic.ObjectEditView):
@@ -1444,9 +1426,8 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class InventoryItemTemplateCreateView(generic.ComponentCreateView):
     queryset = InventoryItemTemplate.objects.all()
-    form = forms.ModularComponentTemplateCreateForm
+    form = forms.InventoryItemTemplateCreateForm
     model_form = forms.InventoryItemTemplateForm
-    template_name = 'dcim/inventoryitemtemplate_create.html'
 
     def alter_object(self, instance, request):
         # Set component (if any)
@@ -1874,14 +1855,13 @@ class ConsolePortView(generic.ObjectView):
 
 class ConsolePortCreateView(generic.ComponentCreateView):
     queryset = ConsolePort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.ConsolePortCreateForm
     model_form = forms.ConsolePortForm
 
 
 class ConsolePortEditView(generic.ObjectEditView):
     queryset = ConsolePort.objects.all()
     form = forms.ConsolePortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class ConsolePortDeleteView(generic.ObjectDeleteView):
@@ -1933,14 +1913,13 @@ class ConsoleServerPortView(generic.ObjectView):
 
 class ConsoleServerPortCreateView(generic.ComponentCreateView):
     queryset = ConsoleServerPort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.ConsoleServerPortCreateForm
     model_form = forms.ConsoleServerPortForm
 
 
 class ConsoleServerPortEditView(generic.ObjectEditView):
     queryset = ConsoleServerPort.objects.all()
     form = forms.ConsoleServerPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class ConsoleServerPortDeleteView(generic.ObjectDeleteView):
@@ -1992,14 +1971,13 @@ class PowerPortView(generic.ObjectView):
 
 class PowerPortCreateView(generic.ComponentCreateView):
     queryset = PowerPort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.PowerPortCreateForm
     model_form = forms.PowerPortForm
 
 
 class PowerPortEditView(generic.ObjectEditView):
     queryset = PowerPort.objects.all()
     form = forms.PowerPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class PowerPortDeleteView(generic.ObjectDeleteView):
@@ -2051,14 +2029,13 @@ class PowerOutletView(generic.ObjectView):
 
 class PowerOutletCreateView(generic.ComponentCreateView):
     queryset = PowerOutlet.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.PowerOutletCreateForm
     model_form = forms.PowerOutletForm
 
 
 class PowerOutletEditView(generic.ObjectEditView):
     queryset = PowerOutlet.objects.all()
     form = forms.PowerOutletForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class PowerOutletDeleteView(generic.ObjectDeleteView):
@@ -2154,42 +2131,13 @@ class InterfaceView(generic.ObjectView):
 
 class InterfaceCreateView(generic.ComponentCreateView):
     queryset = Interface.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.InterfaceCreateForm
     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):
     queryset = Interface.objects.all()
     form = forms.InterfaceForm
-    template_name = 'dcim/interface_edit.html'
 
 
 class InterfaceDeleteView(generic.ObjectDeleteView):
@@ -2244,19 +2192,10 @@ class FrontPortCreateView(generic.ComponentCreateView):
     form = forms.FrontPortCreateForm
     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):
     queryset = FrontPort.objects.all()
     form = forms.FrontPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class FrontPortDeleteView(generic.ObjectDeleteView):
@@ -2308,14 +2247,13 @@ class RearPortView(generic.ObjectView):
 
 class RearPortCreateView(generic.ComponentCreateView):
     queryset = RearPort.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.RearPortCreateForm
     model_form = forms.RearPortForm
 
 
 class RearPortEditView(generic.ObjectEditView):
     queryset = RearPort.objects.all()
     form = forms.RearPortForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class RearPortDeleteView(generic.ObjectDeleteView):
@@ -2369,13 +2307,11 @@ class ModuleBayCreateView(generic.ComponentCreateView):
     queryset = ModuleBay.objects.all()
     form = forms.ModuleBayCreateForm
     model_form = forms.ModuleBayForm
-    patterned_fields = ('name', 'label', 'position')
 
 
 class ModuleBayEditView(generic.ObjectEditView):
     queryset = ModuleBay.objects.all()
     form = forms.ModuleBayForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class ModuleBayDeleteView(generic.ObjectDeleteView):
@@ -2423,14 +2359,13 @@ class DeviceBayView(generic.ObjectView):
 
 class DeviceBayCreateView(generic.ComponentCreateView):
     queryset = DeviceBay.objects.all()
-    form = forms.DeviceComponentCreateForm
+    form = forms.DeviceBayCreateForm
     model_form = forms.DeviceBayForm
 
 
 class DeviceBayEditView(generic.ObjectEditView):
     queryset = DeviceBay.objects.all()
     form = forms.DeviceBayForm
-    template_name = 'dcim/device_component_edit.html'
 
 
 class DeviceBayDeleteView(generic.ObjectDeleteView):
@@ -2552,7 +2487,6 @@ class InventoryItemCreateView(generic.ComponentCreateView):
     queryset = InventoryItem.objects.all()
     form = forms.InventoryItemCreateForm
     model_form = forms.InventoryItemForm
-    template_name = 'dcim/inventoryitem_create.html'
 
     def alter_object(self, instance, request):
         # Set component (if any)
@@ -2736,7 +2670,6 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
-    patterned_fields = ('name', 'label', 'position')
 
 
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):

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

@@ -774,7 +774,6 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
     model_form = None
     filterset = None
     table = None
-    patterned_fields = ('name', 'label')
 
     def get_required_permission(self):
         return f'dcim.add_{self.queryset.model._meta.model_name}'
@@ -804,23 +803,25 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
 
                 new_components = []
                 data = deepcopy(form.cleaned_data)
+                replication_data = {
+                    field: data.pop(field) for field in form.replication_fields
+                }
 
                 try:
                     with transaction.atomic():
 
                         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):
                                 component_data = {
                                     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)
+                                for field, values in replication_data.items():
+                                    if values:
+                                        component_data[field] = values[i]
+
                                 component_form = self.model_form(component_data)
                                 if component_form.is_valid():
                                     instance = component_form.save()
@@ -829,7 +830,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
                                 else:
                                     for field, errors in component_form.errors.as_data().items():
                                         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
                         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.
     """
-    template_name = 'dcim/component_create.html'
+    template_name = 'generic/object_edit.html'
     form = None
     model_form = None
-    patterned_fields = ('name', 'label')
 
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'add')
@@ -549,44 +548,38 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
     def alter_object(self, instance, request):
         return instance
 
-    def initialize_forms(self, request):
+    def initialize_form(self, request):
         data = request.POST if request.method == 'POST' else None
         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):
-        form, model_form = self.initialize_forms(request)
+        form = self.initialize_form(request)
         instance = self.alter_object(self.queryset.model(), request)
 
         return render(request, self.template_name, {
             'object': instance,
-            'replication_form': form,
-            'form': model_form,
+            'form': form,
             'return_url': self.get_return_url(request),
         })
 
     def post(self, request):
         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)
 
         if form.is_valid():
             new_components = []
             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 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'):
                     data.update(form.get_iterative_data(i))
@@ -626,7 +619,6 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
 
         return render(request, self.template_name, {
             'object': instance,
-            'replication_form': form,
-            'form': model_form,
+            'form': form,
             '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 #}
             {% for group, fields in form.fieldsets %}
               <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 %}
                   {% with field=form|getfield:name %}
                     {% 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:
             self.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>
+                are not supported (example: <code>[ge,xe]-0/0/[0-9]</code>).
                 """
 
     def to_python(self, value):

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

@@ -466,6 +466,7 @@ class ViewTestCases:
         """
         bulk_create_count = 3
         bulk_create_data = {}
+        validation_excluded_fields = []
 
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         def test_create_multiple_objects_without_permission(self):
@@ -500,7 +501,7 @@ class ViewTestCases:
             self.assertHttpStatus(response, 302)
             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]:
-                self.assertInstanceEqual(instance, self.bulk_create_data)
+                self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields)
 
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         def test_create_multiple_objects_with_constrained_permission(self):
@@ -532,7 +533,7 @@ class ViewTestCases:
             self.assertHttpStatus(response, 302)
             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]:
-                self.assertInstanceEqual(instance, self.bulk_create_data)
+                self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields)
 
     class BulkImportObjectsViewTestCase(ModelViewTestCase):
         """

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

@@ -13,7 +13,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
         queryset=VirtualMachine.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    name_pattern = ExpandableNameField(
+    name = ExpandableNameField(
         label='Name'
     )
 
@@ -27,4 +27,4 @@ class VMInterfaceBulkCreateForm(
     form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
     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.models import INTERFACE_MODE_HELP_TEXT
 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 netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
@@ -278,6 +277,9 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
 
 
 class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
+    virtual_machine = DynamicModelChoiceField(
+        queryset=VirtualMachine.objects.all()
+    )
     parent = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         required=False,
@@ -338,7 +340,6 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
             'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
         ]
         widgets = {
-            'virtual_machine': forms.HiddenInput(),
             'mode': StaticSelect()
         }
         labels = {
@@ -347,3 +348,10 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
         help_texts = {
             '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__ = (
     '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):
     model = VMInterface
+    validation_excluded_fields = ('name',)
 
     @classmethod
     def setUpTestData(cls):
@@ -290,10 +291,10 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
-            'virtual_machine': virtualmachines[1].pk,
+            'virtual_machine': virtualmachines[0].pk,
             'name': 'Interface X',
             'enabled': False,
-            'bridge': interfaces[3].pk,
+            'bridge': interfaces[1].pk,
             'mac_address': EUI('01-02-03-04-05-06'),
             'mtu': 65000,
             'description': 'New description',
@@ -306,7 +307,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
         cls.bulk_create_data = {
             'virtual_machine': virtualmachines[1].pk,
-            'name_pattern': 'Interface [4-6]',
+            'name': 'Interface [4-6]',
             'enabled': False,
             'bridge': interfaces[3].pk,
             '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()
     form = forms.VMInterfaceCreateForm
     model_form = forms.VMInterfaceForm
-    patterned_fields = ('name',)
 
 
 class VMInterfaceEditView(generic.ObjectEditView):
     queryset = VMInterface.objects.all()
     form = forms.VMInterfaceForm
-    template_name = 'virtualization/vminterface_edit.html'
 
 
 class VMInterfaceDeleteView(generic.ObjectDeleteView):