Coverage for audoma/drf/serializers.py: 62%

1 from typing import (

2 Any,

3 List,

4 Tuple,

5 Type,

6 Union,

7)

8 from uuid import UUID

9

10# TODO - fix this, this does not make any sense

11 from rest_framework import serializers

12 from rest_framework.serializers import * # noqa: F403, F401

13

14 from django.db import models

15 from django.db.models import QuerySet

16

17 from audoma import settings

18 from audoma.django.db import models as audoma_models

19

20

21 try: # pragma: no cover

22 from django.db.models import JSONField as ModelJSONField

23 except 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

30

31

32 from 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)

67

68

69 embeded_serializer_classes = {}

70

71

72 class 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

78

79

80# TODO - many param does nothing, this should be fixed

81 def 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.

86

87 Args:

88 * SerializerClass - serializer class which result should be wrapped

89

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"

98

99 class ManyResultSerializer(serializers.Serializer):

100 results = SerializerClass(many=True)

101

102 def __init__(self, instance: Any = None, **kwargs) -> None:

103 instance = Result(instance, many=True)

104 super().__init__(instance=instance, **kwargs)

105

106 class ResultSerializer(serializers.Serializer):

107 result = SerializerClass()

108

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

116

117 def __init__(self, instance: Any = None, **kwargs) -> None:

118 instance = Result(instance)

119 super().__init__(instance=instance, **kwargs)

120

121 ResultSerializer.__name__ = class_name

122 embeded_serializer_classes[SerializerClass] = ResultSerializer

123 return embeded_serializer_classes[SerializerClass]

124

125

126 class ResultSerializerClassMixin:

127 """

128 Allows to define wrap for serializer result.

129 """

130

131 _wrap_result_serializer = settings.WRAP_RESULT_SERIALIZER

132

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

140

141

142 class 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 """

148

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

183

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

196

197

198 class Serializer(

199 ResultSerializerClassMixin, serializers.Serializer

200): # pragma: no cover

201 pass

202

203

204 class 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()))

210

211 def to_representation(self, value: Any) -> Any:

212 # serializer_field.parentu

213 return self.original_choices.get(value, value)

214

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)

220

221

222 class ListSerializer(

223 ResultSerializerClassMixin, serializers.ListSerializer

224): # pragma: no cover

225 pass

226

227

228 class BulkSerializerMixin:

229 @property

230 def id_attr(self):

231 return getattr(self.Meta, "id_field", "id")

232

233 @property

234 def id_lookup_field(self):

235 return self.fields.get(self.id_attr).source or self.id_attr

236

237 def validate(self, data):

238 pk_field_name = getattr(self.Meta, "id_field_db_field_name", "id")

239

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)

251

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 )

258

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

262

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)

272

273 ret[id_attr] = id_value

274

275 return ret

276

277

278 class BulkListSerializer(ListSerializer):

279 id_field = "id"

280

281 @property

282 def id_attr(self):

283 return getattr(self.child.Meta, "id_field", "id")

284

285 @property

286 def id_lookup_field(self):

287 return self.child.fields.get(self.id_attr).source or self.id_attr

288

289 def data_by_id(self, data):

290 return {i.pop(self.id_attr): i for i in data}

291

292 def objects_to_update(self, queryset, data):

293 return queryset.filter(

294 **{

295 "{}__in".format(self.id_lookup_field): data.keys(),

296 }

297 )

298

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)

302

303 objects_to_update = self.objects_to_update(queryset, all_validated_data_by_id)

304

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))

313

314 return updated_objects