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

Refactor ComponentCreateView to use separate forms for names/labels and model creation

jeremystretch 4 лет назад
Родитель
Сommit
a237c01b4b

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

@@ -4,7 +4,7 @@ from dcim.models import *
 from extras.forms import CustomFieldsMixin
 from extras.models import Tag
 from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
-from .object_create import ComponentForm
+from .object_create import ComponentCreateForm
 
 __all__ = (
     'ConsolePortBulkCreateForm',
@@ -24,7 +24,7 @@ __all__ = (
 # Device components
 #
 
-class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentForm):
+class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
     pk = forms.ModelMultipleChoiceField(
         queryset=Device.objects.all(),
         widget=forms.MultipleHiddenInput()

+ 4 - 584
netbox/dcim/forms/object_create.py

@@ -1,44 +1,19 @@
 from django import forms
-from django.contrib.contenttypes.models import ContentType
 
-from dcim.choices import *
-from dcim.constants import *
 from dcim.models import *
-from extras.forms import CustomFieldModelForm, CustomFieldsMixin
+from extras.forms import CustomFieldModelForm
 from extras.models import Tag
-from ipam.models import VLAN
 from utilities.forms import (
-    add_blank_choice, BootstrapMixin, ColorField, ContentTypeChoiceField, DynamicModelChoiceField,
-    DynamicModelMultipleChoiceField, ExpandableNameField, StaticSelect,
+    BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
 )
-from wireless.choices import *
-from .common import InterfaceCommonForm
 
 __all__ = (
-    'ConsolePortCreateForm',
-    'ConsolePortTemplateCreateForm',
-    'ConsoleServerPortCreateForm',
-    'ConsoleServerPortTemplateCreateForm',
-    'DeviceBayCreateForm',
-    'DeviceBayTemplateCreateForm',
-    'FrontPortCreateForm',
-    'FrontPortTemplateCreateForm',
-    'InterfaceCreateForm',
-    'InterfaceTemplateCreateForm',
-    'InventoryItemCreateForm',
-    'ModuleBayCreateForm',
-    'ModuleBayTemplateCreateForm',
-    'PowerOutletCreateForm',
-    'PowerOutletTemplateCreateForm',
-    'PowerPortCreateForm',
-    'PowerPortTemplateCreateForm',
-    'RearPortCreateForm',
-    'RearPortTemplateCreateForm',
+    'ComponentCreateForm',
     'VirtualChassisCreateForm',
 )
 
 
-class ComponentForm(BootstrapMixin, forms.Form):
+class ComponentCreateForm(BootstrapMixin, forms.Form):
     """
     Subclass this form when facilitating the creation of one or more device component or component templates based on
     a name pattern.
@@ -139,558 +114,3 @@ class VirtualChassisCreateForm(CustomFieldModelForm):
                 member.save()
 
         return instance
-
-
-#
-# Component templates
-#
-
-class ComponentTemplateCreateForm(ComponentForm):
-    """
-    Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
-    """
-    manufacturer = DynamicModelChoiceField(
-        queryset=Manufacturer.objects.all(),
-        required=False,
-        initial_params={
-            'device_types': 'device_type',
-            'module_types': 'module_type',
-        }
-    )
-    device_type = DynamicModelChoiceField(
-        queryset=DeviceType.objects.all(),
-        required=False,
-        query_params={
-            'manufacturer_id': '$manufacturer'
-        }
-    )
-    description = forms.CharField(
-        required=False
-    )
-
-
-class ModularComponentTemplateCreateForm(ComponentTemplateCreateForm):
-    module_type = DynamicModelChoiceField(
-        queryset=ModuleType.objects.all(),
-        required=False,
-        query_params={
-            'manufacturer_id': '$manufacturer'
-        }
-    )
-
-
-class ConsolePortTemplateCreateForm(ModularComponentTemplateCreateForm):
-    type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypeChoices),
-        widget=StaticSelect()
-    )
-    field_order = (
-        'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
-    )
-
-
-class ConsoleServerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
-    type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypeChoices),
-        widget=StaticSelect()
-    )
-    field_order = (
-        'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
-    )
-
-
-class PowerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
-    type = forms.ChoiceField(
-        choices=add_blank_choice(PowerPortTypeChoices),
-        required=False
-    )
-    maximum_draw = forms.IntegerField(
-        min_value=1,
-        required=False,
-        help_text="Maximum power draw (watts)"
-    )
-    allocated_draw = forms.IntegerField(
-        min_value=1,
-        required=False,
-        help_text="Allocated power draw (watts)"
-    )
-    field_order = (
-        'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw',
-        'allocated_draw', 'description',
-    )
-
-
-class PowerOutletTemplateCreateForm(ModularComponentTemplateCreateForm):
-    type = forms.ChoiceField(
-        choices=add_blank_choice(PowerOutletTypeChoices),
-        required=False
-    )
-    power_port = DynamicModelChoiceField(
-        queryset=PowerPortTemplate.objects.all(),
-        required=False,
-        query_params={
-            'devicetype_id': '$device_type',
-            'moduletype_id': '$module_type',
-        }
-    )
-    feed_leg = forms.ChoiceField(
-        choices=add_blank_choice(PowerOutletFeedLegChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    field_order = (
-        'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
-        'description',
-    )
-
-
-class InterfaceTemplateCreateForm(ModularComponentTemplateCreateForm):
-    type = forms.ChoiceField(
-        choices=InterfaceTypeChoices,
-        widget=StaticSelect()
-    )
-    mgmt_only = forms.BooleanField(
-        required=False,
-        label='Management only'
-    )
-    field_order = (
-        'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only',
-        'description',
-    )
-
-
-class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
-    type = forms.ChoiceField(
-        choices=PortTypeChoices,
-        widget=StaticSelect()
-    )
-    color = ColorField(
-        required=False
-    )
-    rear_port_set = forms.MultipleChoiceField(
-        choices=[],
-        label='Rear ports',
-        help_text='Select one rear port assignment for each front port being created.',
-    )
-    field_order = (
-        'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set',
-        'description',
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        device_type = DeviceType.objects.get(
-            pk=self.initial.get('device_type') or self.data.get('device_type')
-        )
-
-        # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
-        occupied_port_positions = [
-            (front_port.rear_port_id, front_port.rear_port_position)
-            for front_port in device_type.frontporttemplates.all()
-        ]
-
-        # Populate rear port choices
-        choices = []
-        rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
-        for rear_port in rear_ports:
-            for i in range(1, rear_port.positions + 1):
-                if (rear_port.pk, i) not in occupied_port_positions:
-                    choices.append(
-                        ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
-                    )
-        self.fields['rear_port_set'].choices = choices
-
-    def clean(self):
-        super().clean()
-
-        # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
-        front_port_count = len(self.cleaned_data['name_pattern'])
-        rear_port_count = len(self.cleaned_data['rear_port_set'])
-        if front_port_count != rear_port_count:
-            raise forms.ValidationError({
-                'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
-                                 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
-            })
-
-    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(':')
-
-        return {
-            'rear_port': int(rear_port),
-            'rear_port_position': int(position),
-        }
-
-
-class RearPortTemplateCreateForm(ModularComponentTemplateCreateForm):
-    type = forms.ChoiceField(
-        choices=PortTypeChoices,
-        widget=StaticSelect(),
-    )
-    color = ColorField(
-        required=False
-    )
-    positions = forms.IntegerField(
-        min_value=REARPORT_POSITIONS_MIN,
-        max_value=REARPORT_POSITIONS_MAX,
-        initial=1,
-        help_text='The number of front ports which may be mapped to each rear port'
-    )
-    field_order = (
-        'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions',
-        'description',
-    )
-
-
-class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
-    # TODO: Support patterned position assignment
-    field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
-
-
-class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
-    field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
-
-
-#
-# Device components
-#
-
-class ComponentCreateForm(CustomFieldsMixin, ComponentForm):
-    """
-    Base form for the creation of device components (models subclassed from ComponentModel).
-    """
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all()
-    )
-    description = forms.CharField(
-        max_length=200,
-        required=False
-    )
-    tags = DynamicModelMultipleChoiceField(
-        queryset=Tag.objects.all(),
-        required=False
-    )
-
-
-class ConsolePortCreateForm(ComponentCreateForm):
-    model = ConsolePort
-    type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypeChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    speed = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortSpeedChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
-
-
-class ConsoleServerPortCreateForm(ComponentCreateForm):
-    model = ConsoleServerPort
-    type = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortTypeChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    speed = forms.ChoiceField(
-        choices=add_blank_choice(ConsolePortSpeedChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
-
-
-class PowerPortCreateForm(ComponentCreateForm):
-    model = PowerPort
-    type = forms.ChoiceField(
-        choices=add_blank_choice(PowerPortTypeChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    maximum_draw = forms.IntegerField(
-        min_value=1,
-        required=False,
-        help_text="Maximum draw in watts"
-    )
-    allocated_draw = forms.IntegerField(
-        min_value=1,
-        required=False,
-        help_text="Allocated draw in watts"
-    )
-    field_order = (
-        'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
-        'description', 'tags',
-    )
-
-
-class PowerOutletCreateForm(ComponentCreateForm):
-    model = PowerOutlet
-    type = forms.ChoiceField(
-        choices=add_blank_choice(PowerOutletTypeChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    power_port = forms.ModelChoiceField(
-        queryset=PowerPort.objects.all(),
-        required=False
-    )
-    feed_leg = forms.ChoiceField(
-        choices=add_blank_choice(PowerOutletFeedLegChoices),
-        required=False
-    )
-    field_order = (
-        'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
-        'tags',
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        # Limit power_port queryset to PowerPorts which belong to the parent Device
-        device = Device.objects.get(
-            pk=self.initial.get('device') or self.data.get('device')
-        )
-        self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
-
-
-class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
-    model = Interface
-    type = forms.ChoiceField(
-        choices=InterfaceTypeChoices,
-        widget=StaticSelect(),
-    )
-    enabled = forms.BooleanField(
-        required=False,
-        initial=True
-    )
-    parent = DynamicModelChoiceField(
-        queryset=Interface.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
-    )
-    bridge = DynamicModelChoiceField(
-        queryset=Interface.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-        }
-    )
-    lag = DynamicModelChoiceField(
-        queryset=Interface.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device',
-            'type': 'lag',
-        },
-        label='LAG'
-    )
-    mac_address = forms.CharField(
-        required=False,
-        label='MAC Address'
-    )
-    wwn = forms.CharField(
-        required=False,
-        label='WWN'
-    )
-    mgmt_only = forms.BooleanField(
-        required=False,
-        label='Management only',
-        help_text='This interface is used only for out-of-band management'
-    )
-    mode = forms.ChoiceField(
-        choices=add_blank_choice(InterfaceModeChoices),
-        required=False,
-        widget=StaticSelect()
-    )
-    rf_role = forms.ChoiceField(
-        choices=add_blank_choice(WirelessRoleChoices),
-        required=False,
-        widget=StaticSelect(),
-        label='Wireless role'
-    )
-    rf_channel = forms.ChoiceField(
-        choices=add_blank_choice(WirelessChannelChoices),
-        required=False,
-        widget=StaticSelect(),
-        label='Wireless channel'
-    )
-    rf_channel_frequency = forms.DecimalField(
-        required=False,
-        label='Channel frequency (MHz)'
-    )
-    rf_channel_width = forms.DecimalField(
-        required=False,
-        label='Channel width (MHz)'
-    )
-    untagged_vlan = DynamicModelChoiceField(
-        queryset=VLAN.objects.all(),
-        required=False,
-        label='Untagged VLAN'
-    )
-    tagged_vlans = DynamicModelMultipleChoiceField(
-        queryset=VLAN.objects.all(),
-        required=False,
-        label='Tagged VLANs'
-    )
-    field_order = (
-        'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address',
-        'wwn', 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
-        'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        # Limit VLAN choices by device
-        device_id = self.initial.get('device') or self.data.get('device')
-        self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id)
-        self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id)
-
-
-class FrontPortCreateForm(ComponentCreateForm):
-    model = FrontPort
-    type = forms.ChoiceField(
-        choices=PortTypeChoices,
-        widget=StaticSelect(),
-    )
-    color = ColorField(
-        required=False
-    )
-    rear_port_set = 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', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
-        'tags',
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        device = Device.objects.get(
-            pk=self.initial.get('device') or self.data.get('device')
-        )
-
-        # Determine which rear port positions are occupied. These will be excluded from the list of available
-        # mappings.
-        occupied_port_positions = [
-            (front_port.rear_port_id, front_port.rear_port_position)
-            for front_port in device.frontports.all()
-        ]
-
-        # Populate rear port choices
-        choices = []
-        rear_ports = RearPort.objects.filter(device=device)
-        for rear_port in rear_ports:
-            for i in range(1, rear_port.positions + 1):
-                if (rear_port.pk, i) not in occupied_port_positions:
-                    choices.append(
-                        ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
-                    )
-        self.fields['rear_port_set'].choices = choices
-
-    def clean(self):
-        super().clean()
-
-        # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
-        front_port_count = len(self.cleaned_data['name_pattern'])
-        rear_port_count = len(self.cleaned_data['rear_port_set'])
-        if front_port_count != rear_port_count:
-            raise forms.ValidationError({
-                'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
-                                 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
-            })
-
-    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(':')
-
-        return {
-            'rear_port': int(rear_port),
-            'rear_port_position': int(position),
-        }
-
-
-class RearPortCreateForm(ComponentCreateForm):
-    model = RearPort
-    type = forms.ChoiceField(
-        choices=PortTypeChoices,
-        widget=StaticSelect(),
-    )
-    color = ColorField(
-        required=False
-    )
-    positions = forms.IntegerField(
-        min_value=REARPORT_POSITIONS_MIN,
-        max_value=REARPORT_POSITIONS_MAX,
-        initial=1,
-        help_text='The number of front ports which may be mapped to each rear port'
-    )
-    field_order = (
-        'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
-        'tags',
-    )
-
-
-class ModuleBayCreateForm(ComponentCreateForm):
-    model = ModuleBay
-    field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
-
-
-class DeviceBayCreateForm(ComponentCreateForm):
-    model = DeviceBay
-    field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
-
-
-class InventoryItemCreateForm(ComponentCreateForm):
-    model = InventoryItem
-    parent = DynamicModelChoiceField(
-        queryset=InventoryItem.objects.all(),
-        required=False,
-        query_params={
-            'device_id': '$device'
-        }
-    )
-    role = DynamicModelChoiceField(
-        queryset=InventoryItemRole.objects.all(),
-        required=False
-    )
-    manufacturer = DynamicModelChoiceField(
-        queryset=Manufacturer.objects.all(),
-        required=False
-    )
-    part_id = forms.CharField(
-        max_length=50,
-        required=False,
-        label='Part ID'
-    )
-    serial = forms.CharField(
-        max_length=50,
-        required=False,
-    )
-    asset_tag = forms.CharField(
-        max_length=50,
-        required=False,
-    )
-    component_type = ContentTypeChoiceField(
-        queryset=ContentType.objects.all(),
-        limit_choices_to=MODULAR_COMPONENT_MODELS,
-        required=False,
-        widget=StaticSelect
-    )
-    component_id = forms.IntegerField(
-        required=False
-    )
-    field_order = (
-        'device', 'parent', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
-        'description', 'component_type', 'component_id', 'tags',
-    )

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

@@ -118,41 +118,27 @@ class DeviceTestCase(TestCase):
 
 class LabelTestCase(TestCase):
 
-    @classmethod
-    def setUpTestData(cls):
-        site = Site.objects.create(name='Site 2', slug='site-2')
-        manufacturer = Manufacturer.objects.create(name='Manufacturer 2', slug='manufacturer-2')
-        cls.device_type = DeviceType.objects.create(
-            manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=1
-        )
-        device_role = DeviceRole.objects.create(
-            name='Device Role 2', slug='device-role-2', color='ffff00'
-        )
-        cls.device = Device.objects.create(
-            name='Device 2', device_type=cls.device_type, device_role=device_role, site=site
-        )
-
     def test_interface_label_count_valid(self):
-        """Test that a `label` can be generated for each generated `name` from `name_pattern` on InterfaceCreateForm"""
+        """
+        Test that generating an equal number of names and labels passes form validation.
+        """
         interface_data = {
-            'device': self.device.pk,
             'name_pattern': 'eth[0-9]',
             'label_pattern': 'Interface[0-9]',
-            'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
         }
-        form = InterfaceCreateForm(interface_data)
+        form = ComponentCreateForm(interface_data)
 
         self.assertTrue(form.is_valid())
 
     def test_interface_label_count_mismatch(self):
-        """Test that a `label` cannot be generated for each generated `name` from `name_pattern` due to invalid `label_pattern` on InterfaceCreateForm"""
+        """
+        Check that attempting to generate a differing number of names and labels results in a validation error.
+        """
         bad_interface_data = {
-            'device': self.device.pk,
             'name_pattern': 'eth[0-9]',
             'label_pattern': 'Interface[0-1]',
-            'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
         }
-        form = InterfaceCreateForm(bad_interface_data)
+        form = ComponentCreateForm(bad_interface_data)
 
         self.assertFalse(form.is_valid())
         self.assertIn('label_pattern', form.errors)

+ 0 - 19
netbox/dcim/views.py

@@ -1054,7 +1054,6 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
 
 class ConsolePortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsolePortTemplate.objects.all()
-    form = forms.ConsolePortTemplateCreateForm
     model_form = forms.ConsolePortTemplateForm
 
 
@@ -1088,7 +1087,6 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsoleServerPortTemplate.objects.all()
-    form = forms.ConsoleServerPortTemplateCreateForm
     model_form = forms.ConsoleServerPortTemplateForm
 
 
@@ -1122,7 +1120,6 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class PowerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerPortTemplate.objects.all()
-    form = forms.PowerPortTemplateCreateForm
     model_form = forms.PowerPortTemplateForm
 
 
@@ -1156,7 +1153,6 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class PowerOutletTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerOutletTemplate.objects.all()
-    form = forms.PowerOutletTemplateCreateForm
     model_form = forms.PowerOutletTemplateForm
 
 
@@ -1190,7 +1186,6 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class InterfaceTemplateCreateView(generic.ComponentCreateView):
     queryset = InterfaceTemplate.objects.all()
-    form = forms.InterfaceTemplateCreateForm
     model_form = forms.InterfaceTemplateForm
 
 
@@ -1224,7 +1219,6 @@ class InterfaceTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class FrontPortTemplateCreateView(generic.ComponentCreateView):
     queryset = FrontPortTemplate.objects.all()
-    form = forms.FrontPortTemplateCreateForm
     model_form = forms.FrontPortTemplateForm
 
 
@@ -1258,7 +1252,6 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class RearPortTemplateCreateView(generic.ComponentCreateView):
     queryset = RearPortTemplate.objects.all()
-    form = forms.RearPortTemplateCreateForm
     model_form = forms.RearPortTemplateForm
 
 
@@ -1292,7 +1285,6 @@ class RearPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class ModuleBayTemplateCreateView(generic.ComponentCreateView):
     queryset = ModuleBayTemplate.objects.all()
-    form = forms.ModuleBayTemplateCreateForm
     model_form = forms.ModuleBayTemplateForm
 
 
@@ -1326,7 +1318,6 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class DeviceBayTemplateCreateView(generic.ComponentCreateView):
     queryset = DeviceBayTemplate.objects.all()
-    form = forms.DeviceBayTemplateCreateForm
     model_form = forms.DeviceBayTemplateForm
 
 
@@ -1741,7 +1732,6 @@ class ConsolePortView(generic.ObjectView):
 
 class ConsolePortCreateView(generic.ComponentCreateView):
     queryset = ConsolePort.objects.all()
-    form = forms.ConsolePortCreateForm
     model_form = forms.ConsolePortForm
 
 
@@ -1800,7 +1790,6 @@ class ConsoleServerPortView(generic.ObjectView):
 
 class ConsoleServerPortCreateView(generic.ComponentCreateView):
     queryset = ConsoleServerPort.objects.all()
-    form = forms.ConsoleServerPortCreateForm
     model_form = forms.ConsoleServerPortForm
 
 
@@ -1859,7 +1848,6 @@ class PowerPortView(generic.ObjectView):
 
 class PowerPortCreateView(generic.ComponentCreateView):
     queryset = PowerPort.objects.all()
-    form = forms.PowerPortCreateForm
     model_form = forms.PowerPortForm
 
 
@@ -1918,7 +1906,6 @@ class PowerOutletView(generic.ObjectView):
 
 class PowerOutletCreateView(generic.ComponentCreateView):
     queryset = PowerOutlet.objects.all()
-    form = forms.PowerOutletCreateForm
     model_form = forms.PowerOutletForm
 
 
@@ -2012,7 +1999,6 @@ class InterfaceView(generic.ObjectView):
 
 class InterfaceCreateView(generic.ComponentCreateView):
     queryset = Interface.objects.all()
-    form = forms.InterfaceCreateForm
     model_form = forms.InterfaceForm
     template_name = 'dcim/interface_create.html'
 
@@ -2098,7 +2084,6 @@ class FrontPortView(generic.ObjectView):
 
 class FrontPortCreateView(generic.ComponentCreateView):
     queryset = FrontPort.objects.all()
-    form = forms.FrontPortCreateForm
     model_form = forms.FrontPortForm
 
 
@@ -2157,7 +2142,6 @@ class RearPortView(generic.ObjectView):
 
 class RearPortCreateView(generic.ComponentCreateView):
     queryset = RearPort.objects.all()
-    form = forms.RearPortCreateForm
     model_form = forms.RearPortForm
 
 
@@ -2216,7 +2200,6 @@ class ModuleBayView(generic.ObjectView):
 
 class ModuleBayCreateView(generic.ComponentCreateView):
     queryset = ModuleBay.objects.all()
-    form = forms.ModuleBayCreateForm
     model_form = forms.ModuleBayForm
 
 
@@ -2271,7 +2254,6 @@ class DeviceBayView(generic.ObjectView):
 
 class DeviceBayCreateView(generic.ComponentCreateView):
     queryset = DeviceBay.objects.all()
-    form = forms.DeviceBayCreateForm
     model_form = forms.DeviceBayForm
 
 
@@ -2397,7 +2379,6 @@ class InventoryItemEditView(generic.ObjectEditView):
 
 class InventoryItemCreateView(generic.ComponentCreateView):
     queryset = InventoryItem.objects.all()
-    form = forms.InventoryItemCreateForm
     model_form = forms.InventoryItemForm
 
 

+ 25 - 10
netbox/netbox/views/generic/object_views.py

@@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from django.db import transaction
 from django.db.models import ProtectedError
+from django.forms.widgets import HiddenInput
 from django.http import HttpResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.html import escape
@@ -14,6 +15,7 @@ from django.utils.safestring import mark_safe
 from django.views.generic import View
 from django_tables2.export import TableExport
 
+from dcim.forms.object_create import ComponentCreateForm
 from extras.models import ExportTemplate
 from extras.signals import clear_webhooks
 from utilities.error_handlers import handle_protectederror
@@ -674,33 +676,45 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
 # Device/VirtualMachine components
 #
 
-# TODO: Replace with BulkCreateView
 class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
     """
     Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
     """
     queryset = None
-    form = None
+    form = ComponentCreateForm
     model_form = None
-    template_name = 'generic/object_edit.html'
+    template_name = 'dcim/component_create.html'
+    patterned_fields = ('name', 'label')
 
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'add')
 
-    def get(self, request):
+    def initialize_forms(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(initial=request.GET)
+        return form, model_form
+
+    def get(self, request):
+        form, model_form = self.initialize_forms(request)
 
         return render(request, self.template_name, {
-            'obj': self.queryset.model(),
             'obj_type': self.queryset.model._meta.verbose_name,
             'form': form,
+            'model_form': model_form,
             'return_url': self.get_return_url(request),
         })
 
     def post(self, request):
-        logger = logging.getLogger('netbox.views.ComponentCreateView')
-        form = self.form(request.POST, initial=request.GET)
+        form, model_form = self.initialize_forms(request)
+
         self.validate_form(request, form)
 
         if form.is_valid() and not form.errors:
@@ -712,6 +726,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
         return render(request, self.template_name, {
             'obj_type': self.queryset.model._meta.verbose_name,
             'form': form,
+            'model_form': model_form,
             'return_url': self.get_return_url(request),
         })
 
@@ -734,8 +749,8 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
                 data['name'] = name
                 data['label'] = label
 
-                if hasattr(form, 'get_iterative_data'):
-                    data.update(form.get_iterative_data(i))
+                # if hasattr(form, 'get_iterative_data'):
+                #     data.update(form.get_iterative_data(i))
 
                 component_form = self.model_form(data)
 

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

@@ -0,0 +1,7 @@
+{% extends 'generic/object_edit.html' %}
+{% load form_helpers %}
+
+{% block form %}
+  {% render_form form %}
+  {% render_form model_form %}
+{% endblock form %}

+ 2 - 70
netbox/virtualization/forms/object_create.py

@@ -1,81 +1,13 @@
 from django import forms
 
-from dcim.choices import InterfaceModeChoices
-from dcim.forms.common import InterfaceCommonForm
-from extras.forms import CustomFieldsMixin
-from extras.models import Tag
-from ipam.models import VLAN
-from utilities.forms import (
-    add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
-    StaticSelect,
-)
-from virtualization.models import VMInterface, VirtualMachine
+from utilities.forms import BootstrapMixin, ExpandableNameField
 
 __all__ = (
     'VMInterfaceCreateForm',
 )
 
 
-class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonForm):
-    model = VMInterface
-    virtual_machine = DynamicModelChoiceField(
-        queryset=VirtualMachine.objects.all()
-    )
+class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
     name_pattern = ExpandableNameField(
         label='Name'
     )
-    enabled = forms.BooleanField(
-        required=False,
-        initial=True
-    )
-    parent = DynamicModelChoiceField(
-        queryset=VMInterface.objects.all(),
-        required=False,
-        query_params={
-            'virtual_machine_id': '$virtual_machine',
-        }
-    )
-    bridge = DynamicModelChoiceField(
-        queryset=VMInterface.objects.all(),
-        required=False,
-        query_params={
-            'virtual_machine_id': '$virtual_machine',
-        }
-    )
-    mac_address = forms.CharField(
-        required=False,
-        label='MAC Address'
-    )
-    description = forms.CharField(
-        max_length=200,
-        required=False
-    )
-    mode = forms.ChoiceField(
-        choices=add_blank_choice(InterfaceModeChoices),
-        required=False,
-        widget=StaticSelect(),
-    )
-    untagged_vlan = DynamicModelChoiceField(
-        queryset=VLAN.objects.all(),
-        required=False
-    )
-    tagged_vlans = DynamicModelMultipleChoiceField(
-        queryset=VLAN.objects.all(),
-        required=False
-    )
-    tags = DynamicModelMultipleChoiceField(
-        queryset=Tag.objects.all(),
-        required=False
-    )
-    field_order = (
-        'virtual_machine', 'name_pattern', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', 'description', 'mode',
-        'untagged_vlan', 'tagged_vlans', 'tags'
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
-
-        # Limit VLAN choices by virtual machine
-        self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
-        self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)

+ 1 - 1
netbox/virtualization/views.py

@@ -447,11 +447,11 @@ class VMInterfaceView(generic.ObjectView):
         }
 
 
-# TODO: This should not use ComponentCreateView
 class VMInterfaceCreateView(generic.ComponentCreateView):
     queryset = VMInterface.objects.all()
     form = forms.VMInterfaceCreateForm
     model_form = forms.VMInterfaceForm
+    patterned_fields = ('name',)
 
 
 class VMInterfaceEditView(generic.ObjectEditView):