Pārlūkot izejas kodu

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
Martin Hauser 1 dienu atpakaļ
vecāks
revīzija
d630afaf14

+ 3 - 2
netbox/ipam/forms/model_forms.py

@@ -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',
         ]
 
 

+ 66 - 1
netbox/ipam/tests/test_views.py

@@ -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

+ 1 - 0
netbox/netbox/views/generic/bulk_views.py

@@ -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:

+ 6 - 23
netbox/templates/generic/bulk_add.html

@@ -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 %}

+ 1 - 22
netbox/templates/htmx/bulk_add_form.html

@@ -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 %}