Przeglądaj źródła

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 dzień temu
rodzic
commit
d630afaf14

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

@@ -483,7 +483,7 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
         return ipaddress
         return ipaddress
 
 
 
 
-class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
+class IPAddressBulkAddForm(TenancyForm, PrimaryModelForm):
     vrf = DynamicModelChoiceField(
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
@@ -498,7 +498,8 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
         fields = [
         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 django.urls import reverse
 from netaddr import IPNetwork
 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.constants import InterfaceTypeChoices
 from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
 from ipam.choices import *
 from ipam.choices import *
@@ -543,6 +544,37 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
                 f'Expected prefix {prefix_str} was not created'
                 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=['*'])
     @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_prefix_prefixes(self):
     def test_prefix_prefixes(self):
         prefixes = (
         prefixes = (
@@ -908,6 +940,39 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'description': 'New description',
             '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):
 class FHRPGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
     model = FHRPGroup
     model = FHRPGroup

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

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

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

@@ -21,31 +21,14 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block pre_form_fields %}
 {% 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>
     </div>
+    {% render_field form.pattern %}
+  </div>
 {% endblock pre_form_fields %}
 {% endblock pre_form_fields %}
 
 
 {% block form %}
 {% 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 %}
 {% 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 %}