Skip to content

Commit 4aba1ec

Browse files
authored
Merge pull request #10 from 4teamwork/jch/TI-2893
Fix MappingSerializer
2 parents 5cc8a44 + 114fd07 commit 4aba1ec

File tree

16 files changed

+314
-80
lines changed

16 files changed

+314
-80
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Generated by Django 4.2.23 on 2025-10-23 05:26
2+
3+
import django.db.models.deletion
4+
import django_extensions.db.fields
5+
from django.db import migrations
6+
from django.db import models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("app", "0004_add_external_uid_field"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="ElectionDistrict",
18+
fields=[
19+
(
20+
"id",
21+
models.BigAutoField(
22+
auto_created=True,
23+
primary_key=True,
24+
serialize=False,
25+
verbose_name="ID",
26+
),
27+
),
28+
(
29+
"created",
30+
django_extensions.db.fields.CreationDateTimeField(
31+
auto_now_add=True, verbose_name="created"
32+
),
33+
),
34+
(
35+
"modified",
36+
django_extensions.db.fields.ModificationDateTimeField(
37+
auto_now=True, verbose_name="modified"
38+
),
39+
),
40+
("uid", models.UUIDField(unique=True, verbose_name="UUID")),
41+
("title", models.CharField(unique=True, verbose_name="Title")),
42+
("number", models.CharField(verbose_name="Number")),
43+
],
44+
options={
45+
"verbose_name": "Election district",
46+
"verbose_name_plural": "Election districts",
47+
"ordering": ("title",),
48+
},
49+
),
50+
migrations.AddField(
51+
model_name="person",
52+
name="election_district",
53+
field=models.ForeignKey(
54+
blank=True,
55+
null=True,
56+
on_delete=django.db.models.deletion.PROTECT,
57+
related_name="persons",
58+
to="app.electiondistrict",
59+
verbose_name="Election district",
60+
),
61+
),
62+
]

app/migrations/max_migration.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0004_add_external_uid_field
1+
0005_electiondistrict_person_election_district

app/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
__all__ = ["Address", "Municipality", "Person", "PersonType"]
1+
__all__ = ["Address", "ElectionDistrict", "Municipality", "Person", "PersonType"]
22

33
from .address import Address
4+
from .election_distinct import ElectionDistrict
45
from .municipality import Municipality
56
from .person import Person
67
from .person import PersonType

app/models/election_distinct.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.db import models
2+
from django_extensions.db.models import TimeStampedModel
3+
4+
5+
class ElectionDistrict(TimeStampedModel):
6+
uid = models.UUIDField(verbose_name="UUID", unique=True)
7+
title = models.CharField(verbose_name="Title", unique=True)
8+
number = models.CharField(verbose_name="Number")
9+
10+
class Meta:
11+
verbose_name = "Election district"
12+
verbose_name_plural = "Election districts"
13+
ordering = ("title",)
14+
15+
def __str__(self) -> str:
16+
return "{} - {}".format(self.number, self.title)

app/models/person.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ class Person(CustomFieldBaseModel):
3535
verbose_name="Place of residence",
3636
related_name="persons",
3737
)
38+
election_district = models.ForeignKey(
39+
"ElectionDistrict",
40+
on_delete=models.PROTECT,
41+
blank=True,
42+
null=True,
43+
verbose_name="Election district",
44+
related_name="persons",
45+
)
3846

3947
class Meta:
4048
verbose_name = "Person"

app/serializers/person.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from app.models import Person
66
from app.serializers import BaseMappingSerializer
77
from django_features.custom_fields.serializers import CustomFieldBaseModelSerializer
8+
from django_features.fields import ExternalUUIDRelatedField
9+
from django_features.fields import RelatedField
810

911

1012
class PersonSerializer(CustomFieldBaseModelSerializer):
@@ -18,6 +20,12 @@ class Meta:
1820

1921

2022
class PersonMappingSerializer(BaseMappingSerializer):
23+
serializer_related_fields = {"addresses": ExternalUUIDRelatedField}
24+
25+
election_district = RelatedField(
26+
related_field_name="title", allow_null=True, creation=False, required=False
27+
)
28+
2129
class Meta:
2230
model = Person
2331
fields = "__all__"

app/tests/factories/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ class Meta:
3232
email = "john.doe@example.com"
3333

3434

35+
class ElectionDistrictFactory(BaseFactory):
36+
class Meta:
37+
model = models.ElectionDistrict
38+
39+
uid = LazyFunction(lambda: uuid.uuid4()) # type: ignore
40+
number = "1"
41+
title = "Election District 1"
42+
43+
3544
class AddressFactory(BaseFactory):
3645
class Meta:
3746
model = models.Address

app/tests/test_mapping_serializer.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
from constance.test import override_config
66
from django.contrib.contenttypes.models import ContentType
77

8+
from app.models import ElectionDistrict
89
from app.models import Municipality
910
from app.models import Person
1011
from app.serializers.person import PersonMappingSerializer
1112
from app.tests import APITestCase
1213
from app.tests.custom_fields.factories import CustomFieldFactory
1314
from app.tests.custom_fields.factories import CustomValueFactory
1415
from app.tests.factories import AddressFactory
16+
from app.tests.factories import ElectionDistrictFactory
1517
from app.tests.factories import PersonFactory
1618
from django_features.custom_fields.models import CustomField
19+
from django_features.custom_fields.models import CustomValue
1720

1821

1922
MODEL_MAPPING_FIELD = {
@@ -30,6 +33,7 @@
3033
"external_choice_field": "choice_value",
3134
"external_multiple_choice_field": "multiple_choice_value",
3235
"external_municipality_title": "place_of_residence.title",
36+
"external_election_district_title": "election_district",
3337
"external_addresses": "addresses",
3438
}
3539
}
@@ -42,66 +46,68 @@ class MappingSerializerTestCase(APITestCase):
4246
def setUp(self) -> None:
4347
self.person_ct = ContentType.objects.get_for_model(Person)
4448

45-
self.char_field = CustomFieldFactory(
49+
self.char_field: CustomField = CustomFieldFactory( # type: ignore
4650
identifier="char_value",
4751
content_type=self.person_ct,
4852
field_type=CustomField.FIELD_TYPES.CHAR,
4953
)
50-
self.text_field = CustomFieldFactory(
54+
self.text_field: CustomField = CustomFieldFactory( # type: ignore
5155
identifier="text_value",
5256
content_type=self.person_ct,
5357
field_type=CustomField.FIELD_TYPES.TEXT,
5458
)
55-
self.date_field = CustomFieldFactory(
59+
self.date_field: CustomField = CustomFieldFactory( # type: ignore
5660
identifier="date_value",
5761
content_type=self.person_ct,
5862
field_type=CustomField.FIELD_TYPES.DATE,
5963
)
60-
self.datetime_field = CustomFieldFactory(
64+
self.datetime_field: CustomField = CustomFieldFactory( # type: ignore
6165
identifier="datetime_value",
6266
content_type=self.person_ct,
6367
field_type=CustomField.FIELD_TYPES.DATETIME,
6468
)
65-
self.integer_field = CustomFieldFactory(
69+
self.integer_field: CustomField = CustomFieldFactory( # type: ignore
6670
identifier="integer_value",
6771
content_type=self.person_ct,
6872
field_type=CustomField.FIELD_TYPES.INTEGER,
6973
)
70-
self.boolean_field = CustomFieldFactory(
74+
self.boolean_field: CustomField = CustomFieldFactory( # type: ignore
7175
identifier="boolean_value",
7276
content_type=self.person_ct,
7377
field_type=CustomField.FIELD_TYPES.BOOLEAN,
7478
)
75-
self.multiple_date_field = CustomFieldFactory(
79+
self.multiple_date_field: CustomField = CustomFieldFactory( # type: ignore
7680
identifier="multiple_date_value",
7781
content_type=self.person_ct,
7882
field_type=CustomField.FIELD_TYPES.DATE,
7983
multiple=True,
8084
)
81-
self.choice_field = CustomFieldFactory(
85+
self.choice_field: CustomField = CustomFieldFactory( # type: ignore
8286
identifier="choice_value",
8387
content_type=self.person_ct,
8488
field_type=CustomField.FIELD_TYPES.DATE,
8589
choice_field=True,
8690
)
87-
self.multiple_choice_field = CustomFieldFactory(
91+
self.multiple_choice_field: CustomField = CustomFieldFactory( # type: ignore
8892
identifier="multiple_choice_value",
8993
content_type=self.person_ct,
9094
field_type=CustomField.FIELD_TYPES.DATE,
9195
choice_field=True,
9296
multiple=True,
9397
)
9498

95-
self.choice_1 = CustomValueFactory(field=self.choice_field, value="2000-01-01")
96-
self.choice_2 = CustomValueFactory(field=self.choice_field, value="2001-01-01")
99+
self.choice_1: CustomValue = CustomValueFactory(field=self.choice_field, value="2000-01-01") # type: ignore
100+
self.choice_2: CustomValue = CustomValueFactory( # type: ignore
101+
field=self.choice_field, value="2001-01-01"
102+
)
97103

98-
self.multiple_choice_1 = CustomValueFactory(
104+
self.multiple_choice_1: CustomValue = CustomValueFactory( # type: ignore
99105
field=self.multiple_choice_field, value="2000-01-01"
100106
)
101-
self.multiple_choice_2 = CustomValueFactory(
107+
self.multiple_choice_2: CustomValue = CustomValueFactory( # type: ignore
102108
field=self.multiple_choice_field, value="2001-01-01"
103109
)
104-
self.multiple_choice_3 = CustomValueFactory(
110+
self.multiple_choice_3: CustomValue = CustomValueFactory( # type: ignore
105111
field=self.multiple_choice_field, value="2002-01-01"
106112
)
107113

@@ -111,6 +117,8 @@ def setUp(self) -> None:
111117

112118
@override_config(MODEL_MAPPING_FIELD=MODEL_MAPPING_FIELD)
113119
def test_mapping_serializer_create(self) -> None:
120+
election_district = ElectionDistrictFactory(title="Koeniz")
121+
114122
data = {
115123
"external_firstname": "Hugo",
116124
"external_lastname": "Boss",
@@ -124,6 +132,7 @@ def test_mapping_serializer_create(self) -> None:
124132
"external_choice_field": "2000-01-01",
125133
"external_multiple_choice_field": ["2000-01-01", "2001-01-01"],
126134
"external_municipality_title": "Muri",
135+
"external_election_district_title": "Koeniz",
127136
"external_addresses": [
128137
self.address_1.external_uid,
129138
self.address_2.external_uid,
@@ -164,13 +173,21 @@ def test_mapping_serializer_create(self) -> None:
164173

165174
self.assertEqual("Muri", instance.place_of_residence.title)
166175
self.assertEqual(1, Municipality.objects.count())
176+
177+
self.assertEqual(election_district, instance.election_district)
178+
self.assertEqual(1, ElectionDistrict.objects.count())
179+
167180
self.assertEqual(2, instance.addresses.count())
168181
self.assertEqual(instance, instance.addresses.first().target)
169182
self.assertEqual(instance, instance.addresses.last().target)
170183

171184
@override_config(MODEL_MAPPING_FIELD=MODEL_MAPPING_FIELD)
172185
def test_mapping_serializer_update(self) -> None:
173-
person: Person = PersonFactory(firstname="old", lastname="old") # type: ignore
186+
old_election_district = ElectionDistrictFactory(title="Old")
187+
election_district = ElectionDistrictFactory(title="Koeniz")
188+
person: Person = PersonFactory( # type: ignore
189+
firstname="old", lastname="old", election_district=old_election_district
190+
)
174191

175192
person.refresh_with_custom_fields()
176193

@@ -187,6 +204,7 @@ def test_mapping_serializer_update(self) -> None:
187204
"external_choice_field": "2001-01-01",
188205
"external_multiple_choice_field": ["2000-01-01", "2002-01-01"],
189206
"external_municipality_title": "Koeniz",
207+
"external_election_district_title": "Koeniz",
190208
"external_addresses": [
191209
self.address_1.external_uid,
192210
self.address_3.external_uid,
@@ -227,6 +245,10 @@ def test_mapping_serializer_update(self) -> None:
227245

228246
self.assertEqual("Koeniz", instance.place_of_residence.title)
229247
self.assertEqual(1, Municipality.objects.count())
248+
249+
self.assertEqual(election_district, instance.election_district)
250+
self.assertEqual(2, ElectionDistrict.objects.count())
251+
230252
self.assertEqual(2, instance.addresses.count())
231253
self.assertEqual(person, instance.addresses.first().target)
232254
self.assertEqual(person, instance.addresses.last().target)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from typing import Any
2+
3+
from app import models
4+
from app.tests import APITestCase
5+
from django_features.serializers import MappingSerializer
6+
7+
8+
class TestMappingSerializer(MappingSerializer):
9+
class Meta:
10+
model = models.Person
11+
fields = "__all__"
12+
13+
@property
14+
def mapping(self) -> dict[str, dict[str, Any]]:
15+
return {
16+
"person": {
17+
"external_base_field": "base_field",
18+
"external_single_field_1": "dict_field.nested_field_1",
19+
"external_single_field_2": "dict_field.nested_field_2",
20+
"external_dict_field.nested_field": "single_field",
21+
"external_object_field.nested_external_field_1": "object_field.nested_field_1",
22+
"external_object_field.nested_external_field_2": "object_field.nested_field_2",
23+
"external_object_field_with_object.external_object_field_1.external_field_1": "object_field_with_object.object_field_1.field_1", # noqa: E501
24+
"external_object_field_with_object.external_object_field_1.external_field_2": "object_field_with_object.object_field_1.field_2", # noqa: E501
25+
"external_object_field_with_object.external_object_field_2.external_field_1": "object_field_with_object.object_field_2.field_1", # noqa: E501
26+
"external_object_field_with_object.external_object_field_2.external_field_2": "object_field_with_object.object_field_2.field_2", # noqa: E501
27+
}
28+
}
29+
30+
31+
class MappingSerializerTestCase(APITestCase):
32+
def test_mapping_serializer_map_initial_data(self) -> None:
33+
data = {
34+
"external_base_field": "base_value",
35+
"external_single_field_1": "nested_value_1",
36+
"external_single_field_2": "nested_value_2",
37+
"external_dict_field": {"nested_field": "single_value"},
38+
"external_object_field": {
39+
"nested_external_field_1": "nested_value_1",
40+
"nested_external_field_2": "nested_value_2",
41+
},
42+
"external_object_field_with_object": {
43+
"external_object_field_1": {
44+
"external_field_1": "value_1",
45+
"external_field_2": "value_2",
46+
},
47+
"external_object_field_2": {
48+
"external_field_1": "value_1",
49+
"external_field_2": "value_2",
50+
},
51+
},
52+
}
53+
54+
expected_data = {
55+
"base_field": "base_value",
56+
"dict_field": {
57+
"nested_field_1": "nested_value_1",
58+
"nested_field_2": "nested_value_2",
59+
},
60+
"single_field": "single_value",
61+
"object_field": {
62+
"nested_field_1": "nested_value_1",
63+
"nested_field_2": "nested_value_2",
64+
},
65+
"object_field_with_object": {
66+
"object_field_1": {"field_1": "value_1", "field_2": "value_2"},
67+
"object_field_2": {"field_1": "value_1", "field_2": "value_2"},
68+
},
69+
}
70+
71+
mapped_data = TestMappingSerializer().map_data(data)
72+
self.assertEqual(mapped_data, expected_data)

changes/TI-2893.other

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve MappingSerializer and add option for more related fields. (`TI-2893 <https://4teamwork.atlassian.net/browse/TI-2893>`_)

0 commit comments

Comments
 (0)