From d0f59054d0c4b2291a4e0e94ec00537f98a4ab50 Mon Sep 17 00:00:00 2001 From: Georg Sauthoff Date: Fri, 10 Feb 2017 15:29:34 +0100 Subject: [PATCH] Fixed #28324 -- Made feedgenerators write feeds with deterministically ordered attributes. --- django/core/serializers/xml_serializer.py | 21 ++++++++++----------- django/utils/xmlutils.py | 6 ++++++ tests/utils_tests/test_feedgenerator.py | 5 +++++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 1fb0093850..1c57e8b660 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -2,7 +2,6 @@ XML serializer. """ -from collections import OrderedDict from xml.dom import pulldom from xml.sax import handler from xml.sax.expatreader import ExpatParser as _ExpatParser @@ -47,7 +46,7 @@ class Serializer(base.Serializer): raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj)) self.indent(1) - attrs = OrderedDict([("model", str(obj._meta))]) + attrs = {'model': str(obj._meta)} if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'): obj_pk = obj.pk if obj_pk is not None: @@ -68,10 +67,10 @@ class Serializer(base.Serializer): ManyToManyFields). """ self.indent(2) - self.xml.startElement("field", OrderedDict([ - ("name", field.name), - ("type", field.get_internal_type()), - ])) + self.xml.startElement('field', { + 'name': field.name, + 'type': field.get_internal_type(), + }) # Get a "string version" of the object's data. if getattr(obj, field.name) is not None: @@ -140,11 +139,11 @@ class Serializer(base.Serializer): def _start_relational_field(self, field): """Output the element for relational fields.""" self.indent(2) - self.xml.startElement("field", OrderedDict([ - ("name", field.name), - ("rel", field.remote_field.__class__.__name__), - ("to", str(field.remote_field.model._meta)), - ])) + self.xml.startElement('field', { + 'name': field.name, + 'rel': field.remote_field.__class__.__name__, + 'to': str(field.remote_field.model._meta), + }) class Deserializer(base.Deserializer): diff --git a/django/utils/xmlutils.py b/django/utils/xmlutils.py index f1edfb2ac9..6b62a1fe74 100644 --- a/django/utils/xmlutils.py +++ b/django/utils/xmlutils.py @@ -3,6 +3,7 @@ Utilities for XML generation/parsing. """ import re +from collections import OrderedDict from xml.sax.saxutils import XMLGenerator @@ -26,3 +27,8 @@ class SimplerXMLGenerator(XMLGenerator): # See http://www.w3.org/International/questions/qa-controls raise UnserializableContentError("Control characters are not supported in XML 1.0") XMLGenerator.characters(self, content) + + def startElement(self, name, attrs): + # Sort attrs for a deterministic output. + sorted_attrs = OrderedDict(sorted(attrs.items())) if attrs else attrs + super().startElement(name, sorted_attrs) diff --git a/tests/utils_tests/test_feedgenerator.py b/tests/utils_tests/test_feedgenerator.py index 78e4d5b737..45c669dcfa 100644 --- a/tests/utils_tests/test_feedgenerator.py +++ b/tests/utils_tests/test_feedgenerator.py @@ -126,6 +126,11 @@ class FeedgeneratorTest(unittest.TestCase): feed.add_item('item_title', 'item_link', 'item_description') feed.writeString('utf-8') + def test_deterministic_attribute_order(self): + feed = feedgenerator.Atom1Feed('title', '/link/', 'desc') + feed_content = feed.writeString('utf-8') + self.assertIn('href="/link/" rel="alternate"', feed_content) + class FeedgeneratorDBTest(TestCase):