From c9032ab07f3694f3ae7da9b0017b764248ce28c9 Mon Sep 17 00:00:00 2001
From: Jacob Kaplan-Moss <jacob@jacobian.org>
Date: Thu, 29 Jun 2006 16:42:49 +0000
Subject: [PATCH] Added a JSON serializer, a few more tests, and a couple more
 lines of docs.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@3237 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
 django/core/serializers/__init__.py       |   4 +-
 django/core/serializers/base.py           |   4 +-
 django/core/serializers/json.py           |  51 +++++++++++
 django/core/serializers/python.py         | 101 ++++++++++++++++++++++
 django/core/serializers/xml_serializer.py |  13 ++-
 docs/serialization.txt                    |  23 ++++-
 tests/modeltests/serializers/models.py    |  27 ++++++
 7 files changed, 211 insertions(+), 12 deletions(-)
 create mode 100644 django/core/serializers/json.py
 create mode 100644 django/core/serializers/python.py

diff --git a/django/core/serializers/__init__.py b/django/core/serializers/__init__.py
index 72c4407b59..75e087ee1b 100644
--- a/django/core/serializers/__init__.py
+++ b/django/core/serializers/__init__.py
@@ -20,7 +20,9 @@ from django.conf import settings
 
 # Built-in serializers
 BUILTIN_SERIALIZERS = {
-    "xml" : "django.core.serializers.xml_serializer",
+    "xml"    : "django.core.serializers.xml_serializer",
+    "python" : "django.core.serializers.python",
+    "json"   : "django.core.serializers.json",
 }
 
 _serializers = {}
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py
index 5c84861326..e939c0c6e7 100644
--- a/django/core/serializers/base.py
+++ b/django/core/serializers/base.py
@@ -33,7 +33,9 @@ class Serializer(object):
         for obj in queryset:
             self.start_object(obj)
             for field in obj._meta.fields:
-                if field.rel is None:
+                if field is obj._meta.pk:
+                    continue
+                elif field.rel is None:
                     self.handle_field(obj, field)
                 else:
                     self.handle_fk_field(obj, field)
diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py
new file mode 100644
index 0000000000..dd6513db57
--- /dev/null
+++ b/django/core/serializers/json.py
@@ -0,0 +1,51 @@
+"""
+Serialize data to/from JSON
+"""
+
+import datetime
+from django.utils import simplejson
+from django.core.serializers.python import Serializer as PythonSerializer
+from django.core.serializers.python import Deserializer as PythonDeserializer
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+class Serializer(PythonSerializer):
+    """
+    Convert a queryset to JSON.
+    """
+    def end_serialization(self):
+        simplejson.dump(self.objects, self.stream, cls=DateTimeAwareJSONEncoder)
+        
+    def getvalue(self):
+        return self.stream.getvalue()
+
+def Deserializer(stream_or_string, **options):
+    """
+    Deserialize a stream or string of JSON data.
+    """
+    if isinstance(stream_or_string, basestring):
+        stream = StringIO(stream_or_string)
+    else:
+        stream = stream_or_string
+    for obj in PythonDeserializer(simplejson.load(stream)):
+        yield obj
+        
+class DateTimeAwareJSONEncoder(simplejson.JSONEncoder):
+    """
+    JSONEncoder subclass that knows how to encode date/time types
+    """
+    
+    DATE_FORMAT = "%Y-%m-%d" 
+    TIME_FORMAT = "%H:%M:%S"
+    
+    def default(self, o):
+        if isinstance(o, datetime.date):
+            return o.strftime(self.DATE_FORMAT)
+        elif isinstance(o, datetime.time):
+            return o.strftime(self.TIME_FORMAT)
+        elif isinstance(o, datetime.datetime):
+            return o.strftime("%s %s" % (self.DATE_FORMAT, self.TIME_FORMAT))
+        else:
+            return super(self, DateTimeAwareJSONEncoder).default(o)
\ No newline at end of file
diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py
new file mode 100644
index 0000000000..7989e1d469
--- /dev/null
+++ b/django/core/serializers/python.py
@@ -0,0 +1,101 @@
+"""
+A Python "serializer". Doesn't do much serializing per se -- just converts to
+and from basic Python data types (lists, dicts, strings, etc.). Useful as a basis for
+other serializers.
+"""
+
+from django.conf import settings
+from django.core.serializers import base
+from django.db import models
+
+class Serializer(base.Serializer):
+    """
+    Serializes a QuerySet to basic Python objects.
+    """
+    
+    def start_serialization(self):
+        self._current = None
+        self.objects = []
+        
+    def end_serialization(self):
+        pass
+        
+    def start_object(self, obj):
+        self._current = {}
+        
+    def end_object(self, obj):
+        self.objects.append({
+            "model"  : str(obj._meta),
+            "pk"     : str(obj._get_pk_val()),
+            "fields" : self._current
+        })
+        self._current = None
+        
+    def handle_field(self, obj, field):
+        self._current[field.name] = getattr(obj, field.name)
+        
+    def handle_fk_field(self, obj, field):
+        related = getattr(obj, field.name)
+        if related is not None:
+            related = related._get_pk_val()
+        self._current[field.name] = related
+    
+    def handle_m2m_field(self, obj, field):
+        self._current[field.name] = [related._get_pk_val() for related in getattr(obj, field.name).iterator()]
+    
+    def getvalue(self):
+        return self.objects
+
+def Deserializer(object_list, **options):
+    """
+    Deserialize simple Python objects back into Django ORM instances.
+    
+    It's expected that you pass the Python objects themselves (instead of a
+    stream or a string) to the constructor
+    """
+    models.get_apps()
+    for d in object_list:
+        # Look up the model and starting build a dict of data for it.
+        Model = _get_model(d["model"])
+        data = {Model._meta.pk.name : d["pk"]}
+        m2m_data = {}
+        
+        # Handle each field
+        for (field_name, field_value) in d["fields"].iteritems():
+            if isinstance(field_value, unicode):
+                field_value = field_value.encode(options.get("encoding", settings.DEFAULT_CHARSET))
+                
+            field = Model._meta.get_field(field_name)
+            
+            # Handle M2M relations (with in_bulk() for performance)
+            if field.rel and isinstance(field.rel, models.ManyToManyRel):
+                pks = []
+                for pk in field_value:
+                    if isinstance(pk, unicode):
+                        pk = pk.encode(options.get("encoding", settings.DEFAULT_CHARSET))
+                m2m_data[field.name] = field.rel.to._default_manager.in_bulk(field_value).values()
+                
+            # Handle FK fields
+            elif field.rel and isinstance(field.rel, models.ManyToOneRel):
+                try:
+                    data[field.name] = field.rel.to._default_manager.get(pk=field_value)
+                except RelatedModel.DoesNotExist:
+                    data[field.name] = None
+                    
+            # Handle all other fields
+            else:
+                data[field.name] = field.to_python(field_value)
+                
+        yield base.DeserializedObject(Model(**data), m2m_data)
+
+def _get_model(model_identifier):
+    """
+    Helper to look up a model from an "app_label.module_name" string.
+    """
+    try:
+        Model = models.get_model(*model_identifier.split("."))
+    except TypeError:
+        Model = None
+    if Model is None:
+        raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)
+    return Model
diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
index ab8769f237..09fff408cf 100644
--- a/django/core/serializers/xml_serializer.py
+++ b/django/core/serializers/xml_serializer.py
@@ -2,10 +2,11 @@
 XML serializer.
 """
 
-from xml.dom import pulldom
-from django.utils.xmlutils import SimplerXMLGenerator
+from django.conf import settings
 from django.core.serializers import base
 from django.db import models
+from django.utils.xmlutils import SimplerXMLGenerator
+from xml.dom import pulldom
 
 class Serializer(base.Serializer):
     """
@@ -16,7 +17,7 @@ class Serializer(base.Serializer):
         """
         Start serialization -- open the XML document and the root element.
         """
-        self.xml = SimplerXMLGenerator(self.stream, self.options.get("encoding", "utf-8"))
+        self.xml = SimplerXMLGenerator(self.stream, self.options.get("encoding", settings.DEFAULT_CHARSET))
         self.xml.startDocument()
         self.xml.startElement("django-objects", {"version" : "1.0"})
         
@@ -58,9 +59,7 @@ class Serializer(base.Serializer):
         # Get a "string version" of the object's data (this is handled by the
         # serializer base class).  None is handled specially.
         value = self.get_string_value(obj, field)
-        if value is None:
-            self.xml.addQuickElement("None")
-        else:
+        if value is not None:
             self.xml.characters(str(value))
 
         self.xml.endElement("field")
@@ -106,7 +105,7 @@ class Deserializer(base.Deserializer):
     
     def __init__(self, stream_or_string, **options):
         super(Deserializer, self).__init__(stream_or_string, **options)
-        self.encoding = self.options.get("encoding", "utf-8")
+        self.encoding = self.options.get("encoding", settings.DEFAULT_CHARSET)
         self.event_stream = pulldom.parse(self.stream) 
     
     def next(self):
diff --git a/docs/serialization.txt b/docs/serialization.txt
index 41954b7a0d..25199e7a50 100644
--- a/docs/serialization.txt
+++ b/docs/serialization.txt
@@ -78,8 +78,25 @@ The Django object itself can be inspected as ``deserialized_object.object``.
 Serialization formats
 ---------------------
 
-Django "ships" with a few included serializers, and there's a simple API for creating and registering your own...
+Django "ships" with a few included serializers:
 
-.. note::
+    ==========  ==============================================================
+    Identifier  Information
+    ==========  ==============================================================
+    ``xml``     Serializes to and from a simple XML dialect.
 
-    ... which will be documented once the API is stable :)
+    ``json``    Serializes to and from JSON_ (using a version of simplejson_
+                bundled with Django).
+
+    ``python``  Translates to and from "simple" Python objects (lists, dicts,
+                strings, etc.).  Not really all that useful on its own, but 
+                used as a base for other serializers.
+    ==========  ==============================================================
+
+.. _json: http://json.org/
+.. _simplejson: http://undefined.org/python/#simplejson
+
+Writing custom serializers
+``````````````````````````
+
+XXX ...
diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py
index 8c9483beba..ccf565c365 100644
--- a/tests/modeltests/serializers/models.py
+++ b/tests/modeltests/serializers/models.py
@@ -91,4 +91,31 @@ API_TESTS = """
 >>> Article.objects.all()
 [<Article: Poker has no place on television>, <Article: Time to reform copyright>]
 
+# Django also ships with a built-in JSON serializers
+>>> json = serializers.serialize("json", Category.objects.filter(pk=2))
+>>> json
+'[{"pk": "2", "model": "serializers.category", "fields": {"name": "Music"}}]'
+
+# You can easily create new objects by deserializing data with an empty PK
+# (It's easier to demo this with JSON...)
+>>> new_author_json = '[{"pk": null, "model": "serializers.author", "fields": {"name": "Bill"}}]'
+>>> for obj in serializers.deserialize("json", new_author_json):
+...     obj.save()
+>>> Author.objects.all()
+[<Author: Bill>, <Author: Jane>, <Author: Joe>]
+
+# All the serializers work the same
+>>> json = serializers.serialize("json", Article.objects.all())
+>>> for obj in serializers.deserialize("json", json):
+...     print obj
+<DeserializedObject: Poker has no place on television>
+<DeserializedObject: Time to reform copyright>
+
+>>> json = json.replace("Poker has no place on television", "Just kidding; I love TV poker")
+>>> for obj in serializers.deserialize("json", json):
+...     obj.save()
+
+>>> Article.objects.all()
+[<Article: Just kidding; I love TV poker>, <Article: Time to reform copyright>]
+
 """