Coverage for netbox/api/serializers/base.py: 100%

68 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from functools import cached_property 

2 

3from drf_spectacular.types import OpenApiTypes 

4from drf_spectacular.utils import extend_schema_field 

5from rest_framework import serializers 

6 

7from netbox.api.fields import RelatedObjectCountField 

8from utilities.api import get_related_object_by_attrs 

9 

10from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField 

11 

12__all__ = ( 

13 'BaseModelSerializer', 

14 'ValidatedModelSerializer', 

15) 

16 

17 

18class BaseModelSerializer(serializers.ModelSerializer): 

19 url = NetBoxAPIHyperlinkedIdentityField() 

20 display_url = NetBoxURLHyperlinkedIdentityField() 

21 display = serializers.SerializerMethodField(read_only=True) 

22 

23 def __init__(self, *args, nested=False, fields=None, omit=None, **kwargs): 

24 """ 

25 Extends the base __init__() method to support dynamic fields. 

26 

27 :param nested: Set to True if this serializer is being employed within a parent serializer 

28 :param fields: An iterable of fields to include when rendering the serialized object, If nested is 

29 True but no fields are specified, Meta.brief_fields will be used. 

30 :param omit: An iterable of fields to omit from the serialized object 

31 """ 

32 self.nested = nested 

33 self._include_fields = fields or [] 

34 self._omit_fields = omit or [] 

35 

36 # Disable validators for nested objects (which already exist) 

37 if self.nested: 

38 self.validators = [] 

39 

40 # If this serializer is nested but no fields have been specified, 

41 # default to using Meta.brief_fields (if set) 

42 if self.nested and not fields and not omit: 

43 self._include_fields = getattr(self.Meta, 'brief_fields', None) 

44 

45 super().__init__(*args, **kwargs) 

46 

47 def to_internal_value(self, data): 

48 

49 # If initialized as a nested serializer, we should expect to receive the attrs or PK 

50 # identifying a related object. 

51 if self.nested: 

52 queryset = self.Meta.model.objects.all() 

53 request = self.context.get('request') 

54 user = request.user if request else None 

55 return get_related_object_by_attrs(queryset, data, user=user) 

56 

57 return super().to_internal_value(data) 

58 

59 @cached_property 

60 def fields(self): 

61 """ 

62 Override the fields property to return only specifically requested fields if needed. 

63 """ 

64 fields = super().fields 

65 

66 # Include only requested fields 

67 if self._include_fields: 

68 for field_name in set(fields) - set(self._include_fields): 

69 fields.pop(field_name, None) 

70 

71 # Remove omitted fields 

72 for field_name in set(self._omit_fields): 

73 fields.pop(field_name, None) 

74 

75 # Related object counts are populated by annotations applied to the viewset's queryset, but these 

76 # annotations are not applied when the object is represented as a nested (brief) related object. Omit 

77 # these fields when serializing a nested object to avoid advertising fields that will never be populated 

78 # (and which would otherwise be declared as required in the generated OpenAPI schema). See #22154. 

79 if self.nested: 

80 for field_name, field in list(fields.items()): 

81 if isinstance(field, RelatedObjectCountField): 

82 fields.pop(field_name) 

83 

84 return fields 

85 

86 @extend_schema_field(OpenApiTypes.STR) 

87 def get_display(self, obj): 

88 return str(obj) 

89 

90 

91class ValidatedModelSerializer(BaseModelSerializer): 

92 """ 

93 Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during 

94 validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144) 

95 

96 Serializers may declare `model_clean_fields` in Meta, naming scalar model fields whose clean()-normalized values 

97 must reach the database. DRF builds a fresh instance from the validated data on create, discarding what clean() 

98 wrote. 

99 """ 

100 

101 # Bypass DRF's built-in validation of unique constraints due to DRF bug #9410. Rely instead 

102 # on our own custom model validation (below). 

103 def get_unique_together_constraints(self, model): 

104 return [] 

105 

106 def validate(self, data): 

107 

108 # Skip validation if we're being used to represent a nested object 

109 if self.nested: 

110 return data 

111 

112 attrs = data.copy() 

113 

114 # Skip ManyToManyFields 

115 opts = self.Meta.model._meta 

116 m2m_values = {} 

117 for field in [*opts.local_many_to_many, *opts.related_objects]: 

118 if field.name in attrs: 

119 m2m_values[field.name] = attrs.pop(field.name) 

120 

121 # Run clean() on an instance of the model 

122 if self.instance is None: 

123 instance = self.Meta.model(**attrs) 

124 else: 

125 instance = self.instance 

126 for k, v in attrs.items(): 

127 setattr(instance, k, v) 

128 instance._m2m_values = m2m_values 

129 # Skip uniqueness validation of individual fields inside `full_clean()` (this is handled by the serializer) 

130 instance.full_clean(validate_unique=False) 

131 

132 # Preserve any normalization performed by model.clean() (e.g. stale custom field pruning) 

133 if 'custom_field_data' in attrs: 

134 data['custom_field_data'] = instance.custom_field_data 

135 

136 for field_name in getattr(self.Meta, 'model_clean_fields', ()): 

137 data[field_name] = getattr(instance, field_name) 

138 

139 return data