|
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. |
|
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 field.rel.field_name == related._meta.pk.name: |
|
85 # Related to remote object via primary key |
|
86 related = related._get_pk_val() |
|
87 else: |
|
88 # Related to remote object via other field |
|
89 related = getattr(related, field.rel.field_name) |
|
90 self.xml.characters(smart_unicode(related)) |
|
91 else: |
|
92 self.xml.addQuickElement("None") |
|
93 self.xml.endElement("field") |
|
94 |
|
95 def handle_m2m_field(self, obj, field): |
|
96 """ |
|
97 Called to handle a ManyToManyField. Related objects are only |
|
98 serialized as references to the object's PK (i.e. the related *data* |
|
99 is not dumped, just the relation). |
|
100 """ |
|
101 if field.creates_table: |
|
102 self._start_relational_field(field) |
|
103 for relobj in getattr(obj, field.name).iterator(): |
|
104 self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())}) |
|
105 self.xml.endElement("field") |
|
106 |
|
107 def _start_relational_field(self, field): |
|
108 """ |
|
109 Helper to output the <field> element for relational fields |
|
110 """ |
|
111 self.indent(2) |
|
112 self.xml.startElement("field", { |
|
113 "name" : field.name, |
|
114 "rel" : field.rel.__class__.__name__, |
|
115 "to" : smart_unicode(field.rel.to._meta), |
|
116 }) |
|
117 |
|
118 class Deserializer(base.Deserializer): |
|
119 """ |
|
120 Deserialize XML. |
|
121 """ |
|
122 |
|
123 def __init__(self, stream_or_string, **options): |
|
124 super(Deserializer, self).__init__(stream_or_string, **options) |
|
125 self.event_stream = pulldom.parse(self.stream) |
|
126 |
|
127 def next(self): |
|
128 for event, node in self.event_stream: |
|
129 if event == "START_ELEMENT" and node.nodeName == "object": |
|
130 self.event_stream.expandNode(node) |
|
131 return self._handle_object(node) |
|
132 raise StopIteration |
|
133 |
|
134 def _handle_object(self, node): |
|
135 """ |
|
136 Convert an <object> node to a DeserializedObject. |
|
137 """ |
|
138 # Look up the model using the model loading mechanism. If this fails, |
|
139 # bail. |
|
140 Model = self._get_model_from_node(node, "model") |
|
141 |
|
142 # Start building a data dictionary from the object. If the node is |
|
143 # missing the pk attribute, bail. |
|
144 pk = node.getAttribute("pk") |
|
145 if not pk: |
|
146 raise base.DeserializationError("<object> node is missing the 'pk' attribute") |
|
147 |
|
148 data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)} |
|
149 |
|
150 # Also start building a dict of m2m data (this is saved as |
|
151 # {m2m_accessor_attribute : [list_of_related_objects]}) |
|
152 m2m_data = {} |
|
153 |
|
154 # Deseralize each field. |
|
155 for field_node in node.getElementsByTagName("field"): |
|
156 # If the field is missing the name attribute, bail (are you |
|
157 # sensing a pattern here?) |
|
158 field_name = field_node.getAttribute("name") |
|
159 if not field_name: |
|
160 raise base.DeserializationError("<field> node is missing the 'name' attribute") |
|
161 |
|
162 # Get the field from the Model. This will raise a |
|
163 # FieldDoesNotExist if, well, the field doesn't exist, which will |
|
164 # be propagated correctly. |
|
165 field = Model._meta.get_field(field_name) |
|
166 |
|
167 # As is usually the case, relation fields get the special treatment. |
|
168 if field.rel and isinstance(field.rel, models.ManyToManyRel): |
|
169 m2m_data[field.name] = self._handle_m2m_field_node(field_node, field) |
|
170 elif field.rel and isinstance(field.rel, models.ManyToOneRel): |
|
171 data[field.attname] = self._handle_fk_field_node(field_node, field) |
|
172 else: |
|
173 if field_node.getElementsByTagName('None'): |
|
174 value = None |
|
175 else: |
|
176 value = field.to_python(getInnerText(field_node).strip()) |
|
177 data[field.name] = value |
|
178 |
|
179 # Return a DeserializedObject so that the m2m data has a place to live. |
|
180 return base.DeserializedObject(Model(**data), m2m_data) |
|
181 |
|
182 def _handle_fk_field_node(self, node, field): |
|
183 """ |
|
184 Handle a <field> node for a ForeignKey |
|
185 """ |
|
186 # Check if there is a child node named 'None', returning None if so. |
|
187 if node.getElementsByTagName('None'): |
|
188 return None |
|
189 else: |
|
190 return field.rel.to._meta.get_field(field.rel.field_name).to_python( |
|
191 getInnerText(node).strip()) |
|
192 |
|
193 def _handle_m2m_field_node(self, node, field): |
|
194 """ |
|
195 Handle a <field> node for a ManyToManyField. |
|
196 """ |
|
197 return [field.rel.to._meta.pk.to_python( |
|
198 c.getAttribute("pk")) |
|
199 for c in node.getElementsByTagName("object")] |
|
200 |
|
201 def _get_model_from_node(self, node, attr): |
|
202 """ |
|
203 Helper to look up a model from a <object model=...> or a <field |
|
204 rel=... to=...> node. |
|
205 """ |
|
206 model_identifier = node.getAttribute(attr) |
|
207 if not model_identifier: |
|
208 raise base.DeserializationError( |
|
209 "<%s> node is missing the required '%s' attribute" \ |
|
210 % (node.nodeName, attr)) |
|
211 try: |
|
212 Model = models.get_model(*model_identifier.split(".")) |
|
213 except TypeError: |
|
214 Model = None |
|
215 if Model is None: |
|
216 raise base.DeserializationError( |
|
217 "<%s> node has invalid model identifier: '%s'" % \ |
|
218 (node.nodeName, model_identifier)) |
|
219 return Model |
|
220 |
|
221 |
|
222 def getInnerText(node): |
|
223 """ |
|
224 Get all the inner text of a DOM node (recursively). |
|
225 """ |
|
226 # inspired by http://mail.python.org/pipermail/xml-sig/2005-March/011022.html |
|
227 inner_text = [] |
|
228 for child in node.childNodes: |
|
229 if child.nodeType == child.TEXT_NODE or child.nodeType == child.CDATA_SECTION_NODE: |
|
230 inner_text.append(child.data) |
|
231 elif child.nodeType == child.ELEMENT_NODE: |
|
232 inner_text.extend(getInnerText(child)) |
|
233 else: |
|
234 pass |
|
235 return u"".join(inner_text) |
|
236 |