Coverage for audoma/drf/serializers.py: 62%
112 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-12 12:52 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-12 12:52 +0000
1from typing import (
2 Any,
3 List,
4 Tuple,
5 Type,
6 Union,
7)
8from uuid import UUID
10# TODO - fix this, this does not make any sense
11from rest_framework import serializers
12from rest_framework.serializers import * # noqa: F403, F401
14from django.db import models
15from django.db.models import QuerySet
17from audoma import settings
18from audoma.django.db import models as audoma_models
21try: # pragma: no cover
22 from django.db.models import JSONField as ModelJSONField
23except ImportError:
24 try:
25 from jsonfield import JSONField as ModelJSONField
26 except ImportError as err:
27 raise ImportError(
28 "You are using old version of Django that doesn't support JSONField. Please install django-jsonfield"
29 ) from err
32from audoma.drf.fields import ( # NOQA # isort:skip # pragma: no cover
33 BooleanField,
34 CharField,
35 ChoiceField,
36 DateField,
37 DateTimeField,
38 DecimalField,
39 DictField,
40 DurationField,
41 EmailField,
42 Field,
43 FileField,
44 FilePathField,
45 FloatField,
46 HiddenField,
47 HStoreField,
48 IPAddressField,
49 ImageField,
50 IntegerField,
51 JSONField,
52 ListField,
53 ModelField,
54 MultipleChoiceField,
55 NullBooleanField,
56 ReadOnlyField,
57 RegexField,
58 SerializerMethodField,
59 SlugField,
60 TimeField,
61 URLField,
62 UUIDField,
63 MACAddressField,
64 PhoneNumberField,
65 MoneyField,
66)
69embeded_serializer_classes = {}
72class Result: # pragma no cover
73 def __init__(self, result: Any, many: bool = False) -> Any:
74 if many:
75 self.results = result
76 else:
77 self.result = result
80# TODO - many param does nothing, this should be fixed
81def result_serializer_class(
82 SerializerClass: Type[serializers.BaseSerializer], many: bool = False
83) -> Type[serializers.BaseSerializer]:
84 """
85 Helper function which wraps the serializer result if necessary.
87 Args:
88 * SerializerClass - serializer class which result should be wrapped
90 Returns: ResultSerializer class
91 """
92 if SerializerClass not in embeded_serializer_classes:
93 class_name = SerializerClass.__name__
94 if class_name.endswith("Serializer"):
95 class_name = class_name[:-10] + "ResultSerializer"
96 else:
97 class_name += "Result"
99 class ManyResultSerializer(serializers.Serializer):
100 results = SerializerClass(many=True)
102 def __init__(self, instance: Any = None, **kwargs) -> None:
103 instance = Result(instance, many=True)
104 super().__init__(instance=instance, **kwargs)
106 class ResultSerializer(serializers.Serializer):
107 result = SerializerClass()
109 def __new__(cls, *args, **kwargs) -> Serializer:
110 _many = kwargs.pop("many", False)
111 if _many:
112 instance = ManyResultSerializer(*args, **kwargs)
113 else:
114 instance = super().__new__(cls, *args, **kwargs)
115 return instance
117 def __init__(self, instance: Any = None, **kwargs) -> None:
118 instance = Result(instance)
119 super().__init__(instance=instance, **kwargs)
121 ResultSerializer.__name__ = class_name
122 embeded_serializer_classes[SerializerClass] = ResultSerializer
123 return embeded_serializer_classes[SerializerClass]
126class ResultSerializerClassMixin:
127 """
128 Allows to define wrap for serializer result.
129 """
131 _wrap_result_serializer = settings.WRAP_RESULT_SERIALIZER
133 @classmethod
134 def get_result_serializer_class(
135 cls, many: bool = False
136 ) -> Type[serializers.BaseSerializer]:
137 if cls._wrap_result_serializer:
138 return result_serializer_class(cls, many=many)
139 return cls
142class ModelSerializer(ResultSerializerClassMixin, serializers.ModelSerializer):
143 """
144 Extends default ModelSerializer,
145 modifies serializer_field_mapping (replaces some fields with audoma fields).
146 Adds support for generating audoma example for field.
147 """
149 serializer_field_mapping = {
150 models.AutoField: IntegerField,
151 models.BigIntegerField: IntegerField,
152 models.BooleanField: BooleanField,
153 models.CharField: CharField,
154 models.CommaSeparatedIntegerField: CharField,
155 models.DateField: DateField,
156 models.DateTimeField: DateTimeField,
157 models.DecimalField: DecimalField,
158 models.DurationField: DurationField,
159 models.EmailField: EmailField,
160 models.Field: ModelField,
161 models.FileField: FileField,
162 models.FloatField: FloatField,
163 models.ImageField: ImageField,
164 models.IntegerField: IntegerField,
165 models.NullBooleanField: NullBooleanField,
166 models.PositiveIntegerField: IntegerField,
167 models.PositiveSmallIntegerField: IntegerField,
168 models.SlugField: SlugField,
169 models.SmallIntegerField: IntegerField,
170 models.TextField: CharField,
171 models.TimeField: TimeField,
172 models.URLField: URLField,
173 models.GenericIPAddressField: IPAddressField,
174 models.FilePathField: FilePathField,
175 models.UUIDField: UUIDField,
176 audoma_models.PhoneNumberField: PhoneNumberField,
177 audoma_models.MACAddressField: MACAddressField,
178 audoma_models.MoneyField: MoneyField,
179 audoma_models.CurrencyField: CharField,
180 ModelJSONField: JSONField,
181 } # pragma: no cover
182 serializer_choice_field = ChoiceField # pragma: no cover
184 def build_standard_field(
185 self, field_name, model_field
186 ) -> Tuple[Union[Type[Field], dict]]:
187 """
188 Adds support for mapping example from model fields to model serializer fields.
189 """
190 field_class, field_kwargs = super().build_standard_field(
191 field_name, model_field
192 )
193 if hasattr(model_field, "example") and model_field.example:
194 field_kwargs["example"] = model_field.example
195 return field_class, field_kwargs
198class Serializer(
199 ResultSerializerClassMixin, serializers.Serializer
200): # pragma: no cover
201 pass
204class DisplayNameWritableField(serializers.ChoiceField):
205 def __init__(self, *args, **kwargs) -> None:
206 super().__init__(*args, **kwargs)
207 self.choices_inverted_dict = dict((y, x) for x, y in list(self.choices.items()))
208 self.original_choices = self.choices
209 self.choices = dict((y, y) for x, y in list(self.original_choices.items()))
211 def to_representation(self, value: Any) -> Any:
212 # serializer_field.parentu
213 return self.original_choices.get(value, value)
215 def to_internal_value(self, data: str) -> dict:
216 try:
217 return self.choices_inverted_dict[data.title()]
218 except KeyError:
219 raise serializers.ValidationError('"%s" is not valid choice.' % data)
222class ListSerializer(
223 ResultSerializerClassMixin, serializers.ListSerializer
224): # pragma: no cover
225 pass
228class BulkSerializerMixin:
229 @property
230 def id_attr(self):
231 return getattr(self.Meta, "id_field", "id")
233 @property
234 def id_lookup_field(self):
235 return self.fields.get(self.id_attr).source or self.id_attr
237 def validate(self, data):
238 pk_field_name = getattr(self.Meta, "id_field_db_field_name", "id")
240 if self.instance is not None and isinstance(self.instance, QuerySet):
241 data_pk = data.get(self.id_attr)
242 existing_pks = [
243 str(x) if isinstance(x, UUID) else x
244 for x in self.instance.values_list(pk_field_name, flat=True)
245 ]
246 if data_pk not in existing_pks:
247 raise serializers.ValidationError(
248 {self.id_attr: "Record with given key does not exist."}
249 )
250 return super().validate(data)
252 def to_internal_value(self, data: dict) -> dict:
253 ret = super().to_internal_value(data)
254 id_attr = getattr(self.Meta, "id_field", "id")
255 request_method = getattr(
256 getattr(self.context.get("view"), "request"), "method", ""
257 )
259 # add id_field field back to validated data
260 # since super by default strips out read-only fields
261 # hence id will no longer be present in validated_data
263 if all(
264 (
265 isinstance(self.root, BulkListSerializer),
266 id_attr,
267 request_method in ("PUT", "PATCH"),
268 )
269 ):
270 id_field = self.fields[id_attr]
271 id_value = id_field.get_value(data)
273 ret[id_attr] = id_value
275 return ret
278class BulkListSerializer(ListSerializer):
279 id_field = "id"
281 @property
282 def id_attr(self):
283 return getattr(self.child.Meta, "id_field", "id")
285 @property
286 def id_lookup_field(self):
287 return self.child.fields.get(self.id_attr).source or self.id_attr
289 def data_by_id(self, data):
290 return {i.pop(self.id_attr): i for i in data}
292 def objects_to_update(self, queryset, data):
293 return queryset.filter(
294 **{
295 "{}__in".format(self.id_lookup_field): data.keys(),
296 }
297 )
299 def update(self, queryset: QuerySet, all_validated_data: List[dict]) -> List[Any]:
300 updated_objects = []
301 all_validated_data_by_id = self.data_by_id(all_validated_data)
303 objects_to_update = self.objects_to_update(queryset, all_validated_data_by_id)
305 for obj in objects_to_update:
306 obj_id = getattr(obj, self.id_attr)
307 if isinstance(obj_id, UUID):
308 obj_id = str(obj_id)
309 obj_validated_data = all_validated_data_by_id.get(obj_id)
310 # use model serializer to actually update the model
311 # in case that method is overwritten
312 updated_objects.append(self.child.update(obj, obj_validated_data))
314 return updated_objects