aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--djangorestframework/mixins.py7
-rw-r--r--djangorestframework/resources.py245
-rw-r--r--djangorestframework/serializer.py347
-rw-r--r--djangorestframework/tests/resources.py60
-rw-r--r--djangorestframework/tests/serializer.py127
-rw-r--r--djangorestframework/tests/validators.py12
-rw-r--r--djangorestframework/views.py4
-rw-r--r--docs/index.rst4
-rw-r--r--docs/library/serializer.rst10
-rw-r--r--examples/blogpost/urls.py14
-rw-r--r--examples/modelresourceexample/urls.py7
11 files changed, 584 insertions, 253 deletions
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 11e3bb38..75fd3646 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -582,7 +582,12 @@ class ListModelMixin(object):
def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.resource.model.objects.all()
- ordering = getattr(self.resource, 'ordering', None)
+
+ if hasattr(self, 'resource'):
+ ordering = getattr(self.resource.Meta, 'ordering', None)
+ else:
+ ordering = None
+
if ordering:
args = as_tuple(ordering)
queryset = queryset.order_by(*args)
diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py
index 07c97d43..7a4f31d5 100644
--- a/djangorestframework/resources.py
+++ b/djangorestframework/resources.py
@@ -6,6 +6,7 @@ from django.db.models.fields.related import RelatedField
from django.utils.encoding import smart_unicode
from djangorestframework.response import ErrorResponse
+from djangorestframework.serializer import Serializer
from djangorestframework.utils import as_tuple
import decimal
@@ -13,130 +14,19 @@ import inspect
import re
-# TODO: _IgnoreFieldException
-# Map model classes to resource classes
-#_model_to_resource = {}
-
-def _model_to_dict(instance, resource=None):
- """
- Given a model instance, return a ``dict`` representing the model.
-
- The implementation is similar to Django's ``django.forms.model_to_dict``, except:
-
- * It doesn't coerce related objects into primary keys.
- * It doesn't drop ``editable=False`` fields.
- * It also supports attribute or method fields on the instance or resource.
- """
- opts = instance._meta
- data = {}
-
- #print [rel.name for rel in opts.get_all_related_objects()]
- #related = [rel.get_accessor_name() for rel in opts.get_all_related_objects()]
- #print [getattr(instance, rel) for rel in related]
- #if resource.fields:
- # fields = resource.fields
- #else:
- # fields = set(opts.fields + opts.many_to_many)
-
- fields = resource and resource.fields or ()
- include = resource and resource.include or ()
- exclude = resource and resource.exclude or ()
-
- extra_fields = fields and list(fields) or list(include)
-
- # Model fields
- for f in opts.fields + opts.many_to_many:
- if fields and not f.name in fields:
- continue
- if exclude and f.name in exclude:
- continue
- if isinstance(f, models.ForeignKey):
- data[f.name] = getattr(instance, f.name)
- else:
- data[f.name] = f.value_from_object(instance)
-
- if extra_fields and f.name in extra_fields:
- extra_fields.remove(f.name)
-
- # Method fields
- for fname in extra_fields:
-
- if isinstance(fname, (tuple, list)):
- fname, fields = fname
- else:
- fname, fields = fname, False
-
- try:
- if hasattr(resource, fname):
- # check the resource first, to allow it to override fields
- obj = getattr(resource, fname)
- # if it's a method like foo(self, instance), then call it
- if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) == 2:
- obj = obj(instance)
- elif hasattr(instance, fname):
- # now check the object instance
- obj = getattr(instance, fname)
- else:
- continue
-
- # TODO: It would be nicer if this didn't recurse here.
- # Let's keep _model_to_dict flat, and _object_to_data recursive.
- if fields:
- Resource = type('Resource', (object,), {'fields': fields,
- 'include': (),
- 'exclude': ()})
- data[fname] = _object_to_data(obj, Resource())
- else:
- data[fname] = _object_to_data(obj)
-
- except NoReverseMatch:
- # Ug, bit of a hack for now
- pass
-
- return data
-
-
-def _object_to_data(obj, resource=None):
- """
- Convert an object into a serializable representation.
- """
- if isinstance(obj, dict):
- # dictionaries
- # TODO: apply same _model_to_dict logic fields/exclude here
- return dict([ (key, _object_to_data(val)) for key, val in obj.iteritems() ])
- if isinstance(obj, (tuple, list, set, QuerySet)):
- # basic iterables
- return [_object_to_data(item, resource) for item in obj]
- if isinstance(obj, models.Manager):
- # Manager objects
- return [_object_to_data(item, resource) for item in obj.all()]
- if isinstance(obj, models.Model):
- # Model instances
- return _object_to_data(_model_to_dict(obj, resource))
- if isinstance(obj, decimal.Decimal):
- # Decimals (force to string representation)
- return str(obj)
- if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
- # function with no args
- return _object_to_data(obj(), resource)
- if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
- # bound method
- return _object_to_data(obj(), resource)
-
- return smart_unicode(obj, strings_only=True)
-
-
-class BaseResource(object):
+class BaseResource(Serializer):
"""
Base class for all Resource classes, which simply defines the interface they provide.
"""
- fields = None
- include = None
- exclude = None
+ class Meta:
+ fields = None
+ include = None
+ exclude = None
- def __init__(self, view):
+ def __init__(self, view, depth=None, stack=[], **kwargs):
+ super(BaseResource, self).__init__(depth, stack, **kwargs)
self.view = view
def validate_request(self, data, files=None):
@@ -150,7 +40,7 @@ class BaseResource(object):
"""
Given the response content, filter it into a serializable object.
"""
- return _object_to_data(obj, self)
+ return self.serialize(obj)
class Resource(BaseResource):
@@ -159,19 +49,20 @@ class Resource(BaseResource):
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
"""
- # The model attribute refers to the Django Model which this Resource maps to.
- # (The Model's class, rather than an instance of the Model)
- model = None
-
- # By default the set of returned fields will be the set of:
- #
- # 0. All the fields on the model, excluding 'id'.
- # 1. All the properties on the model.
- # 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
- #
- # If you wish to override this behaviour,
- # you should explicitly set the fields attribute on your class.
- fields = None
+ class Meta:
+ # The model attribute refers to the Django Model which this Resource maps to.
+ # (The Model's class, rather than an instance of the Model)
+ model = None
+
+ # By default the set of returned fields will be the set of:
+ #
+ # 0. All the fields on the model, excluding 'id'.
+ # 1. All the properties on the model.
+ # 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
+ #
+ # If you wish to override this behaviour,
+ # you should explicitly set the fields attribute on your class.
+ fields = None
class FormResource(Resource):
@@ -183,11 +74,12 @@ class FormResource(Resource):
view, which may be used by some renderers.
"""
- """
- The :class:`Form` class that should be used for request validation.
- This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
- """
- form = None
+ class Meta:
+ """
+ The :class:`Form` class that should be used for request validation.
+ This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
+ """
+ form = None
def validate_request(self, data, files=None):
@@ -297,7 +189,7 @@ class FormResource(Resource):
"""
# A form on the view overrides a form on the resource.
- form = getattr(self.view, 'form', self.form)
+ form = getattr(self.view, 'form', None) or self.Meta.form
# Use the requested method or determine the request method
if method is None and hasattr(self.view, 'request') and hasattr(self.view, 'method'):
@@ -343,43 +235,44 @@ class ModelResource(FormResource):
# Auto-register new ModelResource classes into _model_to_resource
#__metaclass__ = _RegisterModelResource
- """
- The form class that should be used for request validation.
- If set to :const:`None` then the default model form validation will be used.
-
- This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
- """
- form = None
-
- """
- The model class which this resource maps to.
-
- This can be overridden by a :attr:`model` attribute on the :class:`views.View`.
- """
- model = None
-
- """
- The list of fields to use on the output.
+ class Meta:
+ """
+ The form class that should be used for request validation.
+ If set to :const:`None` then the default model form validation will be used.
- May be any of:
+ This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
+ """
+ form = None
- The name of a model field.
- The name of an attribute on the model.
- The name of an attribute on the resource.
- The name of a method on the model, with a signature like ``func(self)``.
- The name of a method on the resource, with a signature like ``func(self, instance)``.
- """
- fields = None
+ """
+ The model class which this resource maps to.
- """
- The list of fields to exclude. This is only used if :attr:`fields` is not set.
- """
- exclude = ('id', 'pk')
+ This can be overridden by a :attr:`model` attribute on the :class:`views.View`.
+ """
+ model = None
- """
- The list of extra fields to include. This is only used if :attr:`fields` is not set.
- """
- include = ('url',)
+ """
+ The list of fields to use on the output.
+
+ May be any of:
+
+ The name of a model field.
+ The name of an attribute on the model.
+ The name of an attribute on the resource.
+ The name of a method on the model, with a signature like ``func(self)``.
+ The name of a method on the resource, with a signature like ``func(self, instance)``.
+ """
+ fields = None
+
+ """
+ The list of fields to exclude. This is only used if :attr:`fields` is not set.
+ """
+ exclude = ('id', 'pk')
+
+ """
+ The list of extra fields to include. This is only used if :attr:`fields` is not set.
+ """
+ include = ('url',)
def __init__(self, view):
@@ -390,8 +283,8 @@ class ModelResource(FormResource):
"""
super(ModelResource, self).__init__(view)
- if getattr(view, 'model', None):
- self.model = view.model
+ self.model = getattr(view, 'model', None) or self.Meta.model
+
def validate_request(self, data, files=None):
"""
@@ -506,7 +399,7 @@ class ModelResource(FormResource):
isinstance(getattr(self.model, attr, None), property)
and not attr.startswith('_'))
- if self.fields:
- return property_fields & set(as_tuple(self.fields))
+ if self.Meta.fields:
+ return property_fields & set(as_tuple(self.Meta.fields))
- return property_fields.union(set(as_tuple(self.include))) - set(as_tuple(self.exclude))
+ return property_fields.union(set(as_tuple(self.Meta.include))) - set(as_tuple(self.Meta.exclude))
diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py
new file mode 100644
index 00000000..47c631bc
--- /dev/null
+++ b/djangorestframework/serializer.py
@@ -0,0 +1,347 @@
+"""
+Customizable serialization.
+"""
+from django.db import models
+from django.db.models.query import QuerySet
+from django.db.models.fields.related import RelatedField
+from django.utils.encoding import smart_unicode
+
+import decimal
+import inspect
+import types
+
+
+# We register serializer classes, so that we can refer to them by their
+# class names, if there are cyclical serialization heirachys.
+_serializers = {}
+
+
+def _field_to_tuple(field):
+ """
+ Convert an item in the `fields` attribute into a 2-tuple.
+ """
+ if isinstance(field, (tuple, list)):
+ return (field[0], field[1])
+ return (field, None)
+
+def _fields_to_list(fields):
+ """
+ Return a list of field names.
+ """
+ return [_field_to_tuple(field)[0] for field in fields or ()]
+
+def _fields_to_dict(fields):
+ """
+ Return a `dict` of field name -> None, or tuple of fields, or Serializer class
+ """
+ return dict([_field_to_tuple(field) for field in fields or ()])
+
+
+class _SkipField(Exception):
+ """
+ Signals that a serialized field should be ignored.
+ We use this mechanism as the default behavior for ensuring
+ that we don't infinitely recurse when dealing with nested data.
+ """
+ pass
+
+
+class _BuildInnerMeta(type):
+ """
+ Some magic so that an inner Meta class gets inheriting behavior.
+ """
+ def __new__(cls, name, bases, attrs):
+ # Get a list of all the inner Metas, from the bases upwards.
+ inner_metas = [getattr(base, 'Meta', object) for base in bases]
+ inner_metas.append(attrs.get('Meta', object))
+
+ # Build up the attributes on the inner Meta.
+ meta_attrs = {}
+ [meta_attrs.update(inner_meta.__dict__) for inner_meta in inner_metas]
+
+ # Drop private attributes.
+ meta_attrs = dict([ (key, val) for (key, val) in meta_attrs.items()
+ if not key.startswith('_') ])
+
+ # Lovely, now we can create our inner Meta class.
+ attrs['Meta'] = type('Meta', (object,), meta_attrs)
+
+ # Build the class and register it.
+ ret = super(_BuildInnerMeta, cls).__new__(cls, name, bases, attrs)
+ _serializers[name] = ret
+
+ return ret
+
+
+class Serializer(object):
+ """
+ Converts python objects into plain old native types suitable for
+ serialization. In particular it handles models and querysets.
+
+ The output format is specified by setting a number of attributes on the
+ inner `Meta` class.
+
+ You may also override any of the serialization methods, to provide
+ for more flexible behavior.
+
+ Valid output types include anything that may be directly rendered into
+ json, xml etc...
+ """
+ __metaclass__ = _BuildInnerMeta
+
+ class Meta:
+ """
+ Information on how to serialize the data.
+ """
+
+ fields = ()
+ """
+ Specify the fields to be serialized on a model or dict.
+ Overrides `include` and `exclude`.
+ """
+
+ include = ()
+ """
+ Fields to add to the default set to be serialized on a model/dict.
+ """
+
+ exclude = ()
+ """
+ Fields to remove from the default set to be serialized on a model/dict.
+ """
+
+ rename = {}
+ """
+ A dict of key->name to use for the field keys.
+ """
+
+ related_serializer = None
+ """
+ The default serializer class to use for any related models.
+ """
+
+ depth = None
+ """
+ The maximum depth to serialize to, or `None`.
+ """
+
+
+ def __init__(self, depth=None, stack=[], **kwargs):
+ """
+ Allow `Meta` items to be set on init.
+ """
+ self.depth = depth
+ self.stack = stack
+
+ if self.depth is None:
+ self.depth = self.Meta.depth
+
+ for (key, val) in kwargs.items():
+ setattr(self.Meta, key, val)
+
+
+ def get_fields(self, obj):
+ """
+ Return the set of field names/keys to use for a model instance/dict.
+ """
+ fields = self.Meta.fields
+
+ # If Meta.fields is not set, we use the default fields and modify
+ # them with Meta.include and Meta.exclude
+ if not fields:
+ default = self.get_default_fields(obj)
+ include = self.Meta.include or ()
+ exclude = self.Meta.exclude or ()
+ fields = set(default + list(include)) - set(exclude)
+
+ else:
+ fields = _fields_to_list(self.Meta.fields)
+
+ return fields
+
+
+ def get_default_fields(self, obj):
+ """
+ Return the default list of field names/keys for a model instance/dict.
+ These are used if `Meta.fields` is not given.
+ """
+ if isinstance(obj, models.Model):
+ opts = obj._meta
+ return [field.name for field in opts.fields + opts.many_to_many]
+ else:
+ return obj.keys()
+
+
+ def get_related_serializer(self, key):
+ info = _fields_to_dict(self.Meta.fields).get(key, None)
+
+ # If an element in `fields` is a 2-tuple of (str, tuple)
+ # then the second element of the tuple is the fields to
+ # set on the related serializer
+ if isinstance(info, (list, tuple)):
+ class OnTheFlySerializer(Serializer):
+ class Meta:
+ fields = info
+ return OnTheFlySerializer
+
+ # If an element in `fields` is a 2-tuple of (str, Serializer)
+ # then the second element of the tuple is the Serializer
+ # class to use for that field.
+ elif isinstance(info, type) and issubclass(info, Serializer):
+ return info
+
+ # If an element in `fields` is a 2-tuple of (str, str)
+ # then the second element of the tuple is the name of the Serializer
+ # class to use for that field.
+ #
+ # Black magic to deal with cyclical Serializer dependancies.
+ # Similar to what Django does for cyclically related models.
+ elif isinstance(info, str) and info in _serializers:
+ return _serializers[info]
+
+ # Otherwise use `related_serializer` or fall back to `Serializer`
+ return getattr(self.Meta, 'related_serializer') or Serializer
+
+
+ def serialize_key(self, key):
+ """
+ Keys serialize to their string value,
+ unless they exist in the `Meta.rename` dict.
+ """
+ return getattr(self.Meta.rename, key, key)
+
+
+ def serialize_val(self, key, obj):
+ """
+ Convert a model field or dict value into a serializable representation.
+ """
+ related_serializer = self.get_related_serializer(key)
+
+ if self.depth is None:
+ depth = None
+ elif self.depth <= 0:
+ return self.serialize_max_depth(obj)
+ else:
+ depth = self.depth - 1
+
+ if any([obj is elem for elem in self.stack]):
+ return self.serialize_recursion(obj)
+ else:
+ stack = self.stack[:]
+ stack.append(obj)
+
+ return related_serializer(depth=depth, stack=stack).serialize(obj)
+
+
+ def serialize_max_depth(self, obj):
+ """
+ Determine how objects should be serialized once `depth` is exceeded.
+ The default behavior is to ignore the field.
+ """
+ raise _SkipField
+
+
+ def serialize_recursion(self, obj):
+ """
+ Determine how objects should be serialized if recursion occurs.
+ The default behavior is to ignore the field.
+ """
+ raise _SkipField
+
+
+ def serialize_model(self, instance):
+ """
+ Given a model instance or dict, serialize it to a dict, using
+ the given behavior given on Meta.
+ """
+ data = {}
+
+ fields = self.get_fields(instance)
+
+ # serialize each required field
+ for fname in fields:
+ if hasattr(self, fname):
+ # check for a method 'fname' on self first
+ meth = getattr(self, fname)
+ if inspect.ismethod(meth) and len(inspect.getargspec(meth)[0]) == 2:
+ obj = meth(instance)
+ elif hasattr(instance, fname):
+ # now check for an attribute 'fname' on the instance
+ obj = getattr(instance, fname)
+ elif fname in instance:
+ # finally check for a key 'fname' on the instance
+ obj = instance[fname]
+ else:
+ continue
+
+ try:
+ key = self.serialize_key(fname)
+ val = self.serialize_val(fname, obj)
+ data[key] = val
+ except _SkipField:
+ pass
+
+ return data
+
+
+ def serialize_iter(self, obj):
+ """
+ Convert iterables into a serializable representation.
+ """
+ return [self.serialize(item) for item in obj]
+
+
+ def serialize_func(self, obj):
+ """
+ Convert no-arg methods and functions into a serializable representation.
+ """
+ return self.serialize(obj())
+
+
+ def serialize_manager(self, obj):
+ """
+ Convert a model manager into a serializable representation.
+ """
+ return self.serialize_iter(obj.all())
+
+
+ def serialize_decimal(self, obj):
+ """
+ Convert a Decimal instance into a serializable representation.
+ """
+ return str(obj)
+
+
+ def serialize_fallback(self, obj):
+ """
+ Convert any unhandled object into a serializable representation.
+ """
+ return smart_unicode(obj, strings_only=True)
+
+
+ def serialize(self, obj):
+ """
+ Convert any object into a serializable representation.
+ """
+
+ if isinstance(obj, (dict, models.Model)):
+ # Model instances & dictionaries
+ return self.serialize_model(obj)
+ elif isinstance(obj, (tuple, list, set, QuerySet, types.GeneratorType)):
+ # basic iterables
+ return self.serialize_iter(obj)
+ elif isinstance(obj, models.Manager):
+ # Manager objects
+ return self.serialize_manager(obj)
+ elif isinstance(obj, decimal.Decimal):
+ # Decimals (force to string representation)
+ return self.serialize_decimal(obj)
+ elif inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
+ # function with no args
+ return self.serialize_func(obj)
+ elif inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
+ # bound method
+ return self.serialize_func(obj)
+
+ # fall back to smart unicode
+ return self.serialize_fallback(obj)
diff --git a/djangorestframework/tests/resources.py b/djangorestframework/tests/resources.py
deleted file mode 100644
index 088e3159..00000000
--- a/djangorestframework/tests/resources.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""Tests for the resource module"""
-from django.test import TestCase
-from djangorestframework.resources import _object_to_data
-
-from django.db import models
-
-import datetime
-import decimal
-
-class TestObjectToData(TestCase):
- """Tests for the _object_to_data function"""
-
- def test_decimal(self):
- """Decimals need to be converted to a string representation."""
- self.assertEquals(_object_to_data(decimal.Decimal('1.5')), '1.5')
-
- def test_function(self):
- """Functions with no arguments should be called."""
- def foo():
- return 1
- self.assertEquals(_object_to_data(foo), 1)
-
- def test_method(self):
- """Methods with only a ``self`` argument should be called."""
- class Foo(object):
- def foo(self):
- return 1
- self.assertEquals(_object_to_data(Foo().foo), 1)
-
- def test_datetime(self):
- """datetime objects are left as-is."""
- now = datetime.datetime.now()
- self.assertEquals(_object_to_data(now), now)
-
- def test_tuples(self):
- """ Test tuple serialisation """
- class M1(models.Model):
- field1 = models.CharField()
- field2 = models.CharField()
-
- class M2(models.Model):
- field = models.OneToOneField(M1)
-
- class M3(models.Model):
- field = models.ForeignKey(M1)
-
- m1 = M1(field1='foo', field2='bar')
- m2 = M2(field=m1)
- m3 = M3(field=m1)
-
- Resource = type('Resource', (object,), {'fields':(), 'include':(), 'exclude':()})
-
- r = Resource()
- r.fields = (('field', ('field1')),)
-
- self.assertEqual(_object_to_data(m2, r), dict(field=dict(field1=u'foo')))
-
- r.fields = (('field', ('field2')),)
- self.assertEqual(_object_to_data(m3, r), dict(field=dict(field2=u'bar')))
-
diff --git a/djangorestframework/tests/serializer.py b/djangorestframework/tests/serializer.py
new file mode 100644
index 00000000..cf00b104
--- /dev/null
+++ b/djangorestframework/tests/serializer.py
@@ -0,0 +1,127 @@
+"""Tests for the resource module"""
+from django.test import TestCase
+from djangorestframework.serializer import Serializer
+
+from django.db import models
+
+import datetime
+import decimal
+
+class TestObjectToData(TestCase):
+ """
+ Tests for the Serializer class.
+ """
+
+ def setUp(self):
+ self.serializer = Serializer()
+ self.serialize = self.serializer.serialize
+
+ def test_decimal(self):
+ """Decimals need to be converted to a string representation."""
+ self.assertEquals(self.serialize(decimal.Decimal('1.5')), '1.5')
+
+ def test_function(self):
+ """Functions with no arguments should be called."""
+ def foo():
+ return 1
+ self.assertEquals(self.serialize(foo), 1)
+
+ def test_method(self):
+ """Methods with only a ``self`` argument should be called."""
+ class Foo(object):
+ def foo(self):
+ return 1
+ self.assertEquals(self.serialize(Foo().foo), 1)
+
+ def test_datetime(self):
+ """
+ datetime objects are left as-is.
+ """
+ now = datetime.datetime.now()
+ self.assertEquals(self.serialize(now), now)
+
+
+class TestFieldNesting(TestCase):
+ """
+ Test nesting the fields in the Serializer class
+ """
+ def setUp(self):
+ self.serializer = Serializer()
+ self.serialize = self.serializer.serialize
+
+ class M1(models.Model):
+ field1 = models.CharField()
+ field2 = models.CharField()
+
+ class M2(models.Model):
+ field = models.OneToOneField(M1)
+
+ class M3(models.Model):
+ field = models.ForeignKey(M1)
+
+ self.m1 = M1(field1='foo', field2='bar')
+ self.m2 = M2(field=self.m1)
+ self.m3 = M3(field=self.m1)
+
+
+ def test_tuple_nesting(self):
+ """
+ Test tuple nesting on `fields` attr
+ """
+ class SerializerM2(Serializer):
+ class Meta:
+ fields = (('field', ('field1',)),)
+
+ class SerializerM3(Serializer):
+ class Meta:
+ fields = (('field', ('field2',)),)
+
+ self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
+ self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
+
+
+ def test_serializer_class_nesting(self):
+ """
+ Test related model serialization
+ """
+ class NestedM2(Serializer):
+ class Meta:
+ fields = ('field1', )
+
+ class NestedM3(Serializer):
+ class Meta:
+ fields = ('field2', )
+
+ class SerializerM2(Serializer):
+ class Meta:
+ fields = [('field', NestedM2)]
+
+ class SerializerM3(Serializer):
+ class Meta:
+ fields = [('field', NestedM3)]
+
+ self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
+ self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
+
+ def test_serializer_classname_nesting(self):
+ """
+ Test related model serialization
+ """
+ class SerializerM2(Serializer):
+ class Meta:
+ fields = [('field', 'NestedM2')]
+
+ class SerializerM3(Serializer):
+ class Meta:
+ fields = [('field', 'NestedM3')]
+
+ class NestedM2(Serializer):
+ class Meta:
+ fields = ('field1', )
+
+ class NestedM3(Serializer):
+ class Meta:
+ fields = ('field2', )
+
+ self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
+ self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
index a1e5d2d7..45b75c4f 100644
--- a/djangorestframework/tests/validators.py
+++ b/djangorestframework/tests/validators.py
@@ -75,7 +75,8 @@ class TestNonFieldErrors(TestCase):
return self.cleaned_data #pragma: no cover
class MockResource(FormResource):
- form = MockForm
+ class Meta:
+ form = MockForm
class MockView(View):
pass
@@ -99,10 +100,12 @@ class TestFormValidation(TestCase):
qwerty = forms.CharField(required=True)
class MockFormResource(FormResource):
- form = MockForm
+ class Meta:
+ form = MockForm
class MockModelResource(ModelResource):
- form = MockForm
+ class Meta:
+ form = MockForm
class MockFormView(View):
resource = MockFormResource
@@ -275,7 +278,8 @@ class TestModelFormValidator(TestCase):
return 'read only'
class MockResource(ModelResource):
- model = MockModel
+ class Meta:
+ model = MockModel
class MockView(View):
resource = MockResource
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 6f2ab5b7..49d722c5 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -150,8 +150,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
- return self.render(response)
-
+ return self.render(response)
+
class ModelView(View):
"""A RESTful view that maps to a model in the database."""
diff --git a/docs/index.rst b/docs/index.rst
index 481bdfe2..2bd37a85 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -80,7 +80,8 @@ Using Django REST framework can be as simple as adding a few lines to your urlco
from myapp.models import MyModel
class MyResource(ModelResource):
- model = MyModel
+ class Meta:
+ model = MyModel
urlpatterns = patterns('',
url(r'^$', ListOrCreateModelView.as_view(resource=MyResource)),
@@ -134,6 +135,7 @@ Library Reference
library/renderers
library/resource
library/response
+ library/serializer
library/status
library/views
diff --git a/docs/library/serializer.rst b/docs/library/serializer.rst
new file mode 100644
index 00000000..7942c506
--- /dev/null
+++ b/docs/library/serializer.rst
@@ -0,0 +1,10 @@
+:mod:`serializer`
+=================
+
+.. module:: serializer
+
+.. autoclass:: serializer::Serializer.Meta
+ :members:
+
+.. autoclass:: serializer::Serializer
+ :members:
diff --git a/examples/blogpost/urls.py b/examples/blogpost/urls.py
index c677b8fa..6fde612e 100644
--- a/examples/blogpost/urls.py
+++ b/examples/blogpost/urls.py
@@ -11,9 +11,10 @@ class BlogPostResource(ModelResource):
"""
A Blog Post has a *title* and *content*, and can be associated with zero or more comments.
"""
- model = BlogPost
- fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
- ordering = ('-created',)
+ class Meta:
+ model = BlogPost
+ fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
+ ordering = ('-created',)
def comments(self, instance):
return reverse('comments', kwargs={'blogpost': instance.key})
@@ -23,9 +24,10 @@ class CommentResource(ModelResource):
"""
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
"""
- model = Comment
- fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
- ordering = ('-created',)
+ class Meta:
+ model = Comment
+ fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
+ ordering = ('-created',)
urlpatterns = patterns('',
diff --git a/examples/modelresourceexample/urls.py b/examples/modelresourceexample/urls.py
index bb71ddd3..86960367 100644
--- a/examples/modelresourceexample/urls.py
+++ b/examples/modelresourceexample/urls.py
@@ -4,9 +4,10 @@ from djangorestframework.resources import ModelResource
from modelresourceexample.models import MyModel
class MyModelResource(ModelResource):
- model = MyModel
- fields = ('foo', 'bar', 'baz', 'url')
- ordering = ('created',)
+ class Meta:
+ model = MyModel
+ fields = ('foo', 'bar', 'baz', 'url')
+ ordering = ('created',)
urlpatterns = patterns('',
url(r'^$', ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'),