You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
8.5 KiB
238 lines
8.5 KiB
""" |
|
XML serializer. |
|
""" |
|
|
|
from django.conf import settings |
|
from django.core.serializers import base |
|
from django.db import models |
|
from django.utils.xmlutils import SimplerXMLGenerator |
|
from django.utils.encoding import smart_unicode |
|
from xml.dom import pulldom |
|
|
|
class Serializer(base.Serializer): |
|
""" |
|
Serializes a QuerySet to XML. |
|
""" |
|
|
|
def indent(self, level): |
|
if self.options.get('indent', None) is not None: |
|
self.xml.ignorableWhitespace('\n' + ' ' * self.options.get('indent', None) * level) |
|
|
|
def start_serialization(self): |
|
""" |
|
Start serialization -- open the XML document and the root element. |
|
""" |
|
self.xml = SimplerXMLGenerator(self.stream, self.options.get("encoding", settings.DEFAULT_CHARSET)) |
|
self.xml.startDocument() |
|
self.xml.startElement("django-objects", {"version" : "1.0"}) |
|
|
|
def end_serialization(self): |
|
""" |
|
End serialization -- end the document. |
|
""" |
|
self.indent(0) |
|
self.xml.endElement("django-objects") |
|
self.xml.endDocument() |
|
|
|
def start_object(self, obj): |
|
""" |
|
Called as each object is handled. |
|
""" |
|
if not hasattr(obj, "_meta"): |
|
raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj)) |
|
|
|
self.indent(1) |
|
self.xml.startElement("object", { |
|
"pk" : smart_unicode(obj._get_pk_val()), |
|
"model" : smart_unicode(obj._meta), |
|
}) |
|
|
|
def end_object(self, obj): |
|
""" |
|
Called after handling all fields for an object. |
|
""" |
|
self.indent(1) |
|
self.xml.endElement("object") |
|
|
|
def handle_field(self, obj, field): |
|
""" |
|
Called to handle each field on an object (except for ForeignKeys and |
|
ManyToManyFields) |
|
""" |
|
self.indent(2) |
|
self.xml.startElement("field", { |
|
"name" : field.name, |
|
"type" : field.get_internal_type() |
|
}) |
|
|
|
# Get a "string version" of the object's data (this is handled by the |
|
# serializer base class). |
|
if getattr(obj, field.name) is not None: |
|
value = self.get_string_value(obj, field) |
|
self.xml.characters(smart_unicode(value)) |
|
else: |
|
self.xml.addQuickElement("None") |
|
|
|
self.xml.endElement("field") |
|
|
|
def handle_fk_field(self, obj, field): |
|
""" |
|
Called to handle a ForeignKey (we need to treat them slightly |
|
differently from regular fields). |
|
""" |
|
self._start_relational_field(field) |
|
related = getattr(obj, field.name) |
|
if related is not None: |
|
if field.rel.field_name == related._meta.pk.name: |
|
# Related to remote object via primary key |
|
related = related._get_pk_val() |
|
else: |
|
# Related to remote object via other field |
|
related = getattr(related, field.rel.field_name) |
|
self.xml.characters(smart_unicode(related)) |
|
else: |
|
self.xml.addQuickElement("None") |
|
self.xml.endElement("field") |
|
|
|
def handle_m2m_field(self, obj, field): |
|
""" |
|
Called to handle a ManyToManyField. Related objects are only |
|
serialized as references to the object's PK (i.e. the related *data* |
|
is not dumped, just the relation). |
|
""" |
|
if field.creates_table: |
|
self._start_relational_field(field) |
|
for relobj in getattr(obj, field.name).iterator(): |
|
self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())}) |
|
self.xml.endElement("field") |
|
|
|
def _start_relational_field(self, field): |
|
""" |
|
Helper to output the <field> element for relational fields |
|
""" |
|
self.indent(2) |
|
self.xml.startElement("field", { |
|
"name" : field.name, |
|
"rel" : field.rel.__class__.__name__, |
|
"to" : smart_unicode(field.rel.to._meta), |
|
}) |
|
|
|
class Deserializer(base.Deserializer): |
|
""" |
|
Deserialize XML. |
|
""" |
|
|
|
def __init__(self, stream_or_string, **options): |
|
super(Deserializer, self).__init__(stream_or_string, **options) |
|
self.event_stream = pulldom.parse(self.stream) |
|
|
|
def next(self): |
|
for event, node in self.event_stream: |
|
if event == "START_ELEMENT" and node.nodeName == "object": |
|
self.event_stream.expandNode(node) |
|
return self._handle_object(node) |
|
raise StopIteration |
|
|
|
def _handle_object(self, node): |
|
""" |
|
Convert an <object> node to a DeserializedObject. |
|
""" |
|
# Look up the model using the model loading mechanism. If this fails, |
|
# bail. |
|
Model = self._get_model_from_node(node, "model") |
|
|
|
# Start building a data dictionary from the object. If the node is |
|
# missing the pk attribute, bail. |
|
pk = node.getAttribute("pk") |
|
if not pk: |
|
raise base.DeserializationError("<object> node is missing the 'pk' attribute") |
|
|
|
data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)} |
|
|
|
# Also start building a dict of m2m data (this is saved as |
|
# {m2m_accessor_attribute : [list_of_related_objects]}) |
|
m2m_data = {} |
|
|
|
# Deseralize each field. |
|
for field_node in node.getElementsByTagName("field"): |
|
# If the field is missing the name attribute, bail (are you |
|
# sensing a pattern here?) |
|
field_name = field_node.getAttribute("name") |
|
if not field_name: |
|
raise base.DeserializationError("<field> node is missing the 'name' attribute") |
|
|
|
# Get the field from the Model. This will raise a |
|
# FieldDoesNotExist if, well, the field doesn't exist, which will |
|
# be propagated correctly. |
|
field = Model._meta.get_field(field_name) |
|
|
|
# As is usually the case, relation fields get the special treatment. |
|
if field.rel and isinstance(field.rel, models.ManyToManyRel): |
|
m2m_data[field.name] = self._handle_m2m_field_node(field_node, field) |
|
elif field.rel and isinstance(field.rel, models.ManyToOneRel): |
|
data[field.attname] = self._handle_fk_field_node(field_node, field) |
|
else: |
|
if field_node.getElementsByTagName('None'): |
|
value = None |
|
else: |
|
value = field.to_python(getInnerText(field_node).strip()) |
|
data[field.name] = value |
|
|
|
# Return a DeserializedObject so that the m2m data has a place to live. |
|
return base.DeserializedObject(Model(**data), m2m_data) |
|
|
|
def _handle_fk_field_node(self, node, field): |
|
""" |
|
Handle a <field> node for a ForeignKey |
|
""" |
|
# Check if there is a child node named 'None', returning None if so. |
|
if node.getElementsByTagName('None'): |
|
return None |
|
else: |
|
return field.rel.to._meta.get_field(field.rel.field_name).to_python( |
|
getInnerText(node).strip()) |
|
|
|
def _handle_m2m_field_node(self, node, field): |
|
""" |
|
Handle a <field> node for a ManyToManyField. |
|
""" |
|
return [field.rel.to._meta.pk.to_python( |
|
c.getAttribute("pk")) |
|
for c in node.getElementsByTagName("object")] |
|
|
|
def _get_model_from_node(self, node, attr): |
|
""" |
|
Helper to look up a model from a <object model=...> or a <field |
|
rel=... to=...> node. |
|
""" |
|
model_identifier = node.getAttribute(attr) |
|
if not model_identifier: |
|
raise base.DeserializationError( |
|
"<%s> node is missing the required '%s' attribute" \ |
|
% (node.nodeName, attr)) |
|
try: |
|
Model = models.get_model(*model_identifier.split(".")) |
|
except TypeError: |
|
Model = None |
|
if Model is None: |
|
raise base.DeserializationError( |
|
"<%s> node has invalid model identifier: '%s'" % \ |
|
(node.nodeName, model_identifier)) |
|
return Model |
|
|
|
|
|
def getInnerText(node): |
|
""" |
|
Get all the inner text of a DOM node (recursively). |
|
""" |
|
# inspired by http://mail.python.org/pipermail/xml-sig/2005-March/011022.html |
|
inner_text = [] |
|
for child in node.childNodes: |
|
if child.nodeType == child.TEXT_NODE or child.nodeType == child.CDATA_SECTION_NODE: |
|
inner_text.append(child.data) |
|
elif child.nodeType == child.ELEMENT_NODE: |
|
inner_text.extend(getInnerText(child)) |
|
else: |
|
pass |
|
return u"".join(inner_text) |
|
|
|
|