Coverage for dcim/api/serializers_/base.py: 40%
61 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from drf_spectacular.types import OpenApiTypes
2from drf_spectacular.utils import extend_schema_field
3from rest_framework import serializers
5from dcim.models import FrontPort, FrontPortTemplate, PortMapping, PortTemplateMapping, RearPort, RearPortTemplate
6from dcim.utils import reconcile_port_mappings
7from utilities.api import get_serializer_for_model
9__all__ = (
10 'ConnectedEndpointsSerializer',
11 'PortSerializer',
12)
15class ConnectedEndpointsSerializer(serializers.ModelSerializer):
16 """
17 Legacy serializer for pre-v3.3 connections
18 """
19 connected_endpoints_type = serializers.SerializerMethodField(read_only=True, allow_null=True)
20 connected_endpoints = serializers.SerializerMethodField(read_only=True)
21 connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
23 @extend_schema_field(OpenApiTypes.STR)
24 def get_connected_endpoints_type(self, obj):
25 if endpoints := obj.connected_endpoints: 25 ↛ 26line 25 didn't jump to line 26 because the condition on line 25 was never true
26 return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
27 return None
29 @extend_schema_field(serializers.ListField(allow_null=True))
30 def get_connected_endpoints(self, obj):
31 """
32 Return the appropriate serializer for the type of connected object.
33 """
34 if endpoints := obj.connected_endpoints: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 serializer = get_serializer_for_model(endpoints[0])
36 context = {'request': self.context['request']}
37 return serializer(endpoints, nested=True, many=True, context=context).data
38 return None
40 @extend_schema_field(serializers.BooleanField)
41 def get_connected_endpoints_reachable(self, obj):
42 """
43 Return whether the connected endpoints are reachable via a complete, active cable path.
44 """
45 # Use the public `path` accessor rather than dereferencing `_path`
46 # directly. `path` already handles the stale in-memory relation case
47 # that can occur while CablePath rows are rebuilt during cable edits.
48 if path := obj.path: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 return path.is_complete and path.is_active
50 return False
53class PortSerializer(serializers.ModelSerializer):
54 """
55 Base serializer for front & rear port and port templates.
56 """
57 @property
58 def _mapper(self):
59 """
60 Return the model and ForeignKey field name used to track port mappings for this model.
61 """
62 if self.Meta.model is FrontPort:
63 return PortMapping, 'front_port'
64 if self.Meta.model is RearPort:
65 return PortMapping, 'rear_port'
66 if self.Meta.model is FrontPortTemplate:
67 return PortTemplateMapping, 'front_port'
68 if self.Meta.model is RearPortTemplate:
69 return PortTemplateMapping, 'rear_port'
70 raise ValueError(f"Could not determine mapping details for {self.__class__}")
72 def _reconcile_mappings(self, instance, mappings):
73 mapping_model, fk_name = self._mapper
74 other_field = 'rear_port' if fk_name == 'front_port' else 'front_port'
76 # Normalize the opposite-port FK from a model instance to its id so the mappings can be
77 # reconciled by value.
78 desired = []
79 for attrs in mappings:
80 attrs = dict(attrs)
81 if other_field in attrs:
82 attrs[f'{other_field}_id'] = attrs.pop(other_field).pk
83 desired.append(attrs)
85 reconcile_port_mappings(mapping_model, parent_field=fk_name, parent=instance, desired=desired)
87 def create(self, validated_data):
88 mappings = validated_data.pop('mappings', [])
89 instance = super().create(validated_data)
90 self._reconcile_mappings(instance, mappings)
92 return instance
94 def update(self, instance, validated_data):
95 mappings = validated_data.pop('mappings', None)
96 instance = super().update(instance, validated_data)
98 # Only reconcile when the client supplied rear_ports; a PATCH that omits it leaves the
99 # existing mappings untouched.
100 if mappings is not None:
101 self._reconcile_mappings(instance, mappings)
103 return instance