diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 2929874b01..c162780ef4 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -7,8 +7,6 @@ from django.apps import apps from django.core.serializers import base from django.db import DEFAULT_DB_ALIAS, models -from django.db.models import CompositePrimaryKey -from django.utils.encoding import is_protected_type class Serializer(base.Serializer): @@ -40,13 +38,7 @@ def get_dump_object(self, obj): return data def _value_from_field(self, obj, field): - if isinstance(field, CompositePrimaryKey): - return [self._value_from_field(obj, f) for f in field] - value = field.value_from_object(obj) - # Protected types (i.e., primitives like None, numbers, dates, - # and Decimals) are passed through as is. All other values are - # converted to string first. - return value if is_protected_type(value) else field.value_to_string(obj) + return field.serialize_to_python(obj, self) def handle_field(self, obj, field): self._current[field.name] = self._value_from_field(obj, field) @@ -192,7 +184,7 @@ def _handle_object(self, obj): # Handle all other fields else: try: - data[field.name] = field.to_python(field_value) + data[field.name] = field.deserialize_from_python(field_value) except Exception as e: raise base.DeserializationError.WithData( e, obj["model"], obj.get("pk"), field_value diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index f8ec0865a7..7e12df494e 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -2,7 +2,6 @@ XML serializer. """ -import json from contextlib import contextmanager from xml.dom import minidom, pulldom from xml.sax import handler @@ -48,6 +47,7 @@ def start_serialization(self): """ Start serialization -- open the XML document and the root element. """ + self.indent_level = 0 self.xml = SimplerXMLGenerator( self.stream, self.options.get("encoding", settings.DEFAULT_CHARSET) ) @@ -58,7 +58,7 @@ def end_serialization(self): """ End serialization -- end the document. """ - self.indent(0) + self.indent(self.indent_level) self.xml.endElement("django-objects") self.xml.endDocument() @@ -71,7 +71,8 @@ def start_object(self, obj): "Non-model object (%s) encountered during serialization" % type(obj) ) - self.indent(1) + self.indent_level += 1 + self.indent(self.indent_level) attrs = {"model": str(obj._meta)} if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"): obj_pk = obj.pk @@ -84,15 +85,17 @@ def end_object(self, obj): """ Called after handling all fields for an object. """ - self.indent(1) + self.indent(self.indent_level) self.xml.endElement("object") + self.indent_level -= 1 def handle_field(self, obj, field): """ Handle each field on an object (except for ForeignKeys and ManyToManyFields). """ - self.indent(2) + self.indent_level += 1 + self.indent(self.indent_level) self.xml.startElement( "field", { @@ -103,11 +106,7 @@ def handle_field(self, obj, field): # Get a "string version" of the object's data. if getattr(obj, field.name) is not None: - value = field.value_to_string(obj) - if field.get_internal_type() == "JSONField": - # Dump value since JSONField.value_to_string() doesn't output - # strings. - value = json.dumps(value, cls=field.encoder) + value = field.serialize_to_xml(obj, self) try: self.xml.characters(value) except UnserializableContentError: @@ -119,6 +118,7 @@ def handle_field(self, obj, field): self.xml.addQuickElement("None") self.xml.endElement("field") + self.indent_level -= 1 def handle_fk_field(self, obj, field): """ @@ -144,6 +144,7 @@ def handle_fk_field(self, obj, field): else: self.xml.addQuickElement("None") self.xml.endElement("field") + self.indent_level -= 1 def handle_m2m_field(self, obj, field): """ @@ -177,6 +178,7 @@ def queryset_iterator(obj, field): else: def handle_m2m(value): + self.indent(self.indent_level + 1) self.xml.addQuickElement("object", attrs={"pk": str(value.pk)}) def queryset_iterator(obj, field): @@ -192,10 +194,12 @@ def queryset_iterator(obj, field): handle_m2m(relobj) self.xml.endElement("field") + self.indent_level -= 1 def _start_relational_field(self, field): """Output the element for relational fields.""" - self.indent(2) + self.indent_level += 1 + self.indent(self.indent_level) self.xml.startElement( "field", { @@ -255,7 +259,7 @@ def _handle_object(self, node): field_names = {f.name for f in Model._meta.get_fields()} # Deserialize each field. - for field_node in node.getElementsByTagName("field"): + for field_node in getChildElementsByTagName(node, "field"): # If the field is missing the name attribute, bail (are you # sensing a pattern here?) field_name = field_node.getAttribute("name") @@ -299,13 +303,10 @@ def _handle_object(self, node): else: data[field.attname] = value else: - if field_node.getElementsByTagName("None"): + if getChildElementsByTagName(field_node, "None"): value = None else: - value = field.to_python(getInnerText(field_node).strip()) - # Load value since JSONField.to_python() outputs strings. - if field.get_internal_type() == "JSONField": - value = json.loads(value, cls=field.decoder) + value = field.deserialize_from_xml(field_node) data[field.name] = value obj = base.build_instance(Model, data, self.db) @@ -416,6 +417,29 @@ def _get_model_from_node(self, node, attr): ) +def getChildElementsByTagName(node, tag_name): + """ + Like Element.getElementsByTagName() but return only direct children. + + Element.getElementsByTagName() searches all descendants (direct children, + children’s children, etc.). This prevents correct deserialization of + third-party embedded fields in places where only direct child elements + should be retrieved. For example: + + + + + + This method is used by the deserializer instead, except for related + fields which aren't supported by embedded fields. + """ + return [ + n + for n in node.childNodes + if n.nodeType == node.ELEMENT_NODE and n.tagName == tag_name + ] + + def getInnerText(node): """Get all the inner text of a DOM node (recursively).""" inner_text_list = getInnerTextList(node) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index ee7a30cc30..eabfbd0231 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -31,6 +31,7 @@ parse_time, ) from django.utils.duration import duration_microseconds, duration_string +from django.utils.encoding import is_protected_type from django.utils.functional import Promise, cached_property from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address from django.utils.text import capfirst @@ -763,6 +764,25 @@ def to_python(self, value): """ return value + def serialize_to_python(self, obj, serializer): + value = self.value_from_object(obj) + # Protected types (i.e., primitives like None, numbers, dates, and + # Decimals) are passed through as is. All other values are converted to + # string first. + return value if is_protected_type(value) else self.value_to_string(obj) + + def deserialize_from_python(self, value): + return self.to_python(value) + + def serialize_to_xml(self, obj, serializer, *, indent=None): + return self.value_to_string(obj) + + def deserialize_from_xml(self, field_node): + from django.core.serializers.xml_serializer import getInnerText + + value = getInnerText(field_node).strip() + return self.to_python(value) + @cached_property def error_messages(self): messages = {} diff --git a/django/db/models/fields/composite.py b/django/db/models/fields/composite.py index 4b74f90c1f..127b1b72a4 100644 --- a/django/db/models/fields/composite.py +++ b/django/db/models/fields/composite.py @@ -155,6 +155,9 @@ def to_python(self, value): ] return value + def serialize_to_python(self, serializer, obj): + return [serializer._value_from_field(obj, f) for f in self] + CompositePrimaryKey.register_lookup(TupleExact) CompositePrimaryKey.register_lookup(TupleGreaterThan) diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py index e0aa5c622b..3795391219 100644 --- a/django/db/models/fields/json.py +++ b/django/db/models/fields/json.py @@ -123,6 +123,17 @@ def get_transform(self, name): return transform return KeyTransformFactory(name) + def deserialize_from_xml(self, value): + value = super().deserialize_from_xml(value) + # Load value since JSONField.to_python() isn't defined to convert + # strings to Python values. + return json.loads(value, cls=self.decoder) + + def serialize_to_xml(self, serializer, obj, *, indent=None): + value = super().serialize_to_xml(serializer, obj) + # Dump value since value_to_string() doesn't output strings. + return json.dumps(value, cls=self.encoder) + def validate(self, value, model_instance): super().validate(value, model_instance) try: diff --git a/tests/serializers/test_xml.py b/tests/serializers/test_xml.py index 38bf9b7b49..c5b6ced58c 100644 --- a/tests/serializers/test_xml.py +++ b/tests/serializers/test_xml.py @@ -24,7 +24,9 @@ class XmlSerializerTestCase(SerializersTestBase, TestCase): %(author_pk)s Poker has no place on ESPN 2006-06-16T11:00:00 - + + +