Pārlūkot izejas kodu

Fixes #22251: Re-parent child ModuleBays when a Module is moved to a new bay (#22336)

bctiemann 1 mēnesi atpakaļ
vecāks
revīzija
35450a6cb8
2 mainītis faili ar 108 papildinājumiem un 0 dzēšanām
  1. 11 0
      netbox/dcim/models/modules.py
  2. 97 0
      netbox/dcim/tests/test_models.py

+ 11 - 0
netbox/dcim/models/modules.py

@@ -326,9 +326,20 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
 
     def save(self, *args, **kwargs):
         is_new = self.pk is None
+        old_module_bay_id = None
+
+        if not is_new:
+            old_module_bay_id = Module.objects.filter(pk=self.pk).values_list(
+                'module_bay_id', flat=True
+            ).first()
 
         super().save(*args, **kwargs)
 
+        if old_module_bay_id is not None and old_module_bay_id != self.module_bay_id:
+            for child_bay in self.modulebays.select_related('module__module_bay'):
+                child_bay.snapshot()
+                child_bay.save()
+
         adopt_components = getattr(self, '_adopt_components', False)
         disable_replication = getattr(self, '_disable_replication', False)
 

+ 97 - 0
netbox/dcim/tests/test_models.py

@@ -1188,6 +1188,103 @@ class ModuleBayTestCase(TestCase):
         self.assertEqual(movable_bay.parent_id, host_bay.pk)
         self.assertEqual(movable_bay.tree_id, host_bay.tree_id)
 
+    @tag('regression')  # #22251
+    def test_moving_module_reparents_child_module_bays(self):
+        """
+        When a module is moved to a different module bay, each child ModuleBay
+        (a bay that belongs to the module) must have its MPTT parent updated
+        to the new host bay. Without the fix the children stay parented to the
+        old bay even though Module.module_bay_id has changed.
+        """
+        device_type = DeviceType.objects.first()
+        device_role = DeviceRole.objects.first()
+        site = Site.objects.first()
+        device = Device.objects.create(
+            name='Move Module Device',
+            device_type=device_type,
+            role=device_role,
+            site=site,
+        )
+        bay_a = ModuleBay.objects.create(device=device, name='Bay A')
+        bay_b = ModuleBay.objects.create(device=device, name='Bay B')
+
+        manufacturer = Manufacturer.objects.first()
+        module_type = ModuleType.objects.create(
+            manufacturer=manufacturer, model='Move Module Type'
+        )
+        module = Module.objects.create(
+            device=device, module_bay=bay_a, module_type=module_type
+        )
+
+        child_1 = ModuleBay.objects.create(device=device, module=module, name='Child Bay 1')
+        child_2 = ModuleBay.objects.create(device=device, module=module, name='Child Bay 2')
+        self.assertEqual(child_1.parent_id, bay_a.pk)
+        self.assertEqual(child_2.parent_id, bay_a.pk)
+
+        # Move the module to bay_b.
+        module.module_bay = bay_b
+        module.save()
+
+        child_1.refresh_from_db()
+        child_2.refresh_from_db()
+        self.assertEqual(child_1.parent_id, bay_b.pk)
+        self.assertEqual(child_2.parent_id, bay_b.pk)
+        # Children must share the same MPTT tree as their new parent.
+        bay_b.refresh_from_db()
+        self.assertEqual(child_1.tree_id, bay_b.tree_id)
+        self.assertEqual(child_2.tree_id, bay_b.tree_id)
+
+    @tag('regression')  # #22251
+    def test_moving_module_reparents_grandchild_module_bays(self):
+        """
+        When a module is moved, grandchild ModuleBays (bays inside a module
+        that is itself installed inside a child bay of the moved module) must
+        also land in the new MPTT tree. MPTT moves subtrees atomically, so
+        calling save() only on direct children is sufficient — this test
+        documents and preserves that invariant for future tree-backend changes.
+        """
+        device_type = DeviceType.objects.first()
+        device_role = DeviceRole.objects.first()
+        site = Site.objects.first()
+        device = Device.objects.create(
+            name='Grandchild Move Device',
+            device_type=device_type,
+            role=device_role,
+            site=site,
+        )
+        bay_a = ModuleBay.objects.create(device=device, name='Bay A')
+        bay_b = ModuleBay.objects.create(device=device, name='Bay B')
+
+        manufacturer = Manufacturer.objects.first()
+        module_type = ModuleType.objects.create(
+            manufacturer=manufacturer, model='Grandchild Move Type'
+        )
+        # Depth-1: module installed in bay_a, with one child bay.
+        module_1 = Module.objects.create(device=device, module_bay=bay_a, module_type=module_type)
+        child_bay = ModuleBay.objects.create(device=device, module=module_1, name='Child Bay')
+
+        # Depth-2: module installed in child_bay, with one grandchild bay.
+        module_2 = Module.objects.create(device=device, module_bay=child_bay, module_type=module_type)
+        grandchild_bay = ModuleBay.objects.create(device=device, module=module_2, name='Grandchild Bay')
+
+        self.assertEqual(child_bay.parent_id, bay_a.pk)
+        self.assertEqual(grandchild_bay.parent_id, child_bay.pk)
+        self.assertEqual(grandchild_bay.tree_id, bay_a.tree_id)
+
+        # Move the top-level module to bay_b.
+        module_1.module_bay = bay_b
+        module_1.save()
+
+        child_bay.refresh_from_db()
+        grandchild_bay.refresh_from_db()
+        bay_b.refresh_from_db()
+
+        self.assertEqual(child_bay.parent_id, bay_b.pk)
+        self.assertEqual(child_bay.tree_id, bay_b.tree_id)
+        # Grandchild's direct parent (child_bay) is unchanged; only tree placement moves.
+        self.assertEqual(grandchild_bay.parent_id, child_bay.pk)
+        self.assertEqual(grandchild_bay.tree_id, bay_b.tree_id)
+
     def test_single_module_token(self):
         device_type = DeviceType.objects.first()
         device_role = DeviceRole.objects.first()