Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions netbox/utilities/counters.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def post_delete_receiver(sender, instance, origin, **kwargs):
parent_pk = getattr(instance, field_name, None)

# Decrement the parent's counter by one
if parent_pk is not None and not hasattr(instance, "_previously_removed"):
if parent_pk is not None and not hasattr(instance, '_previously_removed'):
update_counter(parent_model, parent_pk, counter_name, -1)


Expand All @@ -87,38 +87,48 @@ def post_delete_receiver(sender, instance, origin, **kwargs):

def connect_counters(*models):
"""
Register counter fields and connect post_save & post_delete signal handlers for the affected models.
Register counter fields and connect signal handlers for their child models.
Ensures exactly one receiver per child (sender), even when multiple counters
reference the same sender (e.g., Device).
"""
for model in models:
connected = set() # child models we've already connected

for model in models:
# Find all CounterCacheFields on the model
counter_fields = [
field for field in model._meta.get_fields() if type(field) is CounterCacheField
]
counter_fields = [field for field in model._meta.get_fields() if isinstance(field, CounterCacheField)]

for field in counter_fields:
to_model = apps.get_model(field.to_model_name)

# Register the counter in the registry
change_tracking_fields = registry['counter_fields'][to_model]
change_tracking_fields[f"{field.to_field_name}_id"] = field.name
change_tracking_fields[f'{field.to_field_name}_id'] = field.name

# Connect signals once per child model
if to_model in connected:
continue

# Ensure dispatch_uid is unique per model (sender), not per field
uid_base = f'countercache.{to_model._meta.label_lower}'

# Connect the post_save and post_delete handlers
post_save.connect(
post_save_receiver,
sender=to_model,
weak=False,
dispatch_uid=f'{model._meta.label}.{field.name}'
dispatch_uid=f'{uid_base}.post_save',
)
pre_delete.connect(
pre_delete_receiver,
sender=to_model,
weak=False,
dispatch_uid=f'{model._meta.label}.{field.name}'
dispatch_uid=f'{uid_base}.pre_delete',
)
post_delete.connect(
post_delete_receiver,
sender=to_model,
weak=False,
dispatch_uid=f'{model._meta.label}.{field.name}'
dispatch_uid=f'{uid_base}.post_delete',
)

connected.add(to_model)