diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index 366241390..ca264806b 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -6,6 +6,7 @@ * [#4766](https://github.com/netbox-community/netbox/issues/4766) - Fix redirect after login when `next` is not specified * [#4772](https://github.com/netbox-community/netbox/issues/4772) - Fix "brief" format for the secrets REST API endpoint +* [#4775](https://github.com/netbox-community/netbox/issues/4775) - Allow selecting an alternate device type when creating component templates --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index e7cb461bf..b5b1d029f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -25,8 +25,8 @@ from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, - LabeledComponentForm, NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, - StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine from .choices import * @@ -128,6 +128,28 @@ class InterfaceCommonForm: }) +class LabeledComponentForm(BootstrapMixin, forms.Form): + name_pattern = ExpandableNameField( + label='Name' + ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) + + def clean(self): + + # Validate that the number of components being created from both the name_pattern and label_pattern are equal + name_pattern_count = len(self.cleaned_data['name_pattern']) + label_pattern_count = len(self.cleaned_data['label_pattern']) + if label_pattern_count and name_pattern_count != label_pattern_count: + raise forms.ValidationError({ + 'label_pattern': 'The provided name pattern will create {} components, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }, code='label_pattern_mismatch') + + class BulkRenameForm(forms.Form): """ An extendable form to be used for renaming device components in bulk. @@ -1036,6 +1058,27 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): # Device component templates # +class ComponentTemplateCreateForm(LabeledComponentForm): + """ + Base form for the creation of device component templates. + """ + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + widget=APISelect( + filter_for={ + 'device_type': 'manufacturer_id' + } + ) + ) + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + display_field='model' + ) + ) + + class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1048,10 +1091,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortTemplateCreateForm(LabeledComponentForm): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) +class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() @@ -1085,10 +1125,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsoleServerPortTemplateCreateForm(LabeledComponentForm): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) +class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() @@ -1122,10 +1159,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class PowerPortTemplateCreateForm(LabeledComponentForm): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) +class PowerPortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False @@ -1189,10 +1223,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): ) -class PowerOutletTemplateCreateForm(LabeledComponentForm): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) +class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False @@ -1273,10 +1304,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): } -class InterfaceTemplateCreateForm(LabeledComponentForm): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) +class InterfaceTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2() @@ -1330,13 +1358,7 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): ) -class FrontPortTemplateCreateForm(BootstrapMixin, forms.Form): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) - name_pattern = ExpandableNameField( - label='Name' - ) +class FrontPortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2() @@ -1421,13 +1443,7 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class RearPortTemplateCreateForm(BootstrapMixin, forms.Form): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) - name_pattern = ExpandableNameField( - label='Name' - ) +class RearPortTemplateCreateForm(ComponentTemplateCreateForm): type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2(), @@ -1467,13 +1483,8 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form): - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all() - ) - name_pattern = ExpandableNameField( - label='Name' - ) +class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm): + pass # TODO: DeviceBayTemplate has no fields suitable for bulk-editing yet @@ -2206,9 +2217,22 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt # -# Bulk device component creation +# Device components # +class ComponentCreateForm(LabeledComponentForm): + """ + Base form for the creation of device components. + """ + device = DynamicModelChoiceField( + queryset=Device.objects.all() + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class DeviceBulkAddComponentForm(LabeledComponentForm): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), @@ -2251,10 +2275,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortCreateForm(LabeledComponentForm): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) +class ConsolePortCreateForm(ComponentCreateForm): type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2336,10 +2357,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): } -class ConsoleServerPortCreateForm(LabeledComponentForm): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) +class ConsoleServerPortCreateForm(ComponentCreateForm): type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2435,10 +2453,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm): } -class PowerPortCreateForm(LabeledComponentForm): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) +class PowerPortCreateForm(ComponentCreateForm): type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False, @@ -2543,10 +2558,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): ) -class PowerOutletCreateForm(LabeledComponentForm): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) +class PowerOutletCreateForm(ComponentCreateForm): type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False, @@ -2767,10 +2779,7 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) -class InterfaceCreateForm(InterfaceCommonForm, LabeledComponentForm): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) +class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2(), @@ -3049,13 +3058,7 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm): # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic -class FrontPortCreateForm(BootstrapMixin, forms.Form): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) - name_pattern = ExpandableNameField( - label='Name' - ) +class FrontPortCreateForm(ComponentCreateForm): type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2(), @@ -3230,13 +3233,7 @@ class RearPortForm(BootstrapMixin, forms.ModelForm): } -class RearPortCreateForm(BootstrapMixin, forms.Form): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) - name_pattern = ExpandableNameField( - label='Name' - ) +class RearPortCreateForm(ComponentCreateForm): type = forms.ChoiceField( choices=PortTypeChoices, widget=StaticSelect2(), @@ -3333,17 +3330,8 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayCreateForm(BootstrapMixin, forms.Form): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) - name_pattern = ExpandableNameField( - label='Name' - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) +class DeviceBayCreateForm(ComponentCreateForm): + pass class PopulateDeviceBayForm(BootstrapMixin, forms.Form): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 033e038d1..22788cf64 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -1134,6 +1134,10 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View )) if '_addanother' in request.POST: return redirect(request.get_full_path()) + elif 'device_type' in form.cleaned_data: + return redirect(form.cleaned_data['device_type'].get_absolute_url()) + elif 'device' in form.cleaned_data: + return redirect(form.cleaned_data['device'].get_absolute_url()) else: return redirect(self.get_return_url(request))