Django

Code

root/django/trunk/django/core/serializers/xml_serializer.py

Revision 8321, 8.5 kB (checked in by russellm, 3 months ago)

Fixed #8134 -- Corrected serialization of m2m fields with intermediate models. Thanks to Rock Howard for the report, and kire for the test case.

  • Property svn:eol-style set to native
Line 
1 """
2 XML serializer.
3 """
4
5 from django.conf import settings
6 from django.core.serializers import base
7 from django.db import models
8 from django.utils.xmlutils import SimplerXMLGenerator
9 from django.utils.encoding import smart_unicode
10 from xml.dom import pulldom
11
12 class Serializer(base.Serializer):
13     """
14     Serializes a QuerySet to XML.
15     """
16
17     def indent(self, level):
18         if self.options.get('indent', None) is not None:
19             self.xml.ignorableWhitespace('\n' + ' ' * self.options.get('indent', None) * level)
20
21     def start_serialization(self):
22         """
23         Start serialization -- open the XML document and the root element.
24         """
25         self.xml = SimplerXMLGenerator(self.stream, self.options.get("encoding", settings.DEFAULT_CHARSET))
26         self.xml.startDocument()
27         self.xml.startElement("django-objects", {"version" : "1.0"})
28
29     def end_serialization(self):
30         """
31         End serialization -- end the document.
32         """
33         self.indent(0)
34         self.xml.endElement("django-objects")
35         self.xml.endDocument()
36
37     def start_object(self, obj):
38         """
39         Called as each object is handled.
40         """
41         if not hasattr(obj, "_meta"):
42             raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
43
44         self.indent(1)
45         self.xml.startElement("object", {
46             "pk"    : smart_unicode(obj._get_pk_val()),
47             "model" : smart_unicode(obj._meta),
48         })
49
50     def end_object(self, obj):
51         """
52         Called after handling all fields for an object.
53         """
54         self.indent(1)
55         self.xml.endElement("object")
56
57     def handle_field(self, obj, field):
58         """
59         Called to handle each field on an object (except for ForeignKeys and
60         ManyToManyFields)
61         """
62         self.indent(2)
63         self.xml.startElement("field", {
64             "name" : field.name,
65             "type" : field.get_internal_type()
66         })
67
68         # Get a "string version" of the object's data (this is handled by the
69         # serializer base class).
70         if getattr(obj, field.name) is not None:
71             value = self.get_string_value(obj, field)
72             self.xml.characters(smart_unicode(value))
73         else:
74             self.xml.addQuickElement("None")
75
76         self.xml.endElement("field")
77
78     def handle_fk_field(self, obj, field):
79         """
80         Called to handle a ForeignKey (we need to treat them slightly
81         differently from regular fields).
82         """
83         self._start_relational_field(field)
84         related = getattr(obj, field.name)
85         if related is not None:
86             if field.rel.field_name == related._meta.pk.name:
87                 # Related to remote object via primary key
88                 related = related._get_pk_val()
89             else:
90                 # Related to remote object via other field
91                 related = getattr(related, field.rel.field_name)
92             self.xml.characters(smart_unicode(related))
93         else:
94             self.xml.addQuickElement("None")
95         self.xml.endElement("field")
96
97     def handle_m2m_field(self, obj, field):
98         """
99         Called to handle a ManyToManyField. Related objects are only
100         serialized as references to the object's PK (i.e. the related *data*
101         is not dumped, just the relation).
102         """
103         if field.creates_table:
104             self._start_relational_field(field)
105             for relobj in getattr(obj, field.name).iterator():
106                 self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())})
107             self.xml.endElement("field")
108
109     def _start_relational_field(self, field):
110         """
111         Helper to output the <field> element for relational fields
112         """
113         self.indent(2)
114         self.xml.startElement("field", {
115             "name" : field.name,
116             "rel"  : field.rel.__class__.__name__,
117             "to"   : smart_unicode(field.rel.to._meta),
118         })
119
120 class Deserializer(base.Deserializer):
121     """
122     Deserialize XML.
123     """
124
125     def __init__(self, stream_or_string, **options):
126         super(Deserializer, self).__init__(stream_or_string, **options)
127         self.event_stream = pulldom.parse(self.stream)
128
129     def next(self):
130         for event, node in self.event_stream:
131             if event == "START_ELEMENT" and node.nodeName == "object":
132                 self.event_stream.expandNode(node)
133                 return self._handle_object(node)
134         raise StopIteration
135
136     def _handle_object(self, node):
137         """
138         Convert an <object> node to a DeserializedObject.
139         """
140         # Look up the model using the model loading mechanism. If this fails,
141         # bail.
142         Model = self._get_model_from_node(node, "model")
143
144         # Start building a data dictionary from the object.  If the node is
145         # missing the pk attribute, bail.
146         pk = node.getAttribute("pk")
147         if not pk:
148             raise base.DeserializationError("<object> node is missing the 'pk' attribute")
149
150         data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)}
151
152         # Also start building a dict of m2m data (this is saved as
153         # {m2m_accessor_attribute : [list_of_related_objects]})
154         m2m_data = {}
155
156         # Deseralize each field.
157         for field_node in node.getElementsByTagName("field"):
158             # If the field is missing the name attribute, bail (are you
159             # sensing a pattern here?)
160             field_name = field_node.getAttribute("name")
161             if not field_name:
162                 raise base.DeserializationError("<field> node is missing the 'name' attribute")
163
164             # Get the field from the Model. This will raise a
165             # FieldDoesNotExist if, well, the field doesn't exist, which will
166             # be propagated correctly.
167             field = Model._meta.get_field(field_name)
168
169             # As is usually the case, relation fields get the special treatment.
170             if field.rel and isinstance(field.rel, models.ManyToManyRel):
171                 m2m_data[field.name] = self._handle_m2m_field_node(field_node, field)
172             elif field.rel and isinstance(field.rel, models.ManyToOneRel):
173                 data[field.attname] = self._handle_fk_field_node(field_node, field)
174             else:
175                 if field_node.getElementsByTagName('None'):
176                     value = None
177                 else:
178                     value = field.to_python(getInnerText(field_node).strip())
179                 data[field.name] = value
180
181         # Return a DeserializedObject so that the m2m data has a place to live.
182         return base.DeserializedObject(Model(**data), m2m_data)
183
184     def _handle_fk_field_node(self, node, field):
185         """
186         Handle a <field> node for a ForeignKey
187         """
188         # Check if there is a child node named 'None', returning None if so.
189         if node.getElementsByTagName('None'):
190             return None
191         else:
192             return field.rel.to._meta.get_field(field.rel.field_name).to_python(
193                        getInnerText(node).strip())
194
195     def _handle_m2m_field_node(self, node, field):
196         """
197         Handle a <field> node for a ManyToManyField.
198         """
199         return [field.rel.to._meta.pk.to_python(
200                     c.getAttribute("pk"))
201                     for c in node.getElementsByTagName("object")]
202
203     def _get_model_from_node(self, node, attr):
204         """
205         Helper to look up a model from a <object model=...> or a <field
206         rel=... to=...> node.
207         """
208         model_identifier = node.getAttribute(attr)
209         if not model_identifier:
210             raise base.DeserializationError(
211                 "<%s> node is missing the required '%s' attribute" \
212                     % (node.nodeName, attr))
213         try:
214             Model = models.get_model(*model_identifier.split("."))
215         except TypeError:
216             Model = None
217         if Model is None:
218             raise base.DeserializationError(
219                 "<%s> node has invalid model identifier: '%s'" % \
220                     (node.nodeName, model_identifier))
221         return Model
222
223
224 def getInnerText(node):
225     """
226     Get all the inner text of a DOM node (recursively).
227     """
228     # inspired by http://mail.python.org/pipermail/xml-sig/2005-March/011022.html
229     inner_text = []
230     for child in node.childNodes:
231         if child.nodeType == child.TEXT_NODE or child.nodeType == child.CDATA_SECTION_NODE:
232             inner_text.append(child.data)
233         elif child.nodeType == child.ELEMENT_NODE:
234             inner_text.extend(getInnerText(child))
235         else:
236            pass
237     return u"".join(inner_text)
Note: See TracBrowser for help on using the browser.