From 32137117c9a734e8e72981cb7e88d1f66929631b Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Fri, 27 Mar 2026 09:51:11 +0100 Subject: [PATCH] feat(dcim): Add changelog message support to bulk component creation Add ChangelogMessageMixin to DeviceBulkAddComponentForm and capture changelog_message during bulk component creation. Ensure message is applied to each created component instance. Add test coverage for changelog message propagation. --- netbox/dcim/forms/bulk_create.py | 5 ++- netbox/dcim/tests/test_views.py | 48 ++++++++++++++++++++++- netbox/netbox/views/generic/bulk_views.py | 3 ++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index a13b2bf38..dd3fa00b6 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from dcim.models import * from extras.models import Tag -from netbox.forms.mixins import CustomFieldsMixin +from netbox.forms.mixins import ChangelogMessageMixin, CustomFieldsMixin from utilities.forms import form_from_model from utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField @@ -27,7 +27,8 @@ __all__ = ( # Device components # -class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm): + +class DeviceBulkAddComponentForm(ChangelogMessageMixin, CustomFieldsMixin, ComponentCreateForm): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 3f7e90952..6aeac74d7 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -3,11 +3,13 @@ from decimal import Decimal from zoneinfo import ZoneInfo import yaml +from django.contrib.contenttypes.models import ContentType from django.test import override_settings, tag from django.urls import reverse from netaddr import EUI -from core.models import ObjectType +from core.choices import ObjectChangeActionChoices +from core.models import ObjectChange, ObjectType from dcim.choices import * from dcim.constants import * from dcim.models import * @@ -2741,6 +2743,50 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{console_ports[2].pk},Console Port 9,New description9", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) + def test_bulk_add_components_with_changelog_message(self): + device1 = Device.objects.get(name='Device 1') + device2 = create_test_device('Device 2') + changelog_message = 'Bulk-created console ports' + + 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(self.model)) + + request = { + 'path': reverse('dcim:device_bulk_add_consoleport'), + 'data': post_data({ + 'pk': [device1.pk, device2.pk], + 'name': 'Console Port Bulk', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'Bulk-created console port', + 'changelog_message': changelog_message, + '_create': True, + }), + } + + initial_count = self._get_queryset().count() + response = self.client.post(**request) + self.assertHttpStatus(response, 302) + self.assertEqual(initial_count + 2, self._get_queryset().count()) + + created_ports = list(ConsolePort.objects.filter(name='Console Port Bulk').order_by('device_id')) + self.assertEqual(len(created_ports), 2) + self.assertEqual([port.device_id for port in created_ports], [device1.pk, device2.pk]) + + objectchanges = ObjectChange.objects.filter( + action=ObjectChangeActionChoices.ACTION_CREATE, + changed_object_type=ContentType.objects.get_for_model(ConsolePort), + changed_object_id__in=[port.pk for port in created_ports], + ) + self.assertEqual(objectchanges.count(), 2) + for objectchange in objectchanges: + self.assertEqual(objectchange.message, changelog_message) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): consoleport = ConsolePort.objects.first() diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 25756e212..e87f48937 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -1139,6 +1139,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): new_components = [] data = deepcopy(form.cleaned_data) + changelog_message = data.pop('changelog_message', '') replication_data = { field: data.pop(field) for field in form.replication_fields } @@ -1160,6 +1161,8 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): component_form = self.model_form(component_data) if component_form.is_valid(): + if changelog_message: + component_form.instance._changelog_message = changelog_message instance = component_form.save() logger.debug(f"Created {instance} on {instance.parent_object}") new_components.append(instance)