Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions django/core/serializers/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
58 changes: 41 additions & 17 deletions django/core/serializers/xml_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
XML serializer.
"""

import json
from contextlib import contextmanager
from xml.dom import minidom, pulldom
from xml.sax import handler
Expand Down Expand Up @@ -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)
)
Expand All @@ -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()

Expand All @@ -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
Expand All @@ -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",
{
Expand All @@ -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:
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand All @@ -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 <field> element for relational fields."""
self.indent(2)
self.indent_level += 1
self.indent(self.indent_level)
self.xml.startElement(
"field",
{
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:

<field name="author" type="EmbeddedModelField">
<object model="app.Model">
<field name="id" type="..."><None></None></field>

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)
Expand Down
20 changes: 20 additions & 0 deletions django/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
Expand Down
3 changes: 3 additions & 0 deletions django/db/models/fields/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions django/db/models/fields/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion tests/serializers/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class XmlSerializerTestCase(SerializersTestBase, TestCase):
<field name="author" rel="ManyToOneRel" to="serializers.author">%(author_pk)s</field>
<field name="headline" type="CharField">Poker has no place on ESPN</field>
<field name="pub_date" type="DateTimeField">2006-06-16T11:00:00</field>
<field name="categories" rel="ManyToManyRel" to="serializers.category"><object pk="%(first_category_pk)s"></object><object pk="%(second_category_pk)s"></object></field>
<field name="categories" rel="ManyToManyRel" to="serializers.category">
<object pk="%(first_category_pk)s"></object>
<object pk="%(second_category_pk)s"></object></field>
<field name="meta_data" rel="ManyToManyRel" to="serializers.categorymetadata"></field>
<field name="topics" rel="ManyToManyRel" to="serializers.topic"></field>
</object>
Expand Down
Loading