diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index c8c488329..bffe7a1eb 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/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', ] diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 74def845f..e77d88b68 100644 --- a/netbox/ipam/tests/test_views.py +++ b/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 diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index ce2efdf08..e75f7c4db 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/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: diff --git a/netbox/templates/generic/bulk_add.html b/netbox/templates/generic/bulk_add.html index b9c3999a1..99b60b3a1 100644 --- a/netbox/templates/generic/bulk_add.html +++ b/netbox/templates/generic/bulk_add.html @@ -21,31 +21,14 @@ {% endblock %} {% block pre_form_fields %} -
-
-

{% trans "Pattern" %}

-
- {% render_field form.pattern %} +
+
+

{% trans "Pattern" %}

+ {% render_field form.pattern %} +
{% endblock pre_form_fields %} {% block form %} - {% if model_form.fieldsets %} - {% for fieldset in model_form.fieldsets %} - {% render_fieldset model_form fieldset %} - {% endfor %} - {% else %} -
- {% render_form model_form %} -
- {% endif %} - - {% if model_form.custom_fields %} -
-
-

{% trans "Custom Fields" %}

-
- {% render_custom_fields model_form %} -
- {% endif %} + {% include 'htmx/form.html' with form=model_form %} {% endblock form %} diff --git a/netbox/templates/htmx/bulk_add_form.html b/netbox/templates/htmx/bulk_add_form.html index 7ea742b30..3e0a19899 100644 --- a/netbox/templates/htmx/bulk_add_form.html +++ b/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 %} -
- {% render_form model_form %} -
-{% endif %} - -{% if model_form.custom_fields %} -
-
-

{% trans "Custom Fields" %}

-
- {% render_custom_fields model_form %} -
-{% endif %} +{% include 'htmx/form.html' with form=model_form %}