diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index 43b852928..f6bc27079 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/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', ) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index a21265db4..4fa27ae69 100644 --- a/netbox/dcim/forms/models.py +++ b/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, diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index d2c941b34..a03597db1 100644 --- a/netbox/dcim/forms/object_create.py +++ b/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 Meta(model_forms.ConsolePortTemplateForm.Meta): + exclude = ('name', 'label') -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: [ge,xe]-0/0/[0-9]. {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 ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm): + + class Meta(model_forms.ConsoleServerPortTemplateForm.Meta): + exclude = ('name', 'label') -class DeviceComponentCreateForm(ComponentCreateForm): - device = DynamicModelChoiceField( - queryset=Device.objects.all() - ) - field_order = ('device', 'name_pattern', 'label_pattern') +class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm): + + class Meta(model_forms.PowerPortTemplateForm.Meta): + exclude = ('name', 'label') -class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm): - rear_port_set = forms.MultipleChoiceField( +class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm): + + 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 {module} 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( +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(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_type', 'name_pattern', 'label_pattern', 'position_pattern') + replication_fields = ('name', 'label', 'position') + + class Meta(model_forms.ModuleBayForm.Meta): + exclude = ('name', 'label', 'position') -class ModuleBayCreateForm(DeviceComponentCreateForm): - position_pattern = ExpandableNameField( - label='Position', - required=False, - help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)' - ) - field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern') +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( diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 838336e21..8f1285901 100644 --- a/netbox/dcim/models/device_components.py +++ b/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): diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index d34003ee5..dfc77b854 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -239,7 +239,7 @@ INTERFACE_BUTTONS = """
  • Inventory Item
  • {% endif %} {% if perms.dcim.add_interface %} -
  • Child Interface
  • +
  • Child Interface
  • {% endif %} {% if perms.ipam.add_l2vpntermination %}
  • L2VPN Termination
  • diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 53474314f..1cd75765a 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/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) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index a25267166..50b36e36d 100644 --- a/netbox/dcim/tests/test_views.py +++ b/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, diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 6ee74377a..aee0cb384 100644 --- a/netbox/dcim/views.py +++ b/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): diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 7340ea2a0..f0741af2c 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/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): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 7617e0402..a56a832b6 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/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) + form = self.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() - - 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), }) diff --git a/netbox/templates/dcim/component_template_create.html b/netbox/templates/dcim/component_template_create.html deleted file mode 100644 index d164db872..000000000 --- a/netbox/templates/dcim/component_template_create.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} - -{% block form %} - {% if form.module_type %} -
    -
    - -
    -
    -
    -
    - {% render_field replication_form.device_type %} -
    -
    - {% render_field replication_form.module_type %} -
    -
    - {% 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 %} diff --git a/netbox/templates/dcim/device_component_edit.html b/netbox/templates/dcim/device_component_edit.html deleted file mode 100644 index 44b93d870..000000000 --- a/netbox/templates/dcim/device_component_edit.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} - -{% block form %} -
    - {% if form.instance.device %} -
    - -
    - -
    -
    - {% endif %} - {% render_form form %} -
    -{% endblock form %} diff --git a/netbox/templates/dcim/frontporttemplate_create.html b/netbox/templates/dcim/frontporttemplate_create.html deleted file mode 100644 index 50e9d355c..000000000 --- a/netbox/templates/dcim/frontporttemplate_create.html +++ /dev/null @@ -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 %} diff --git a/netbox/templates/dcim/inventoryitem_create.html b/netbox/templates/dcim/inventoryitem_create.html deleted file mode 100644 index be910f143..000000000 --- a/netbox/templates/dcim/inventoryitem_create.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'dcim/component_create.html' %} -{% load helpers %} -{% load form_helpers %} - -{% block replication_fields %} - {{ block.super }} - {% if object.component %} -
    - -
    - -
    -
    - {% endif %} -{% endblock replication_fields %} diff --git a/netbox/templates/dcim/inventoryitemtemplate_create.html b/netbox/templates/dcim/inventoryitemtemplate_create.html deleted file mode 100644 index 9180cf6ab..000000000 --- a/netbox/templates/dcim/inventoryitemtemplate_create.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'dcim/component_template_create.html' %} -{% load helpers %} -{% load form_helpers %} - -{% block replication_fields %} - {{ block.super }} - {% if object.component %} -
    - -
    - -
    -
    - {% endif %} -{% endblock replication_fields %} diff --git a/netbox/templates/dcim/modulebaytemplate_create.html b/netbox/templates/dcim/modulebaytemplate_create.html deleted file mode 100644 index 74323ac4b..000000000 --- a/netbox/templates/dcim/modulebaytemplate_create.html +++ /dev/null @@ -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 %} diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 4ce270b30..56e4f5a32 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -59,9 +59,11 @@ Context: {# Render grouped fields according to Form #} {% for group, fields in form.fieldsets %}
    -
    -
    {{ group }}
    -
    + {% if group %} +
    +
    {{ group }}
    +
    + {% endif %} {% for name in fields %} {% with field=form|getfield:name %} {% if not field.field.widget.is_hidden %} diff --git a/netbox/templates/virtualization/vminterface_edit.html b/netbox/templates/virtualization/vminterface_edit.html deleted file mode 100644 index efb138954..000000000 --- a/netbox/templates/virtualization/vminterface_edit.html +++ /dev/null @@ -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 %} - -
    -
    -
    Interface
    -
    - {% if form.instance.virtual_machine %} -
    - -
    - -
    -
    - {% endif %} - {% render_field form.name %} - {% render_field form.description %} - {% render_field form.tags %} -
    - -
    -
    -
    Addressing
    -
    - {% render_field form.vrf %} - {% render_field form.mac_address %} -
    - -
    -
    -
    Operation
    -
    - {% render_field form.mtu %} - {% render_field form.enabled %} -
    - -
    -
    -
    Related Interfaces
    -
    - {% render_field form.parent %} - {% render_field form.bridge %} -
    - -
    -
    -
    802.1Q Switching
    -
    - {% render_field form.mode %} - {% render_field form.vlan_group %} - {% render_field form.untagged_vlan %} - {% render_field form.tagged_vlans %} -
    - - {% if form.custom_fields %} -
    -
    -
    Custom Fields
    -
    - {% render_custom_fields form %} -
    - {% endif %} -{% endblock %} diff --git a/netbox/utilities/forms/fields/expandable.py b/netbox/utilities/forms/fields/expandable.py index 214775f03..fca370c26 100644 --- a/netbox/utilities/forms/fields/expandable.py +++ b/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: [ge,xe]-0/0/[0-9] + are not supported (example: [ge,xe]-0/0/[0-9]). """ def to_python(self, value): diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 7fa9f66bc..93cb88088 100644 --- a/netbox/utilities/testing/views.py +++ b/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): """ diff --git a/netbox/virtualization/forms/bulk_create.py b/netbox/virtualization/forms/bulk_create.py index 6cf7c0d7c..03997f88d 100644 --- a/netbox/virtualization/forms/bulk_create.py +++ b/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',) diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py index fca9c6b56..268afb9bb 100644 --- a/netbox/virtualization/forms/models.py +++ b/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 diff --git a/netbox/virtualization/forms/object_create.py b/netbox/virtualization/forms/object_create.py index feab3bb3a..79457a56e 100644 --- a/netbox/virtualization/forms/object_create.py +++ b/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',) diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 01d4394f3..d00ceb5a2 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/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'), diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 5b26f8503..611725d62 100644 --- a/netbox/virtualization/views.py +++ b/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):