diff options
| -rw-r--r-- | djangorestframework/mixins.py | 7 | ||||
| -rw-r--r-- | djangorestframework/resources.py | 245 | ||||
| -rw-r--r-- | djangorestframework/serializer.py | 347 | ||||
| -rw-r--r-- | djangorestframework/tests/resources.py | 60 | ||||
| -rw-r--r-- | djangorestframework/tests/serializer.py | 127 | ||||
| -rw-r--r-- | djangorestframework/tests/validators.py | 12 | ||||
| -rw-r--r-- | djangorestframework/views.py | 4 | ||||
| -rw-r--r-- | docs/index.rst | 4 | ||||
| -rw-r--r-- | docs/library/serializer.rst | 10 | ||||
| -rw-r--r-- | examples/blogpost/urls.py | 14 | ||||
| -rw-r--r-- | examples/modelresourceexample/urls.py | 7 | 
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'),  | 
