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),
})