mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-05 11:46:50 -06:00
Compare commits
4 Commits
71ad3cd088
...
b97f6fa588
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b97f6fa588 | ||
|
|
598f8d034d | ||
|
|
ec13a79907 | ||
|
|
60fce84c96 |
@@ -1222,6 +1222,8 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
|
||||
def save(self, *args, **kwargs):
|
||||
if self.module:
|
||||
self.parent = self.module.module_bay
|
||||
else:
|
||||
self.parent = None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -957,6 +957,11 @@ class Device(
|
||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||
for component in components:
|
||||
component.custom_field_data = cf_defaults
|
||||
# Set denormalized references
|
||||
for component in components:
|
||||
component._site = self.site
|
||||
component._location = self.location
|
||||
component._rack = self.rack
|
||||
components = model.objects.bulk_create(components)
|
||||
# Prefetch related objects to minimize queries needed during post_save
|
||||
prefetch_fields = get_prefetchable_fields(model)
|
||||
|
||||
@@ -315,6 +315,12 @@ class Module(PrimaryModel, ConfigContextModel):
|
||||
for component in create_instances:
|
||||
component.custom_field_data = cf_defaults
|
||||
|
||||
# Set denormalized references
|
||||
for component in create_instances:
|
||||
component._site = self.device.site
|
||||
component._location = self.device.location
|
||||
component._rack = self.device.rack
|
||||
|
||||
if component_model is not ModuleBay:
|
||||
component_model.objects.bulk_create(create_instances)
|
||||
# Emit the post_save signal for each newly created object
|
||||
|
||||
@@ -44,6 +44,9 @@ def handle_location_site_change(instance, created, **kwargs):
|
||||
Device.objects.filter(location__in=locations).update(site=instance.site)
|
||||
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
|
||||
CableTermination.objects.filter(_location__in=locations).update(_site=instance.site)
|
||||
# Update component models for devices in these locations
|
||||
for model in COMPONENT_MODELS:
|
||||
model.objects.filter(device__location__in=locations).update(_site=instance.site)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Rack)
|
||||
@@ -53,6 +56,12 @@ def handle_rack_site_change(instance, created, **kwargs):
|
||||
"""
|
||||
if not created:
|
||||
Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
|
||||
# Update component models for devices in this rack
|
||||
for model in COMPONENT_MODELS:
|
||||
model.objects.filter(device__rack=instance).update(
|
||||
_site=instance.site,
|
||||
_location=instance.location,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Device)
|
||||
|
||||
@@ -841,6 +841,32 @@ class ModuleBayTestCase(TestCase):
|
||||
nested_bay = module.modulebays.get(name='SFP A-21')
|
||||
self.assertEqual(nested_bay.label, 'A-21')
|
||||
|
||||
@tag('regression') # #20912
|
||||
def test_module_bay_parent_cleared_when_module_removed(self):
|
||||
"""Test that the parent field is properly cleared when a module bay's module assignment is removed"""
|
||||
device = Device.objects.first()
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Test Module Type')
|
||||
bay1 = ModuleBay.objects.create(device=device, name='Test Bay 1')
|
||||
bay2 = ModuleBay.objects.create(device=device, name='Test Bay 2')
|
||||
|
||||
# Install a module in bay1
|
||||
module1 = Module.objects.create(device=device, module_bay=bay1, module_type=module_type)
|
||||
|
||||
# Assign bay2 to module1 and verify parent is now set to bay1 (module1's bay)
|
||||
bay2.module = module1
|
||||
bay2.save()
|
||||
bay2.refresh_from_db()
|
||||
self.assertEqual(bay2.parent, bay1)
|
||||
self.assertEqual(bay2.module, module1)
|
||||
|
||||
# Clear the module assignment (return bay2 to device level) Verify parent is cleared
|
||||
bay2.module = None
|
||||
bay2.save()
|
||||
bay2.refresh_from_db()
|
||||
self.assertIsNone(bay2.parent)
|
||||
self.assertIsNone(bay2.module)
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
|
||||
@@ -1071,14 +1071,17 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'name': 'VLAN Group 4',
|
||||
'slug': 'vlan-group-4',
|
||||
'vid_ranges': [[1, 4094]]
|
||||
},
|
||||
{
|
||||
'name': 'VLAN Group 5',
|
||||
'slug': 'vlan-group-5',
|
||||
'vid_ranges': [[1, 4094]]
|
||||
},
|
||||
{
|
||||
'name': 'VLAN Group 6',
|
||||
'slug': 'vlan-group-6',
|
||||
'vid_ranges': [[1, 4094]]
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
|
||||
@@ -141,8 +141,8 @@ class ModelTestCase(TestCase):
|
||||
elif value and type(field) is GenericForeignKey:
|
||||
model_dict[key] = value.pk
|
||||
|
||||
# Handle API output
|
||||
elif api:
|
||||
|
||||
# Replace ContentType numeric IDs with <app_label>.<model>
|
||||
if type(getattr(instance, key)) in (ContentType, ObjectType):
|
||||
object_type = ObjectType.objects.get(pk=value)
|
||||
@@ -152,9 +152,13 @@ class ModelTestCase(TestCase):
|
||||
elif type(value) is IPNetwork:
|
||||
model_dict[key] = str(value)
|
||||
|
||||
else:
|
||||
field = instance._meta.get_field(key)
|
||||
# Normalize arrays of numeric ranges (e.g. VLAN IDs or port ranges).
|
||||
# DB uses canonical half-open [lo, hi) via NumericRange; API uses inclusive [lo, hi].
|
||||
# Convert to inclusive pairs for stable API comparisons.
|
||||
elif type(field) is ArrayField and issubclass(type(field.base_field), RangeField):
|
||||
model_dict[key] = [[r.lower, r.upper - 1] for r in value]
|
||||
|
||||
else:
|
||||
# Convert ArrayFields to CSV strings
|
||||
if type(field) is ArrayField:
|
||||
if getattr(field.base_field, 'choices', None):
|
||||
|
||||
Reference in New Issue
Block a user