Merge pull request #22859 from netbox-community/22825-circuit-term-fielddoesnotexist
Fixes #22825: Handle CircuitTermination origins in cable path tracing
This commit is contained in:
commit
1f5a30dcd2
|
|
@ -780,18 +780,22 @@ class CablePath(models.Model):
|
|||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Record a direct reference to this CablePath on its originating object(s)
|
||||
# Record a direct reference to this CablePath on its originating object(s). Only PathEndpoint
|
||||
# subclasses carry the denormalized `_path` back-reference; other valid origins (e.g.
|
||||
# CircuitTermination) do not, so skip the update for them.
|
||||
origin_model = self.origin_type.model_class()
|
||||
origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
|
||||
origin_model.objects.filter(pk__in=origin_ids).update(_path=self.pk)
|
||||
if issubclass(origin_model, PathEndpoint):
|
||||
origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
|
||||
origin_model.objects.filter(pk__in=origin_ids).update(_path=self.pk)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Mirror save() - clear _path on origins to prevent stale references
|
||||
# in table views that render _path.destinations
|
||||
# in table views that render _path.destinations. Only PathEndpoint subclasses carry `_path`.
|
||||
if self.path:
|
||||
origin_model = self.origin_type.model_class()
|
||||
origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
|
||||
origin_model.objects.filter(pk__in=origin_ids, _path=self.pk).update(_path=None)
|
||||
if issubclass(origin_model, PathEndpoint):
|
||||
origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
|
||||
origin_model.objects.filter(pk__in=origin_ids, _path=self.pk).update(_path=None)
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -2680,6 +2680,45 @@ class LegacyCablePathTestCase(BaseCablePathTestCase):
|
|||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
|
||||
def test_225_circuittermination_origin_passive_network(self):
|
||||
"""
|
||||
[CT1] --C1-- [RP1] [FP1]
|
||||
|
||||
A CircuitTermination cabled into a passive (FrontPort/RearPort-only) device can become a
|
||||
CablePath origin. Unlike PathEndpoint origins, CircuitTermination has no `_path` back-reference
|
||||
field, so saving and deleting such a path must not attempt to write it (see #22825).
|
||||
"""
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||
PortMapping.objects.create(
|
||||
device=self.device, front_port=frontport1, front_port_position=1,
|
||||
rear_port=rearport1, rear_port_position=1,
|
||||
)
|
||||
circuittermination1 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit,
|
||||
termination=self.site,
|
||||
term_side='A'
|
||||
)
|
||||
cable1 = Cable(
|
||||
a_terminations=[circuittermination1],
|
||||
b_terminations=[rearport1]
|
||||
)
|
||||
cable1.save()
|
||||
|
||||
# Re-fetch so the in-memory instance reflects the cable set above (from_origin reads .cable).
|
||||
circuittermination1.refresh_from_db()
|
||||
|
||||
# A path traced from the CircuitTermination origin must save without raising FieldDoesNotExist
|
||||
# on the missing `_path` field.
|
||||
cablepath = CablePath.from_origin([circuittermination1])
|
||||
cablepath.save()
|
||||
self.assertEqual(cablepath.origin_type.model_class(), CircuitTermination)
|
||||
self.assertEqual(cablepath.origins, [circuittermination1])
|
||||
|
||||
# Deleting the path must likewise not attempt to clear a nonexistent `_path` field.
|
||||
cablepath.delete()
|
||||
self.assertIsNone(CablePath.objects.filter(pk=cablepath.pk).first())
|
||||
|
||||
def test_301_create_path_via_existing_cable(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
|
||||
|
|
|
|||
|
|
@ -208,9 +208,15 @@ class PathTraceView(generic.ObjectView):
|
|||
# Get the total length of the cable and whether the length is definitive (fully defined)
|
||||
total_length, is_definitive = path.get_total_length() if path else (None, False)
|
||||
|
||||
# Determine the path to the SVG trace image
|
||||
api_viewname = f"{path.origin_type.app_label}-api:{path.origin_type.model}-trace"
|
||||
svg_url = f"{reverse(api_viewname, kwargs={'pk': path.origins[0].pk})}?render=svg"
|
||||
# Determine the path to the SVG trace image. The `-trace` API action (and the SVG renderer,
|
||||
# which calls origin.trace()) exist only for PathEndpoint origins. Other valid origins such as
|
||||
# CircuitTermination have no such action, so omit the SVG for them.
|
||||
origin_model = path.origin_type.model_class()
|
||||
if issubclass(origin_model, PathEndpoint):
|
||||
api_viewname = f"{path.origin_type.app_label}-api:{path.origin_type.model}-trace"
|
||||
svg_url = f"{reverse(api_viewname, kwargs={'pk': path.origins[0].pk})}?render=svg"
|
||||
else:
|
||||
svg_url = None
|
||||
|
||||
return {
|
||||
'path': path,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
{# Cable trace SVG & options #}
|
||||
<div class="col col-md-5">
|
||||
{% if path %}
|
||||
{% if svg_url %}
|
||||
<div class="text-center my-3">
|
||||
<object data="{{ svg_url }}" class="rack_elevation"></object>
|
||||
<div>
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="trace-end">
|
||||
{% if path.is_split and path.get_asymmetric_nodes %}
|
||||
<h3 class="text-danger">{% trans "Asymmetric Path" %}!</h3>
|
||||
|
|
|
|||
Loading…
Reference in New Issue