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

This commit is contained in:
bctiemann 2026-06-02 11:25:56 -04:00 committed by GitHub
parent b55b50b12e
commit 35450a6cb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 108 additions and 0 deletions

View File

@ -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)

View File

@ -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()