From e96620260a6c1b5cf8cff2112d40d061984a7b2c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 2 Aug 2022 13:49:34 -0400 Subject: [PATCH] Closes #9903: Implement a mechanism for automatically updating denormalized fields --- docs/release-notes/version-3.3.md | 1 + netbox/extras/registry.py | 1 + netbox/netbox/denormalized.py | 54 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 netbox/netbox/denormalized.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index e30ff011c..49d6891e2 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -124,6 +124,7 @@ Custom field UI visibility has no impact on API operation. * [#9261](https://github.com/netbox-community/netbox/issues/9261) - `NetBoxTable` no longer automatically clears pre-existing calls to `prefetch_related()` on its queryset * [#9434](https://github.com/netbox-community/netbox/issues/9434) - Enabled `django-rich` test runner for more user-friendly output +* [#9903](https://github.com/netbox-community/netbox/issues/9903) - Implement a mechanism for automatically updating denormalized fields ### REST API Changes diff --git a/netbox/extras/registry.py b/netbox/extras/registry.py index 07fd4cc24..e1437c00e 100644 --- a/netbox/extras/registry.py +++ b/netbox/extras/registry.py @@ -28,3 +28,4 @@ registry = Registry() registry['model_features'] = { feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES } +registry['denormalized_fields'] = collections.defaultdict(list) diff --git a/netbox/netbox/denormalized.py b/netbox/netbox/denormalized.py new file mode 100644 index 000000000..5808acddc --- /dev/null +++ b/netbox/netbox/denormalized.py @@ -0,0 +1,54 @@ +import logging + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from extras.registry import registry + + +logger = logging.getLogger('netbox.denormalized') + + +def register(model, field_name, mappings): + """ + Register a denormalized model field to ensure that it is kept up-to-date with the related object. + + Args: + model: The class being updated + field_name: The name of the field related to the triggering instance + mappings: Dictionary mapping of local to remote fields + """ + logger.debug(f'Registering denormalized field {model}.{field_name}') + + field = model._meta.get_field(field_name) + rel_model = field.related_model + + registry['denormalized_fields'][rel_model].append( + (model, field_name, mappings) + ) + + +@receiver(post_save) +def update_denormalized_fields(sender, instance, created, raw, **kwargs): + """ + Check if the sender has denormalized fields registered, and update them as necessary. + """ + # Skip for new objects or those being populated from raw data + if created or raw: + return + + # Look up any denormalized fields referencing this model from the application registry + for model, field_name, mappings in registry['denormalized_fields'].get(sender, []): + logger.debug(f'Updating denormalized values for {model}.{field_name}') + filter_params = { + field_name: instance.pk, + } + update_params = { + # Map the denormalized field names to the instance's values + denorm: getattr(instance, origin) for denorm, origin in mappings.items() + } + + # TODO: Improve efficiency here by placing conditions on the query? + # Update all the denormalized fields with the triggering object's new values + count = model.objects.filter(**filter_params).update(**update_params) + logger.debug(f'Updated {count} rows')