diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 3b81f7d89..52ad8c26f 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -471,15 +471,19 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) return ipaddress -class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm): - address_pattern = ExpandableIPAddressField(label='Address Pattern') - vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global') +class IPAddressPatternForm(BootstrapMixin, forms.Form): + pattern = ExpandableIPAddressField(label='Address pattern') - pattern_map = ('address_pattern', 'address') + +class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm): class Meta: model = IPAddress - fields = ['address_pattern', 'vrf', 'tenant', 'status', 'description'] + fields = ['address', 'status', 'vrf', 'tenant', 'description'] + + def __init__(self, *args, **kwargs): + super(IPAddressBulkAddForm, self).__init__(*args, **kwargs) + self.fields['vrf'].empty_label = 'Global' class IPAddressFromCSVForm(forms.ModelForm): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 612a89922..149db5f87 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -584,8 +584,9 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView): permission_required = 'ipam.add_ipaddress' - form = forms.IPAddressBulkAddForm - model_form = forms.IPAddressForm + pattern_form = forms.IPAddressPatternForm + model_form = forms.IPAddressBulkAddForm + pattern_target = 'address' template_name = 'ipam/ipaddress_bulk_add.html' default_return_url = 'ipam:ipaddress_list' diff --git a/netbox/templates/ipam/ipaddress_bulk_add.html b/netbox/templates/ipam/ipaddress_bulk_add.html index d53f73bd5..6dcfaf24e 100644 --- a/netbox/templates/ipam/ipaddress_bulk_add.html +++ b/netbox/templates/ipam/ipaddress_bulk_add.html @@ -12,18 +12,18 @@
IP Addresses
- {% render_field form.address_pattern %} - {% render_field form.vrf %} - {% render_field form.tenant %} - {% render_field form.status %} - {% render_field form.description %} + {% render_field pattern_form.pattern %} + {% render_field model_form.status %} + {% render_field model_form.vrf %} + {% render_field model_form.tenant %} + {% render_field model_form.description %}
- {% if form.custom_fields %} + {% if model_form.custom_fields %}
Custom Fields
- {% render_custom_fields form %} + {% render_custom_fields model_form %}
{% endif %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index a91055964..861977c31 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -290,66 +290,78 @@ class BulkAddView(View): """ Create new objects in bulk. - form: Form class + pattern_form: Form class which provides the `pattern` field model_form: The ModelForm used to create individual objects template_name: The name of the template default_return_url: Name of the URL to which the user is redirected after creating the objects """ - form = None + pattern_form = None model_form = None + pattern_target = '' template_name = None default_return_url = 'home' def get(self, request): - form = self.form() + pattern_form = self.pattern_form() + model_form = self.model_form() return render(request, self.template_name, { 'obj_type': self.model_form._meta.model._meta.verbose_name, - 'form': form, + 'pattern_form': pattern_form, + 'model_form': model_form, 'return_url': reverse(self.default_return_url), }) def post(self, request): model = self.model_form._meta.model - form = self.form(request.POST) - if form.is_valid(): + pattern_form = self.pattern_form(request.POST) + model_form = self.model_form(request.POST) - # Read the pattern field and target from the form's pattern_map - pattern_field, pattern_target = form.pattern_map - pattern = form.cleaned_data[pattern_field] - model_form_data = form.cleaned_data + if pattern_form.is_valid(): + pattern = pattern_form.cleaned_data['pattern'] new_objs = [] + try: with transaction.atomic(): - # Validate and save each object individually + + # Create objects from the expanded. Abort the transaction on the first validation error. for value in pattern: - model_form_data[pattern_target] = value - model_form = self.model_form(model_form_data) + + # Reinstantiate the model form each time to avoid overwriting the same instance. Use a mutable + # copy of the POST QueryDict so that we can update the target field value. + model_form = self.model_form(request.POST.copy()) + model_form.data[self.pattern_target] = value + + # Validate each new object independently. if model_form.is_valid(): obj = model_form.save() new_objs.append(obj) else: - for error in model_form.errors.as_data().values(): - form.add_error(None, error) - # Abort the creation of all objects if errors exist - if form.errors: - raise ValidationError("Validation of one or more model forms failed.") - except ValidationError: + # Copy any errors on the pattern target field to the pattern form. + errors = model_form.errors.as_data() + if errors.get(self.pattern_target): + pattern_form.add_error('pattern', errors[self.pattern_target]) + # Raise an IntegrityError to break the for loop and abort the transaction. + raise IntegrityError() + + # If we make it to this point, validation has succeeded on all new objects. + msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural) + messages.success(request, msg) + UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg) + + if '_addanother' in request.POST: + return redirect(request.path) + return redirect(self.default_return_url) + + except IntegrityError: pass - if not form.errors: - msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural) - messages.success(request, msg) - UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg) - if '_addanother' in request.POST: - return redirect(request.path) - return redirect(self.default_return_url) - return render(request, self.template_name, { - 'form': form, + 'pattern_form': pattern_form, + 'model_form': model_form, 'obj_type': model._meta.verbose_name, 'return_url': reverse(self.default_return_url), })