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

1from typing import ( 

2 Any, 

3 List, 

4 Tuple, 

5 Type, 

6 Union, 

7) 

8from uuid import UUID 

9 

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

11from rest_framework import serializers 

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

13 

14from django.db import models 

15from django.db.models import QuerySet 

16 

17from audoma import settings 

18from audoma.django.db import models as audoma_models 

19 

20 

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 

30 

31 

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) 

67 

68 

69embeded_serializer_classes = {} 

70 

71 

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 

78 

79 

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. 

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 

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

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

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 

198class Serializer( 

199 ResultSerializerClassMixin, serializers.Serializer 

200): # pragma: no cover 

201 pass 

202 

203 

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

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 

222class ListSerializer( 

223 ResultSerializerClassMixin, serializers.ListSerializer 

224): # pragma: no cover 

225 pass 

226 

227 

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

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