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

1from typing import ( 

2 Any, 

3 List, 

4 Tuple, 

5 Type, 

6 Union, 

7) 

8from uuid import UUID 

9 

10from rest_framework import serializers 

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

12 

13from django.db import models 

14from django.db.models import QuerySet 

15 

16from audoma import settings 

17from audoma.django.db import models as audoma_models 

18 

19 

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 

29 

30 

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) 

66 

67 

68embeded_serializer_classes = {} 

69 

70 

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 

77 

78 

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. 

84 

85 Args: 

86 * SerializerClass - serializer class which result should be wrapped 

87 

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" 

96 

97 class ManyResultSerializer(serializers.Serializer): 

98 results = SerializerClass(many=True) 

99 

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

101 instance = Result(instance, many=True) 

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

103 

104 class ResultSerializer(serializers.Serializer): 

105 result = SerializerClass() 

106 

107 def __new__(cls, *args, **kwargs) -> Serializer: 

108 _many = kwargs.pop("many", False) 

109 

110 if _many: 

111 instance = ManyResultSerializer(*args, **kwargs) 

112 else: 

113 instance = super().__new__(cls, *args, **kwargs) 

114 return instance 

115 

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

117 instance = Result(instance) 

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

119 

120 ResultSerializer.__name__ = class_name 

121 embeded_serializer_classes[SerializerClass] = ResultSerializer 

122 return embeded_serializer_classes[SerializerClass] 

123 

124 

125class ResultSerializerClassMixin: 

126 """ 

127 Allows to define wrap for serializer result. 

128 """ 

129 

130 _wrap_result_serializer = settings.WRAP_RESULT_SERIALIZER 

131 

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 

139 

140 

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

147 

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 

182 

183 serializer_choice_field = ChoiceField 

184 

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 

197 

198 

199class Serializer(ResultSerializerClassMixin, serializers.Serializer): 

200 pass 

201 

202 

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

209 

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

211 # serializer_field.parentu 

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

213 

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) 

219 

220 

221class ListSerializer(ResultSerializerClassMixin, serializers.ListSerializer): 

222 pass 

223 

224 

225class BulkSerializerMixin: 

226 @property 

227 def id_attr(self): 

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

229 

230 @property 

231 def id_lookup_field(self): 

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

233 

234 def validate(self, data): 

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

236 

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) 

248 

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 ) 

255 

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 

259 

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) 

269 

270 ret[id_attr] = id_value 

271 

272 return ret 

273 

274 

275class BulkListSerializer(ListSerializer): 

276 id_field = "id" 

277 

278 @property 

279 def id_attr(self): 

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

281 

282 @property 

283 def id_lookup_field(self): 

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

285 

286 def data_by_id(self, data): 

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

288 

289 def objects_to_update(self, queryset, data): 

290 return queryset.filter( 

291 **{ 

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

293 } 

294 ) 

295 

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) 

299 

300 objects_to_update = self.objects_to_update(queryset, all_validated_data_by_id) 

301 

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

310 

311 return updated_objects