From 70e966923a35bbee377fe5fc102334024f228921 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 30 Oct 2020 13:32:10 -0400 Subject: [PATCH] Add CablePath.get_path() to prefetch path nodes --- netbox/dcim/models/cables.py | 36 ++++++++++++++++++++++--- netbox/dcim/models/device_components.py | 3 +-- netbox/dcim/utils.py | 9 ------- netbox/dcim/views.py | 2 +- netbox/templates/dcim/cable_trace.html | 3 ++- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 891230cb0..03656533c 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -1,3 +1,5 @@ +from collections import defaultdict + from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -9,7 +11,7 @@ from taggit.managers import TaggableManager from dcim.choices import * from dcim.constants import * from dcim.fields import PathField -from dcim.utils import decompile_path_node, path_node_to_object +from dcim.utils import decompile_path_node from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem from extras.utils import extras_features from utilities.fields import ColorField @@ -366,8 +368,7 @@ class CablePath(models.Model): unique_together = ('origin_type', 'origin_id') def __str__(self): - path = ', '.join([str(path_node_to_object(node)) for node in self.path]) - return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})" + return f"Path #{self.pk}: {self.origin} to {self.destination} ({len(self.path)} nodes)" def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -376,6 +377,35 @@ class CablePath(models.Model): model = self.origin._meta.model model.objects.filter(pk=self.origin.pk).update(_path=self.pk) + def get_path(self): + """ + Return the path as a list of prefetched objects. + """ + # Compile a list of IDs to prefetch for each type of model in the path + to_prefetch = defaultdict(list) + for node in self.path: + ct_id, object_id = decompile_path_node(node) + to_prefetch[ct_id].append(object_id) + + # Prefetch path objects using one query per model type. Prefetch related devices where appropriate. + prefetched = {} + for ct_id, object_ids in to_prefetch.items(): + model_class = ContentType.objects.get_for_id(ct_id).model_class() + queryset = model_class.objects.filter(pk__in=object_ids) + if hasattr(model_class, 'device'): + queryset = queryset.prefetch_related('device') + prefetched[ct_id] = { + obj.id: obj for obj in queryset + } + + # Replicate the path using the prefetched objects. + path = [] + for node in self.path: + ct_id, object_id = decompile_path_node(node) + path.append(prefetched[ct_id][object_id]) + + return path + def get_total_length(self): """ Return the sum of the length of each cable in the path. diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 8f071a3c9..3bc8ebd1e 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -11,7 +11,6 @@ from taggit.managers import TaggableManager from dcim.choices import * from dcim.constants import * from dcim.fields import MACAddressField -from dcim.utils import path_node_to_object from extras.models import ObjectChange, TaggedItem from extras.utils import extras_features from utilities.fields import NaturalOrderingField @@ -172,7 +171,7 @@ class PathEndpoint(models.Model): return [] # Construct the complete path - path = [self, *[path_node_to_object(obj) for obj in self._path.path]] + path = [self, *self._path.get_path()] while (len(path) + 1) % 3: # Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort) path.append(None) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index b82dd58d2..3d5a50b32 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -21,15 +21,6 @@ def object_to_path_node(obj): return compile_path_node(ct.pk, obj.pk) -def path_node_to_object(repr): - """ - Given a path node representation, return the corresponding object. - """ - ct_id, object_id = decompile_path_node(repr) - model_class = ContentType.objects.get(pk=ct_id).model_class() - return model_class.objects.get(pk=int(object_id)) - - def trace_path(node): from .models import FrontPort, RearPort diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 27888824f..b48d6f8c1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2049,7 +2049,7 @@ class PathTraceView(ObjectView): return render(request, 'dcim/cable_trace.html', { 'obj': obj, - 'path': path, + 'path': obj.trace(), 'related_paths': related_paths, 'total_length': path.get_total_length(), }) diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index a328f2052..7e5622f07 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -8,7 +8,7 @@ {% block content %}
- {% for near_end, cable, far_end in path.origin.trace %} + {% for near_end, cable, far_end in path %} {# Near end #} {% if near_end.device %} @@ -50,6 +50,7 @@

Trace completed!

+
Total segments: {{ path|length }}
{% if total_length %}
Total length: {{ total_length|floatformat:"-2" }} Meters
{% endif %}