|
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, DEFAULT_DB_ALIAS |
|
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. |
|
69 if getattr(obj, field.name) is not None: |
|
70 self.xml.characters(field.value_to_string(obj)) |
|
71 else: |
|
72 self.xml.addQuickElement("None") |
|
73 |
|
74 self.xml.endElement("field") |
|
75 |
|
76 def handle_fk_field(self, obj, field): |
|
77 """ |
|
78 Called to handle a ForeignKey (we need to treat them slightly |
|
79 differently from regular fields). |
|
80 """ |
|
81 self._start_relational_field(field) |
|
82 related = getattr(obj, field.name) |
|
83 if related is not None: |
|
84 if self.use_natural_keys and hasattr(related, 'natural_key'): |
|
85 # If related object has a natural key, use it |
|
86 related = related.natural_key() |
|
87 # Iterable natural keys are rolled out as subelements |
|
88 for key_value in related: |
|
89 self.xml.startElement("natural", {}) |
|
90 self.xml.characters(smart_unicode(key_value)) |
|
91 self.xml.endElement("natural") |
|
92 else: |
|
93 if field.rel.field_name == related._meta.pk.name: |
|
94 # Related to remote object via primary key |
|
95 related = related._get_pk_val() |
|
96 else: |
|
97 # Related to remote object via other field |
|
98 related = getattr(related, field.rel.field_name) |
|
99 self.xml.characters(smart_unicode(related)) |
|
100 else: |
|
101 self.xml.addQuickElement("None") |
|
102 self.xml.endElement("field") |
|
103 |
|
104 def handle_m2m_field(self, obj, field): |
|
105 """ |
|
106 Called to handle a ManyToManyField. Related objects are only |
|
107 serialized as references to the object's PK (i.e. the related *data* |
|
108 is not dumped, just the relation). |
|
109 """ |
|
110 if field.rel.through._meta.auto_created: |
|
111 self._start_relational_field(field) |
|
112 if self.use_natural_keys and hasattr(field.rel.to, 'natural_key'): |
|
113 # If the objects in the m2m have a natural key, use it |
|
114 def handle_m2m(value): |
|
115 natural = value.natural_key() |
|
116 # Iterable natural keys are rolled out as subelements |
|
117 self.xml.startElement("object", {}) |
|
118 for key_value in natural: |
|
119 self.xml.startElement("natural", {}) |
|
120 self.xml.characters(smart_unicode(key_value)) |
|
121 self.xml.endElement("natural") |
|
122 self.xml.endElement("object") |
|
123 else: |
|
124 def handle_m2m(value): |
|
125 self.xml.addQuickElement("object", attrs={ |
|
126 'pk' : smart_unicode(value._get_pk_val()) |
|
127 }) |
|
128 for relobj in getattr(obj, field.name).iterator(): |
|
129 handle_m2m(relobj) |
|
130 |
|
131 self.xml.endElement("field") |
|
132 |
|
133 def _start_relational_field(self, field): |
|
134 """ |
|
135 Helper to output the <field> element for relational fields |
|
136 """ |
|
137 self.indent(2) |
|
138 self.xml.startElement("field", { |
|
139 "name" : field.name, |
|
140 "rel" : field.rel.__class__.__name__, |
|
141 "to" : smart_unicode(field.rel.to._meta), |
|
142 }) |
|
143 |
|
144 class Deserializer(base.Deserializer): |
|
145 """ |
|
146 Deserialize XML. |
|
147 """ |
|
148 |
|
149 def __init__(self, stream_or_string, **options): |
|
150 super(Deserializer, self).__init__(stream_or_string, **options) |
|
151 self.event_stream = pulldom.parse(self.stream) |
|
152 self.db = options.pop('using', DEFAULT_DB_ALIAS) |
|
153 |
|
154 def next(self): |
|
155 for event, node in self.event_stream: |
|
156 if event == "START_ELEMENT" and node.nodeName == "object": |
|
157 self.event_stream.expandNode(node) |
|
158 return self._handle_object(node) |
|
159 raise StopIteration |
|
160 |
|
161 def _handle_object(self, node): |
|
162 """ |
|
163 Convert an <object> node to a DeserializedObject. |
|
164 """ |
|
165 # Look up the model using the model loading mechanism. If this fails, |
|
166 # bail. |
|
167 Model = self._get_model_from_node(node, "model") |
|
168 |
|
169 # Start building a data dictionary from the object. If the node is |
|
170 # missing the pk attribute, bail. |
|
171 pk = node.getAttribute("pk") |
|
172 if not pk: |
|
173 raise base.DeserializationError("<object> node is missing the 'pk' attribute") |
|
174 |
|
175 data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)} |
|
176 |
|
177 # Also start building a dict of m2m data (this is saved as |
|
178 # {m2m_accessor_attribute : [list_of_related_objects]}) |
|
179 m2m_data = {} |
|
180 |
|
181 # Deseralize each field. |
|
182 for field_node in node.getElementsByTagName("field"): |
|
183 # If the field is missing the name attribute, bail (are you |
|
184 # sensing a pattern here?) |
|
185 field_name = field_node.getAttribute("name") |
|
186 if not field_name: |
|
187 raise base.DeserializationError("<field> node is missing the 'name' attribute") |
|
188 |
|
189 # Get the field from the Model. This will raise a |
|
190 # FieldDoesNotExist if, well, the field doesn't exist, which will |
|
191 # be propagated correctly. |
|
192 field = Model._meta.get_field(field_name) |
|
193 |
|
194 # As is usually the case, relation fields get the special treatment. |
|
195 if field.rel and isinstance(field.rel, models.ManyToManyRel): |
|
196 m2m_data[field.name] = self._handle_m2m_field_node(field_node, field) |
|
197 elif field.rel and isinstance(field.rel, models.ManyToOneRel): |
|
198 data[field.attname] = self._handle_fk_field_node(field_node, field) |
|
199 else: |
|
200 if field_node.getElementsByTagName('None'): |
|
201 value = None |
|
202 else: |
|
203 value = field.to_python(getInnerText(field_node).strip()) |
|
204 data[field.name] = value |
|
205 |
|
206 # Return a DeserializedObject so that the m2m data has a place to live. |
|
207 return base.DeserializedObject(Model(**data), m2m_data) |
|
208 |
|
209 def _handle_fk_field_node(self, node, field): |
|
210 """ |
|
211 Handle a <field> node for a ForeignKey |
|
212 """ |
|
213 # Check if there is a child node named 'None', returning None if so. |
|
214 if node.getElementsByTagName('None'): |
|
215 return None |
|
216 else: |
|
217 if hasattr(field.rel.to._default_manager, 'get_by_natural_key'): |
|
218 keys = node.getElementsByTagName('natural') |
|
219 if keys: |
|
220 # If there are 'natural' subelements, it must be a natural key |
|
221 field_value = [getInnerText(k).strip() for k in keys] |
|
222 obj = field.rel.to._default_manager.db_manager(self.db).get_by_natural_key(*field_value) |
|
223 obj_pk = getattr(obj, field.rel.field_name) |
|
224 # If this is a natural foreign key to an object that |
|
225 # has a FK/O2O as the foreign key, use the FK value |
|
226 if field.rel.to._meta.pk.rel: |
|
227 obj_pk = obj_pk.pk |
|
228 else: |
|
229 # Otherwise, treat like a normal PK |
|
230 field_value = getInnerText(node).strip() |
|
231 obj_pk = field.rel.to._meta.get_field(field.rel.field_name).to_python(field_value) |
|
232 return obj_pk |
|
233 else: |
|
234 field_value = getInnerText(node).strip() |
|
235 return field.rel.to._meta.get_field(field.rel.field_name).to_python(field_value) |
|
236 |
|
237 def _handle_m2m_field_node(self, node, field): |
|
238 """ |
|
239 Handle a <field> node for a ManyToManyField. |
|
240 """ |
|
241 if hasattr(field.rel.to._default_manager, 'get_by_natural_key'): |
|
242 def m2m_convert(n): |
|
243 keys = n.getElementsByTagName('natural') |
|
244 if keys: |
|
245 # If there are 'natural' subelements, it must be a natural key |
|
246 field_value = [getInnerText(k).strip() for k in keys] |
|
247 obj_pk = field.rel.to._default_manager.db_manager(self.db).get_by_natural_key(*field_value).pk |
|
248 else: |
|
249 # Otherwise, treat like a normal PK value. |
|
250 obj_pk = field.rel.to._meta.pk.to_python(n.getAttribute('pk')) |
|
251 return obj_pk |
|
252 else: |
|
253 m2m_convert = lambda n: field.rel.to._meta.pk.to_python(n.getAttribute('pk')) |
|
254 return [m2m_convert(c) for c in node.getElementsByTagName("object")] |
|
255 |
|
256 def _get_model_from_node(self, node, attr): |
|
257 """ |
|
258 Helper to look up a model from a <object model=...> or a <field |
|
259 rel=... to=...> node. |
|
260 """ |
|
261 model_identifier = node.getAttribute(attr) |
|
262 if not model_identifier: |
|
263 raise base.DeserializationError( |
|
264 "<%s> node is missing the required '%s' attribute" \ |
|
265 % (node.nodeName, attr)) |
|
266 try: |
|
267 Model = models.get_model(*model_identifier.split(".")) |
|
268 except TypeError: |
|
269 Model = None |
|
270 if Model is None: |
|
271 raise base.DeserializationError( |
|
272 "<%s> node has invalid model identifier: '%s'" % \ |
|
273 (node.nodeName, model_identifier)) |
|
274 return Model |
|
275 |
|
276 |
|
277 def getInnerText(node): |
|
278 """ |
|
279 Get all the inner text of a DOM node (recursively). |
|
280 """ |
|
281 # inspired by http://mail.python.org/pipermail/xml-sig/2005-March/011022.html |
|
282 inner_text = [] |
|
283 for child in node.childNodes: |
|
284 if child.nodeType == child.TEXT_NODE or child.nodeType == child.CDATA_SECTION_NODE: |
|
285 inner_text.append(child.data) |
|
286 elif child.nodeType == child.ELEMENT_NODE: |
|
287 inner_text.extend(getInnerText(child)) |
|
288 else: |
|
289 pass |
|
290 return u"".join(inner_text) |