Compare commits

...

21 Commits

Author SHA1 Message Date
Idris Foughali
dc1d8a54c7 Merge cf16a29ad3 into 598f8d034d 2025-12-12 21:55:25 -06:00
Jason Novinger
598f8d034d Fixes #20912: Clear ModuleBay parent when module assignment removed (#20974)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-12-12 13:31:59 -08:00
Arthur Hanson
ec13a79907 Fixes #20875: Fix updating of denormalized fields for component models (#20956) 2025-12-12 13:29:34 -06:00
ifoughali
cf16a29ad3 Style: removed comment 2025-12-05 15:24:35 +01:00
ifoughali
544c97d923 XMerge branch 'closes-20817-Fix-datasource-sync-broken-when-cron-is-set' of https://github.com/ifoughal/netbox into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-12-05 15:23:43 +01:00
ifoughali
77ee6baa23 refactor: moved status update logic from clean() to save() method 2025-12-05 15:23:38 +01:00
Idris Foughali
09d1049267 Merge branch 'netbox-community:main' into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-12-05 14:51:56 +01:00
Idris Foughali
93e5f919ba Merge branch 'netbox-community:main' into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-12-01 10:07:15 +01:00
ifoughali
929d024003 Merge branch 'closes-20817-Fix-datasource-sync-broken-when-cron-is-set' of https://github.com/ifoughal/netbox into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-11-26 09:00:22 +01:00
ifoughali
e4b614038e revert: re-added queued status set for datasource object 2025-11-26 09:00:17 +01:00
Idris Foughali
3016b1d90b Merge branch 'netbox-community:main' into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-11-26 08:55:12 +01:00
ifoughali
57b47dc1ea style: use != instead of not in for single SYNCING check 2025-11-26 08:05:20 +01:00
ifoughali
da4c669312 Feat: reworked status update logic 2025-11-20 11:27:39 +01:00
ifoughali
71f707b7ac Feat: removed SCHEDULED choice due to redundency with sync interval 2025-11-20 11:26:43 +01:00
ifoughali
e11508dd6c Fix: removed status update from the enqueue method 2025-11-20 10:50:35 +01:00
ifoughali
5b5b5c8909 Revert "Feat: set status as editable field"
This reverts commit b4160ad59b.
2025-11-19 20:18:59 +01:00
Idris Foughali
a49869af42 Feat: removed QUEUED from ready for sync condition 2025-11-19 19:01:01 +00:00
Idris Foughali
2e0ff04f84 Feat: added 2 states for DataSourceStatusChoices 2025-11-19 18:52:27 +00:00
Idris Foughali
bfeba36514 Feat: added status update during save method of DataSourceForm 2025-11-19 18:51:25 +00:00
Idris Foughali
111aca115b Feat: added clean method to set data-source state to Ready or scheduled 2025-11-19 18:51:01 +00:00
Idris Foughali
b4160ad59b Feat: set status as editable field 2025-11-19 18:49:47 +00:00
8 changed files with 67 additions and 5 deletions

View File

@@ -13,6 +13,7 @@ class DataSourceStatusChoices(ChoiceSet):
SYNCING = 'syncing' SYNCING = 'syncing'
COMPLETED = 'completed' COMPLETED = 'completed'
FAILED = 'failed' FAILED = 'failed'
READY = 'ready'
CHOICES = ( CHOICES = (
(NEW, _('New'), 'blue'), (NEW, _('New'), 'blue'),
@@ -20,6 +21,7 @@ class DataSourceStatusChoices(ChoiceSet):
(SYNCING, _('Syncing'), 'cyan'), (SYNCING, _('Syncing'), 'cyan'),
(COMPLETED, _('Completed'), 'green'), (COMPLETED, _('Completed'), 'green'),
(FAILED, _('Failed'), 'red'), (FAILED, _('Failed'), 'red'),
(READY, _('Ready'), 'green'),
) )

View File

@@ -16,6 +16,7 @@ from utilities.forms import get_field_value
from utilities.forms.fields import CommentField, JSONField from utilities.forms.fields import CommentField, JSONField
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import HTMXSelect from utilities.forms.widgets import HTMXSelect
from core.choices import DataSourceStatusChoices
__all__ = ( __all__ = (
'ConfigRevisionForm', 'ConfigRevisionForm',
@@ -79,14 +80,28 @@ class DataSourceForm(NetBoxModelForm):
if self.instance and self.instance.parameters: if self.instance and self.instance.parameters:
self.fields[field_name].initial = self.instance.parameters.get(name) self.fields[field_name].initial = self.instance.parameters.get(name)
def save(self, *args, **kwargs):
def save(self, *args, **kwargs):
parameters = {} parameters = {}
for name in self.fields: for name in self.fields:
if name.startswith('backend_'): if name.startswith('backend_'):
parameters[name[8:]] = self.cleaned_data[name] parameters[name[8:]] = self.cleaned_data[name]
self.instance.parameters = parameters self.instance.parameters = parameters
# Determine initial status based on new/existing instance
if not self.instance.pk:
# New instance
object_status = DataSourceStatusChoices.NEW
else:
# Existing instance
if not self.cleaned_data.get("sync_interval"):
object_status = DataSourceStatusChoices.READY
else:
object_status = self.instance.status
# # Final override only if the user explicitly provided a status
self.instance.status = object_status
return super().save(*args, **kwargs) return super().save(*args, **kwargs)

View File

@@ -111,10 +111,7 @@ class DataSource(JobsMixin, PrimaryModel):
@property @property
def ready_for_sync(self): def ready_for_sync(self):
return self.enabled and self.status not in ( return self.enabled and self.status != DataSourceStatusChoices.SYNCING
DataSourceStatusChoices.QUEUED,
DataSourceStatusChoices.SYNCING
)
def clean(self): def clean(self):
super().clean() super().clean()

View File

@@ -1222,6 +1222,8 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.module: if self.module:
self.parent = self.module.module_bay self.parent = self.module.module_bay
else:
self.parent = None
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@@ -957,6 +957,11 @@ class Device(
if cf_defaults := CustomField.objects.get_defaults_for_model(model): if cf_defaults := CustomField.objects.get_defaults_for_model(model):
for component in components: for component in components:
component.custom_field_data = cf_defaults 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) components = model.objects.bulk_create(components)
# Prefetch related objects to minimize queries needed during post_save # Prefetch related objects to minimize queries needed during post_save
prefetch_fields = get_prefetchable_fields(model) prefetch_fields = get_prefetchable_fields(model)

View File

@@ -315,6 +315,12 @@ class Module(PrimaryModel, ConfigContextModel):
for component in create_instances: for component in create_instances:
component.custom_field_data = cf_defaults 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: if component_model is not ModuleBay:
component_model.objects.bulk_create(create_instances) component_model.objects.bulk_create(create_instances)
# Emit the post_save signal for each newly created object # Emit the post_save signal for each newly created object

View File

@@ -44,6 +44,9 @@ def handle_location_site_change(instance, created, **kwargs):
Device.objects.filter(location__in=locations).update(site=instance.site) Device.objects.filter(location__in=locations).update(site=instance.site)
PowerPanel.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) 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) @receiver(post_save, sender=Rack)
@@ -53,6 +56,12 @@ def handle_rack_site_change(instance, created, **kwargs):
""" """
if not created: if not created:
Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location) 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) @receiver(post_save, sender=Device)

View File

@@ -841,6 +841,32 @@ class ModuleBayTestCase(TestCase):
nested_bay = module.modulebays.get(name='SFP A-21') nested_bay = module.modulebays.get(name='SFP A-21')
self.assertEqual(nested_bay.label, '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): class CableTestCase(TestCase):