feat(ipam): Add changelog message support to bulk Prefix/IP creation

Extend bulk add forms for Prefix and IPAddress to support changelog
messages. Switch IPAddressBulkAddForm to PrimaryModelForm base, update
field ordering, consolidate template rendering, and add test coverage.

Fixes #21780
This commit is contained in:
Martin Hauser
2026-04-06 20:15:02 +02:00
parent 02f9ca8f01
commit d630afaf14
5 changed files with 77 additions and 48 deletions

View File

@@ -483,7 +483,7 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
return ipaddress
class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
class IPAddressBulkAddForm(TenancyForm, PrimaryModelForm):
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -498,7 +498,8 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
'address', 'vrf', 'status', 'role', 'dns_name', 'tenant_group', 'tenant', 'description', 'owner',
'comments', 'tags',
]

View File

@@ -5,7 +5,8 @@ from django.test import override_settings
from django.urls import reverse
from netaddr import IPNetwork
from core.models import ObjectType
from core.choices import ObjectChangeActionChoices
from core.models import ObjectChange, ObjectType
from dcim.constants import InterfaceTypeChoices
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from ipam.choices import *
@@ -543,6 +544,37 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
f'Expected prefix {prefix_str} was not created'
)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_bulk_add_prefixes_with_changelog_message(self):
obj_perm = ObjectPermission(name='Test permission', actions=['add'])
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix))
changelog_message = 'Bulk-created prefixes'
prefixes = [IPNetwork(f'198.18.{i}.0/24') for i in range(3)]
url = reverse('ipam:prefix_bulk_add')
data = {
'pattern': '198.18.[0-2].0/24',
'status': PrefixStatusChoices.STATUS_ACTIVE,
'changelog_message': changelog_message,
}
response = self.client.post(url, data)
self.assertHttpStatus(response, 302)
created_prefixes = list(Prefix.objects.filter(prefix__in=prefixes))
self.assertEqual(len(created_prefixes), len(prefixes))
objectchanges = ObjectChange.objects.filter(
action=ObjectChangeActionChoices.ACTION_CREATE,
changed_object_type=ContentType.objects.get_for_model(Prefix),
changed_object_id__in=[obj.pk for obj in created_prefixes],
)
self.assertEqual(objectchanges.count(), len(prefixes))
for objectchange in objectchanges:
self.assertEqual(objectchange.message, changelog_message)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_prefixes(self):
prefixes = (
@@ -908,6 +940,39 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_bulk_add_ipaddresses_with_changelog_message(self):
obj_perm = ObjectPermission(name='Test permission', actions=['add'])
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ObjectType.objects.get_for_model(IPAddress))
vrf = VRF.objects.get(name='VRF 1')
changelog_message = 'Bulk-created IP addresses'
addresses = [IPNetwork(f'198.51.100.{i}/24') for i in range(10, 13)]
url = reverse('ipam:ipaddress_bulk_add')
data = {
'pattern': '198.51.100.[10-12]/24',
'vrf': vrf.pk,
'status': IPAddressStatusChoices.STATUS_ACTIVE,
'changelog_message': changelog_message,
}
response = self.client.post(url, data)
self.assertHttpStatus(response, 302)
created_addresses = list(IPAddress.objects.filter(address__in=addresses, vrf=vrf))
self.assertEqual(len(created_addresses), len(addresses))
objectchanges = ObjectChange.objects.filter(
action=ObjectChangeActionChoices.ACTION_CREATE,
changed_object_type=ContentType.objects.get_for_model(IPAddress),
changed_object_id__in=[obj.pk for obj in created_addresses],
)
self.assertEqual(objectchanges.count(), len(addresses))
for objectchange in objectchanges:
self.assertEqual(objectchange.message, changelog_message)
class FHRPGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = FHRPGroup

View File

@@ -243,6 +243,7 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
# Validate each new object independently.
if model_form.is_valid():
model_form.instance._changelog_message = model_form.cleaned_data.get('changelog_message', '')
obj = model_form.save()
new_objects.append(obj)
else:

View File

@@ -21,31 +21,14 @@
{% endblock %}
{% block pre_form_fields %}
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Pattern" %}</h2>
</div>
{% render_field form.pattern %}
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Pattern" %}</h2>
</div>
{% render_field form.pattern %}
</div>
{% endblock pre_form_fields %}
{% block form %}
{% if model_form.fieldsets %}
{% for fieldset in model_form.fieldsets %}
{% render_fieldset model_form fieldset %}
{% endfor %}
{% else %}
<div class="field-group my-5">
{% render_form model_form %}
</div>
{% endif %}
{% if model_form.custom_fields %}
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
</div>
{% render_custom_fields model_form %}
</div>
{% endif %}
{% include 'htmx/form.html' with form=model_form %}
{% endblock form %}

View File

@@ -1,22 +1 @@
{% load helpers %}
{% load form_helpers %}
{% load i18n %}
{% if model_form.fieldsets %}
{% for fieldset in model_form.fieldsets %}
{% render_fieldset model_form fieldset %}
{% endfor %}
{% else %}
<div class="field-group my-5">
{% render_form model_form %}
</div>
{% endif %}
{% if model_form.custom_fields %}
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
</div>
{% render_custom_fields model_form %}
</div>
{% endif %}
{% include 'htmx/form.html' with form=model_form %}