Commit 412b5fc2 by Tom Christie

Support for nesting resources etc...

--HG--
rename : djangorestframework/tests/resources.py => djangorestframework/tests/serializer.py
parent 323d52e7
......@@ -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)
......
......@@ -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.
"""
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,6 +49,7 @@ class Resource(BaseResource):
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
"""
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
......@@ -183,6 +74,7 @@ class FormResource(Resource):
view, which may be used by some renderers.
"""
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`.
......@@ -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,6 +235,7 @@ class ModelResource(FormResource):
# Auto-register new ModelResource classes into _model_to_resource
#__metaclass__ = _RegisterModelResource
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.
......@@ -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))
"""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')))
"""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'}})
......@@ -75,6 +75,7 @@ class TestNonFieldErrors(TestCase):
return self.cleaned_data #pragma: no cover
class MockResource(FormResource):
class Meta:
form = MockForm
class MockView(View):
......@@ -99,9 +100,11 @@ class TestFormValidation(TestCase):
qwerty = forms.CharField(required=True)
class MockFormResource(FormResource):
class Meta:
form = MockForm
class MockModelResource(ModelResource):
class Meta:
form = MockForm
class MockFormView(View):
......@@ -275,6 +278,7 @@ class TestModelFormValidator(TestCase):
return 'read only'
class MockResource(ModelResource):
class Meta:
model = MockModel
class MockView(View):
......
......@@ -80,6 +80,7 @@ Using Django REST framework can be as simple as adding a few lines to your urlco
from myapp.models import MyModel
class MyResource(ModelResource):
class Meta:
model = MyModel
urlpatterns = patterns('',
......@@ -134,6 +135,7 @@ Library Reference
library/renderers
library/resource
library/response
library/serializer
library/status
library/views
......
:mod:`serializer`
=================
.. module:: serializer
.. autoclass:: serializer::Serializer.Meta
:members:
.. autoclass:: serializer::Serializer
:members:
......@@ -11,6 +11,7 @@ class BlogPostResource(ModelResource):
"""
A Blog Post has a *title* and *content*, and can be associated with zero or more comments.
"""
class Meta:
model = BlogPost
fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
ordering = ('-created',)
......@@ -23,6 +24,7 @@ class CommentResource(ModelResource):
"""
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
"""
class Meta:
model = Comment
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
ordering = ('-created',)
......
......@@ -4,6 +4,7 @@ from djangorestframework.resources import ModelResource
from modelresourceexample.models import MyModel
class MyModelResource(ModelResource):
class Meta:
model = MyModel
fields = ('foo', 'bar', 'baz', 'url')
ordering = ('created',)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment