From 6a1a9c0eade674780060cf8af5f5b3375156cdd5 Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:40:05 +0100 Subject: [PATCH] Fixed #36062 -- Handled serialization of CompositePrimaryKeys. --- django/core/serializers/xml_serializer.py | 2 +- django/db/models/fields/composite.py | 25 +++++++++++++ tests/composite_pk/fixtures/tenant.json | 8 +++++ tests/composite_pk/models/__init__.py | 3 +- tests/composite_pk/models/tenant.py | 6 ++++ tests/composite_pk/tests.py | 43 ++++++++++++++++++++++- 6 files changed, 84 insertions(+), 3 deletions(-) diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 3530d443b2..360d5309d8 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -56,7 +56,7 @@ class Serializer(base.Serializer): if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"): obj_pk = obj.pk if obj_pk is not None: - attrs["pk"] = str(obj_pk) + attrs["pk"] = obj._meta.pk.value_to_string(obj) self.xml.startElement("object", attrs) diff --git a/django/db/models/fields/composite.py b/django/db/models/fields/composite.py index 2b196f6d2a..4b74f90c1f 100644 --- a/django/db/models/fields/composite.py +++ b/django/db/models/fields/composite.py @@ -1,3 +1,5 @@ +import json + from django.core import checks from django.db.models import NOT_PROVIDED, Field from django.db.models.expressions import ColPairs @@ -13,6 +15,11 @@ from django.db.models.fields.tuple_lookups import ( from django.utils.functional import cached_property +class AttributeSetter: + def __init__(self, name, value): + setattr(self, name, value) + + class CompositeAttribute: def __init__(self, field): self.field = field @@ -130,6 +137,24 @@ class CompositePrimaryKey(Field): ) ] + def value_to_string(self, obj): + values = [] + vals = self.value_from_object(obj) + for field, value in zip(self.fields, vals): + obj = AttributeSetter(field.attname, value) + values.append(field.value_to_string(obj)) + return json.dumps(values, ensure_ascii=False) + + def to_python(self, value): + if isinstance(value, str): + # Assume we're deserializing. + vals = json.loads(value) + value = [ + field.to_python(val) + for field, val in zip(self.fields, vals, strict=True) + ] + return value + CompositePrimaryKey.register_lookup(TupleExact) CompositePrimaryKey.register_lookup(TupleGreaterThan) diff --git a/tests/composite_pk/fixtures/tenant.json b/tests/composite_pk/fixtures/tenant.json index 3eeff42fef..66a25e94f6 100644 --- a/tests/composite_pk/fixtures/tenant.json +++ b/tests/composite_pk/fixtures/tenant.json @@ -71,5 +71,13 @@ "tenant_id": 2, "id": "ffffffff-ffff-ffff-ffff-ffffffffffff" } + }, + { + "pk": [1, "2022-01-12T05:55:14.956"], + "model": "composite_pk.timestamped", + "fields": { + "id": 1, + "created": "2022-01-12T05:55:14.956" + } } ] diff --git a/tests/composite_pk/models/__init__.py b/tests/composite_pk/models/__init__.py index 35c3943716..5996ae33b0 100644 --- a/tests/composite_pk/models/__init__.py +++ b/tests/composite_pk/models/__init__.py @@ -1,9 +1,10 @@ -from .tenant import Comment, Post, Tenant, Token, User +from .tenant import Comment, Post, Tenant, TimeStamped, Token, User __all__ = [ "Comment", "Post", "Tenant", + "TimeStamped", "Token", "User", ] diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py index ac0b3d9715..810fb50db7 100644 --- a/tests/composite_pk/models/tenant.py +++ b/tests/composite_pk/models/tenant.py @@ -48,3 +48,9 @@ class Post(models.Model): pk = models.CompositePrimaryKey("tenant_id", "id") tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) id = models.UUIDField() + + +class TimeStamped(models.Model): + pk = models.CompositePrimaryKey("id", "created") + id = models.SmallIntegerField(unique=True) + created = models.DateTimeField(auto_now_add=True) diff --git a/tests/composite_pk/tests.py b/tests/composite_pk/tests.py index 4ebdbc371e..303c6592fb 100644 --- a/tests/composite_pk/tests.py +++ b/tests/composite_pk/tests.py @@ -17,7 +17,7 @@ from django.db.models import CompositePrimaryKey from django.forms import modelform_factory from django.test import TestCase -from .models import Comment, Post, Tenant, User +from .models import Comment, Post, Tenant, TimeStamped, User class CommentForm(forms.ModelForm): @@ -224,6 +224,13 @@ class CompositePKFixturesTests(TestCase): self.assertEqual(post_2.tenant_id, 2) self.assertEqual(post_2.pk, (post_2.tenant_id, post_2.id)) + def assert_deserializer(self, format, users, serialized_users): + deserialized_user = list(serializers.deserialize(format, serialized_users))[0] + self.assertEqual(deserialized_user.object.email, users[0].email) + self.assertEqual(deserialized_user.object.id, users[0].id) + self.assertEqual(deserialized_user.object.tenant, users[0].tenant) + self.assertEqual(deserialized_user.object.pk, users[0].pk) + def test_serialize_user_json(self): users = User.objects.filter(pk=(1, 1)) result = serializers.serialize("json", users) @@ -241,6 +248,7 @@ class CompositePKFixturesTests(TestCase): } ], ) + self.assert_deserializer(format="json", users=users, serialized_users=result) def test_serialize_user_jsonl(self): users = User.objects.filter(pk=(1, 2)) @@ -257,6 +265,7 @@ class CompositePKFixturesTests(TestCase): }, }, ) + self.assert_deserializer(format="jsonl", users=users, serialized_users=result) @unittest.skipUnless(HAS_YAML, "No yaml library detected") def test_serialize_user_yaml(self): @@ -276,6 +285,7 @@ class CompositePKFixturesTests(TestCase): }, ], ) + self.assert_deserializer(format="yaml", users=users, serialized_users=result) def test_serialize_user_python(self): users = User.objects.filter(pk=(2, 4)) @@ -294,6 +304,13 @@ class CompositePKFixturesTests(TestCase): }, ], ) + self.assert_deserializer(format="python", users=users, serialized_users=result) + + def test_serialize_user_xml(self): + users = User.objects.filter(pk=(1, 1)) + result = serializers.serialize("xml", users) + self.assertIn('', result) + self.assert_deserializer(format="xml", users=users, serialized_users=result) def test_serialize_post_uuid(self): posts = Post.objects.filter(pk=(2, "11111111-1111-1111-1111-111111111111")) @@ -311,3 +328,27 @@ class CompositePKFixturesTests(TestCase): }, ], ) + + def test_serialize_datetime(self): + result = serializers.serialize("json", TimeStamped.objects.all()) + self.assertEqual( + json.loads(result), + [ + { + "model": "composite_pk.timestamped", + "pk": [1, "2022-01-12T05:55:14.956"], + "fields": { + "id": 1, + "created": "2022-01-12T05:55:14.956", + }, + }, + ], + ) + + def test_invalid_pk_extra_field(self): + json = ( + '[{"fields": {"email": "user0001@example.com", "id": 1, "tenant": 1}, ' + '"pk": [1, 1, "extra"], "model": "composite_pk.user"}]' + ) + with self.assertRaises(serializers.base.DeserializationError): + next(serializers.deserialize("json", json))