diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 779ad193f..3963b73f4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1273,7 +1273,7 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel): verbose_name_plural = _('module bays') class MPTTMeta: - order_insertion_by = ('name',) + order_insertion_by = ('module', 'name',) def clean(self): super().clean() diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 97ffeccbe..07999b2c2 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -343,21 +343,42 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel): ) else: # ModuleBays must be saved individually for MPTT - for instance in create_instances: - instance.save() + # Use delay_mptt_updates for better performance when creating multiple ModuleBays + with ModuleBay.objects.delay_mptt_updates(): + for instance in create_instances: + instance.save() update_fields = ['module'] - component_model.objects.bulk_update(update_instances, update_fields) - # Emit the post_save signal for each updated object - for component in update_instances: - post_save.send( - sender=component_model, - instance=component, - created=False, - raw=False, - using='default', - update_fields=update_fields - ) + + if component_model is not ModuleBay: + component_model.objects.bulk_update(update_instances, update_fields) + # Emit the post_save signal for each updated object + for component in update_instances: + post_save.send( + sender=component_model, + instance=component, + created=False, + raw=False, + using='default', + update_fields=update_fields + ) + else: + # ModuleBays must be saved individually to maintain MPTT tree structure + # Use delay_mptt_updates for better performance + with ModuleBay.objects.delay_mptt_updates(): + for component in update_instances: + component.save() + post_save.send( + sender=component_model, + instance=component, + created=False, + raw=False, + using='default', + update_fields=update_fields + ) + # Rebuild the tree once to apply order_insertion_by after all operations + if create_instances or update_instances: + ModuleBay.objects.rebuild() # Interface bridges have to be set after interface instantiation update_interface_bridges(self.device, self.module_type.interfacetemplates, self) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index b8d70e112..36363056b 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -895,9 +895,18 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): renamed_pks = self._rename_objects(form, selected_objects) if '_apply' in request.POST: - for obj in selected_objects: - setattr(obj, self.field_name, obj.new_name) - obj.save() + # For MPTT models, delay tree updates until all saves are complete + if issubclass(self.queryset.model, MPTTModel): + with self.queryset.model.objects.delay_mptt_updates(): + for obj in selected_objects: + setattr(obj, self.field_name, obj.new_name) + obj.save() + # Rebuild the tree to apply order_insertion_by after renaming + self.queryset.model.objects.rebuild() + else: + for obj in selected_objects: + setattr(obj, self.field_name, obj.new_name) + obj.save() # Enforce constrained permissions if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):