Skip to content

Commit 69d67b3

Browse files
authored
Merge pull request #15 from 4teamwork/jch/TI-2893
Fix listing for MappingSerializer
2 parents a892fc1 + 11f132a commit 69d67b3

File tree

4 files changed

+259
-56
lines changed

4 files changed

+259
-56
lines changed

app/tests/test_mapping_serializer.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,50 @@ def test_mapping_serializer_update(self) -> None:
252252
self.assertEqual(2, instance.addresses.count())
253253
self.assertEqual(person, instance.addresses.first().target)
254254
self.assertEqual(person, instance.addresses.last().target)
255+
256+
@override_config(MODEL_MAPPING_FIELD=MODEL_MAPPING_FIELD)
257+
def test_list_mapping_serializer_create(self) -> None:
258+
koeniz = ElectionDistrictFactory(title="Koeniz")
259+
muri = ElectionDistrictFactory(title="Muri")
260+
261+
data = [
262+
{
263+
"external_firstname": "Hugo",
264+
"external_lastname": "Boss",
265+
"external_election_district_title": "Koeniz",
266+
"external_addresses": [
267+
self.address_1.external_uid,
268+
self.address_2.external_uid,
269+
],
270+
},
271+
{
272+
"external_firstname": "Stefanie",
273+
"external_lastname": "Muster",
274+
"external_election_district_title": "Muri",
275+
"external_addresses": [
276+
self.address_3.external_uid,
277+
],
278+
},
279+
]
280+
281+
serializer = PersonMappingSerializer(data=data, many=True)
282+
self.assertTrue(serializer.is_valid(raise_exception=True))
283+
serializer.save()
284+
285+
hugo = Person.objects.get(firstname="Hugo")
286+
stefanie = Person.objects.get(firstname="Stefanie")
287+
288+
self.assertEqual("Hugo", hugo.firstname)
289+
self.assertEqual("Boss", hugo.lastname)
290+
291+
self.assertEqual("Stefanie", stefanie.firstname)
292+
self.assertEqual("Muster", stefanie.lastname)
293+
294+
self.assertEqual(2, ElectionDistrict.objects.count())
295+
self.assertEqual(koeniz, hugo.election_district)
296+
self.assertEqual(muri, stefanie.election_district)
297+
298+
self.assertEqual(2, hugo.addresses.count())
299+
self.assertEqual(1, stefanie.addresses.count())
300+
self.assertEqual(hugo, hugo.addresses.first().target)
301+
self.assertEqual(stefanie, stefanie.addresses.last().target)

app/tests/test_mapping_serializer_data.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,86 @@ def test_mapping_serializer_map_initial_data(self) -> None:
7070

7171
mapped_data = TestMappingSerializer().map_data(data)
7272
self.assertEqual(mapped_data, expected_data)
73+
74+
def test_list_mapping_serializer_map_initial_data(self) -> None:
75+
data = [
76+
{
77+
"external_base_field": "base_value",
78+
"external_single_field_1": "nested_value_1",
79+
"external_single_field_2": "nested_value_2",
80+
"external_dict_field": {"nested_field": "single_value"},
81+
"external_object_field": {
82+
"nested_external_field_1": "nested_value_1",
83+
"nested_external_field_2": "nested_value_2",
84+
},
85+
"external_object_field_with_object": {
86+
"external_object_field_1": {
87+
"external_field_1": "value_1",
88+
"external_field_2": "value_2",
89+
},
90+
"external_object_field_2": {
91+
"external_field_1": "value_1",
92+
"external_field_2": "value_2",
93+
},
94+
},
95+
},
96+
{
97+
"external_base_field": "other_value",
98+
"external_single_field_1": "nested_value_3",
99+
"external_single_field_2": "nested_value_4",
100+
"external_dict_field": {"nested_field": "other_value"},
101+
"external_object_field": {
102+
"nested_external_field_1": "nested_value_3",
103+
"nested_external_field_2": "nested_value_4",
104+
},
105+
"external_object_field_with_object": {
106+
"external_object_field_1": {
107+
"external_field_1": "value_3",
108+
"external_field_2": "value_4",
109+
},
110+
"external_object_field_2": {
111+
"external_field_1": "value_3",
112+
"external_field_2": "value_4",
113+
},
114+
},
115+
},
116+
]
117+
118+
expected_data = [
119+
{
120+
"base_field": "base_value",
121+
"dict_field": {
122+
"nested_field_1": "nested_value_1",
123+
"nested_field_2": "nested_value_2",
124+
},
125+
"single_field": "single_value",
126+
"object_field": {
127+
"nested_field_1": "nested_value_1",
128+
"nested_field_2": "nested_value_2",
129+
},
130+
"object_field_with_object": {
131+
"object_field_1": {"field_1": "value_1", "field_2": "value_2"},
132+
"object_field_2": {"field_1": "value_1", "field_2": "value_2"},
133+
},
134+
},
135+
{
136+
"base_field": "other_value",
137+
"dict_field": {
138+
"nested_field_1": "nested_value_3",
139+
"nested_field_2": "nested_value_4",
140+
},
141+
"single_field": "other_value",
142+
"object_field": {
143+
"nested_field_1": "nested_value_3",
144+
"nested_field_2": "nested_value_4",
145+
},
146+
"object_field_with_object": {
147+
"object_field_1": {"field_1": "value_3", "field_2": "value_4"},
148+
"object_field_2": {"field_1": "value_3", "field_2": "value_4"},
149+
},
150+
},
151+
]
152+
153+
serializer = TestMappingSerializer(many=True)
154+
mapped_data = serializer.map_list_data(data)
155+
self.assertEqual(mapped_data, expected_data)

changes/TI-2893.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix MappingSerializer if many is true. [TI-2893](https://4teamwork.atlassian.net/browse/TI-2893>)

django_features/serializers.py

Lines changed: 128 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,74 @@
55
from django.core.exceptions import ValidationError
66
from django.db import models
77
from django.db.models import NOT_PROVIDED
8+
from rest_framework import serializers
89
from rest_framework.fields import empty
910
from rest_framework.relations import ManyRelatedField
1011

1112
from django_features.custom_fields.serializers import CustomFieldBaseModelSerializer
1213
from django_features.fields import UUIDRelatedField
1314

1415

15-
class BaseMappingSerializer(CustomFieldBaseModelSerializer):
16+
class PropertySerializer(serializers.Serializer):
1617
relation_separator: str = "."
18+
19+
class Meta:
20+
abstract = True
21+
fields = "__all__"
22+
model = None
23+
24+
@property
25+
def mapping(self) -> dict[str, dict[str, Any]]:
26+
if getattr(self, "_mapping") is None:
27+
raise ValueError(
28+
"Property 'mapping' on instance must be set and can't be 'None'"
29+
)
30+
return self._mapping
31+
32+
@mapping.setter
33+
def mapping(self, value: dict[str, dict[str, Any]]) -> None:
34+
self._mapping = value
35+
36+
@property
37+
def mapping_fields(self) -> list[str]:
38+
mapping_fields = getattr(
39+
self, "_mapping_fields", list(self.model_mapping.values())
40+
)
41+
if mapping_fields is None:
42+
raise ValueError("Property 'mapping_fields' must be set and can't be 'None")
43+
return mapping_fields
44+
45+
@mapping_fields.setter
46+
def mapping_fields(self, value: list[str]) -> None:
47+
self._mapping_fields = value
48+
49+
@property
50+
def model_mapping(self) -> dict[str, Any]:
51+
for key_path in self.mapping.keys():
52+
key = key_path.split(self.relation_separator)[-1]
53+
if key.lower() == self.model.__name__.lower():
54+
return self.mapping.get(key_path, {})
55+
return {}
56+
57+
@model_mapping.setter
58+
def model_mapping(self, value: dict[str, Any]) -> None:
59+
self._model_mapping = value
60+
61+
@property
62+
def model(self) -> models.Model:
63+
model = getattr(self, "_model", self.Meta.model)
64+
if model is None:
65+
raise ValueError(
66+
"Property 'model' must be set and can't be 'None. Default is 'Meta.model"
67+
)
68+
return model
69+
70+
@model.setter
71+
def model(self, value: models.Model) -> None:
72+
self._model = value
73+
74+
75+
class BaseMappingSerializer(CustomFieldBaseModelSerializer, PropertySerializer):
1776
serializer_related_field = UUIDRelatedField
1877
serializer_related_fields: dict[str, Any] = {}
1978

@@ -32,26 +91,11 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
3291
self.exclude: list[str] = []
3392
self.related_fields: set[str] = set()
3493

35-
@property
36-
def mapping(self) -> dict[str, dict[str, Any]]:
37-
raise NotImplementedError("Mapping must be set")
38-
39-
@property
40-
def mapping_fields(self) -> list[str]:
41-
raise NotImplementedError("Mapping fields must be set")
42-
43-
@property
44-
def model(self) -> models.Model:
45-
if self.Meta.model is None:
46-
raise ValueError("Meta.model must be set")
47-
return self.Meta.model
48-
4994
def get_fields(self) -> dict[str, Any]:
5095
initial_fields = super().get_fields()
5196
fields: dict[str, Any] = dict()
5297
nested_fields: dict[str, Any] = dict()
5398
nested_field_fields: dict[str, list[str]] = dict()
54-
self.related_fields: set[str] = set()
5599
for internal_name in self.mapping_fields:
56100
if internal_name in self.exclude:
57101
continue
@@ -160,40 +204,16 @@ def __init__(
160204
**kwargs: Any,
161205
) -> None:
162206
self.exclude = exclude
163-
self.nested_fields = nested_fields
164-
self.parent_mapping = parent_mapping
207+
self.mapping_fields = nested_fields
208+
self.mapping = parent_mapping
165209
self.Meta.model = field.related_model
166210
super().__init__(*args, **kwargs)
167211

168-
@property
169-
def mapping(self) -> dict[str, dict[str, Any]]:
170-
return self.parent_mapping
171212

172-
@property
173-
def mapping_fields(self) -> list[str]:
174-
return self.nested_fields
175-
176-
177-
class MappingSerializer(BaseMappingSerializer):
213+
class DataMappingSerializer(PropertySerializer):
178214
_default_prefix = "default"
179215
_format_prefix = "format"
180216

181-
class Meta:
182-
abstract = True
183-
fields = "__all__"
184-
model = None
185-
186-
def __init__(
187-
self,
188-
instance: Any = None,
189-
data: Any = empty,
190-
**kwargs: Any,
191-
) -> None:
192-
self.instance = instance
193-
self.unmapped_data = data
194-
mapped_data = self.map_data(data)
195-
super().__init__(instance, data=mapped_data, **kwargs)
196-
197217
def _get_nested_data(self, field_path: list[str], data: Any) -> tuple[Any, bool]:
198218
field_name = field_path[0]
199219
if not isinstance(data, dict):
@@ -248,17 +268,69 @@ def map_data(self, initial_data: Any) -> Any:
248268
)
249269
return data
250270

251-
@property
252-
def mapping_fields(self) -> list[str]:
253-
return list(self.model_mapping.values())
254271

255-
@property
256-
def model_mapping(self) -> dict[str, Any]:
257-
mapping = getattr(self, "mapping", None)
258-
if mapping is None:
259-
raise ValueError("Mapping must be set")
260-
for key_path in mapping.keys():
261-
key = key_path.split(self.relation_separator)[-1]
262-
if key.lower() == self.model.__name__.lower():
263-
return mapping.get(key_path, {})
264-
return {}
272+
class ListDataMappingSerializer(serializers.ListSerializer, DataMappingSerializer):
273+
def __init__(self, data: Any = empty, *args: Any, **kwargs: Any) -> None:
274+
self.instance = None
275+
self.mapping = kwargs.pop("mapping", {})
276+
self.model = kwargs.pop("model")
277+
self.unmapped_data = data if data is not empty else []
278+
mapped_data = self.map_list_data(self.unmapped_data)
279+
super().__init__(data=mapped_data, *args, **kwargs)
280+
281+
def map_list_data(self, initial_data: Any) -> list[Any]:
282+
list_data: list[dict[str, Any]] = []
283+
for item in initial_data:
284+
list_data.append(self.map_data(item))
285+
return list_data
286+
287+
288+
class MappingSerializer(BaseMappingSerializer, DataMappingSerializer):
289+
list_serializer_class = ListDataMappingSerializer
290+
291+
class Meta:
292+
abstract = True
293+
fields = "__all__"
294+
model = None
295+
296+
def __init__(
297+
self,
298+
instance: Any = None,
299+
data: Any = empty,
300+
**kwargs: Any,
301+
) -> None:
302+
self.instance = instance
303+
self.unmapped_data = data
304+
mapped_data = self.map_data(data)
305+
super().__init__(instance, data=mapped_data, **kwargs)
306+
307+
@classmethod
308+
def many_init(cls, *args: Any, **kwargs: Any) -> ListDataMappingSerializer:
309+
"""
310+
Overwrite the many_init function from the ModelSerializer to change the default listing serializer to the given
311+
list_serializer_class attribute instead of the default ListSerializer. Therefore, the list serializer class can
312+
be set with the attribute list_serializer_class on the serializer class instead of the Meta class.
313+
"""
314+
315+
list_kwargs = {}
316+
for key in serializers.LIST_SERIALIZER_KWARGS_REMOVE:
317+
value = kwargs.pop(key, None)
318+
if value is not None:
319+
list_kwargs[key] = value
320+
child = cls(*args, **kwargs)
321+
list_kwargs["child"] = child
322+
list_kwargs["mapping"] = getattr(child, "mapping", {})
323+
list_kwargs.update(
324+
{
325+
key: value
326+
for key, value in kwargs.items()
327+
if key in serializers.LIST_SERIALIZER_KWARGS
328+
}
329+
)
330+
meta = getattr(cls, "Meta", None)
331+
list_serializer_class = getattr(
332+
meta, "list_serializer_class", cls.list_serializer_class
333+
)
334+
model = getattr(meta, "model", None)
335+
list_kwargs["model"] = model
336+
return list_serializer_class(*args, **list_kwargs)

0 commit comments

Comments
 (0)