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