This commit is contained in:
Mjumbe Poe 2012-09-09 14:31:28 -07:00
commit 51860dddb0
6 changed files with 348 additions and 29 deletions

View File

@ -0,0 +1,40 @@
from djangorestframework.serializers import ModelSerializer
from djangorestframework.generics import RootAPIView, InstanceAPIView
class ModelResource (object):
serializer_class = ModelSerializer
collection_view_class = RootAPIView
instance_view_class = InstanceAPIView
# The collection_name is the path at the root of the resource. For
# example, say we have a Dog model, and a dog with id=1:
#
# http://api.example.com/dogs/1/
#
# The collection name is 'dogs'. This will default to the plural name
# for the model.
#
collection_name = None
# The instance_name is the name of one model instance, and is used as a
# prefix for internal URL names. For example, for out Dog model with
# instance_name 'dog', may have the following urls:
#
# url('dogs/', collection_view, name='dog_collection'),
# url('dogs/(P<pk>[^/])/)', instance_view, name='dog_instance'),
#
instance_name = None
# The id_field_name is the name of the field that will identify a
# resource in the collection. For example, if we wanted our dogs
# identified by a 'slug' field, we would have:
#
# url('dogs/(P<slug>[^/])/)', instance_view, name='dog_instance'),
#
# and:
#
# http://api.example.com/dogs/fido/
#
# The default value is 'pk'.
#
id_field_name = 'pk'

View File

@ -0,0 +1,92 @@
from django.conf.urls.defaults import patterns, url
# Note, these live in django.conf.urls since 1.4, and will no longer be
# available from django.conf.urls.defaults in 1.6.
from djangorestframework.resources import ModelResource
class DefaultResourceRouter (object):
def __init__(self, default_resource=ModelResource):
self.default_resource = default_resource
self._registry = []
@property
def urls(self):
"""
Return a urlpatterns object suitable for including. I.e.:
urlpatterns = patterns('',
...
url('^api/', include(router.urls, namespace=...)),
...
)
"""
return patterns('', *self.get_urls())
def get_urls(self):
"""
Return a list of urls for all registered resources.
"""
urls = []
for model, resource in self._registry:
urls += self.make_patterns(
model, resource, resource.id_field_name,
resource.collection_name, resource.instance_name
)
return urls
def make_patterns(self, model, resource, id_field_name=None,
collection_name=None, instance_name=None):
"""
Get the URL patterns for the given model and resource. By default,
this will return pair of urls -- one for the collection of resources
representing the model, and one for individual instances of the model.
"""
patterns = []
if collection_name is None:
collection_name = unicode(model._meta.verbose_name_plural)
if instance_name is None:
instance_name = unicode(model._meta.verbose_name)
if id_field_name is None:
id_field_name = u'pk'
# The collection
if resource.collection_view_class:
class CollectionView (resource, resource.collection_view_class):
pass
collection_view = CollectionView.as_view()
url_string = '^{0}/$'.format(collection_name)
patterns.append(
url(url_string, collection_view,
name='{0}_collection'.format(instance_name))
)
# The instance
if resource.instance_view_class:
class InstanceView (resource, resource.instance_view_class):
pass
instance_view = InstanceView.as_view()
url_string = '^{0}/(?P<{1}>[^/]+)/$'.format(collection_name, id_field_name)
patterns.append(
url(url_string, instance_view,
name='{0}_instance'.format(instance_name))
)
return patterns
def register(self, model, resource=None):
"""
Register a new resource with the API. By default a generic
ModelResource will be used for the given model.
"""
resource = resource or self.default_resource
self._registry.append((model, resource))

View File

@ -91,6 +91,9 @@ INSTALLED_APPS = (
# 'django.contrib.admindocs',
'djangorestframework',
'djangorestframework.tokenauth',
# Load up the models
'djangorestframework.tests'
)
STATIC_URL = '/static/'

View File

@ -19,10 +19,18 @@ class CustomUser(models.Model):
class UserGroupMap(models.Model):
user = models.ForeignKey(to=CustomUser)
group = models.ForeignKey(to=Group)
group = models.ForeignKey(to=Group)
@models.permalink
def get_absolute_url(self):
return ('user_group_map', (), {
'pk': self.id
})
class Company(models.Model):
name = models.CharField(max_length=20)
class Employee(models.Model):
employee_id = models.CharField(max_length=20, primary_key=True)

View File

@ -0,0 +1,140 @@
from django.test.testcases import TestCase
from djangorestframework.generics import RootAPIView, InstanceAPIView, ListAPIView, DetailAPIView
from djangorestframework.resources import ModelResource
from djangorestframework.routers import DefaultResourceRouter
from djangorestframework.serializers import Serializer, ModelSerializer
from djangorestframework.tests.models import Company, Employee
from django.conf.urls.defaults import patterns, url, include
from django.core.urlresolvers import reverse, NoReverseMatch
import random
import string
__all__ = ('DefaultResourceRouterTestCase',)
class DummyUrlConfModule(object):
def __init__(self, object_with_urls):
self._object_with_urls = object_with_urls
@property
def urlpatterns(self):
urlpatterns = patterns('',
url(r'^', include(self._object_with_urls.urls, namespace='api')),
)
return urlpatterns
class DefaultResourceRouterTestCase(TestCase):
def setUp(self):
self.api = DefaultResourceRouter()
self.urlconfmodule = DummyUrlConfModule(self.api)
def test_list_view(self):
# Check that the URL gets registered
self.api.register(Company)
list_url = reverse('api:company_collection', urlconf=self.urlconfmodule)
self.assertEqual(list_url, '/companys/')
def test_instance_view(self):
self.api.register(Company)
company = Company(name='Acme Ltd')
company.save()
# Check that the URL gets registered
instance_url = reverse(
'api:company_instance', urlconf=self.urlconfmodule,
kwargs={'pk':company.id},
)
self.assertEqual(instance_url, '/companys/' + str(company.id) + '/')
def test_instance_view_with_nonumeric_primary_key(self):
"""
Check that the api can properly reverse urls for models with
non-numeric primary keys
"""
self.api.register(Employee)
employee = Employee(employee_id='EMP001')
employee.save()
instance_url = reverse(
'api:employee_instance', urlconf=self.urlconfmodule,
kwargs={'pk':employee.employee_id}
)
self.assertEqual(instance_url, '/employees/EMP001/')
def test_with_different_name(self):
class CompanyResource (ModelResource):
id_field_name = 'name'
self.api.register(Company, CompanyResource)
company = Company(name='Acme')
company.save()
instance_url = reverse(
'api:company_instance', urlconf=self.urlconfmodule,
kwargs={'name':company.name},
)
self.assertEqual(instance_url, '/companys/Acme/')
def test_with_different_collection_name(self):
class CompanyResource (ModelResource):
collection_name = 'companies'
self.api.register(Company, CompanyResource)
list_url = reverse('api:company_collection', urlconf=self.urlconfmodule)
self.assertEqual(list_url, '/companies/')
instance_url = reverse('api:company_instance', urlconf=self.urlconfmodule, kwargs={'pk':1})
self.assertEqual(instance_url, '/companies/1/')
def test_with_default_collection_view_class(self):
self.api.register(Company)
company = Company(name='Acme Ltd')
company.save()
view = self.api.urls[0]._callback
self.assertIsInstance(view.cls_instance, RootAPIView)
self.assertIsInstance(view.cls_instance, ModelResource)
def test_with_default_instance_view_class(self):
self.api.register(Company)
view = self.api.urls[1]._callback
self.assertIsInstance(view.cls_instance, InstanceAPIView)
self.assertIsInstance(view.cls_instance, ModelResource)
def test_with_different_collection_view_class(self):
class CompanyResource(ModelResource):
collection_view_class = ListAPIView
self.api.register(Company, CompanyResource)
view = self.api.urls[0]._callback
self.assertIsInstance(view.cls_instance, ListAPIView)
self.assertIsInstance(view.cls_instance, ModelResource)
def test_with_different_instance_view_class(self):
class CompanyResource(ModelResource):
instance_view_class = DetailAPIView
self.api.register(Company, CompanyResource)
view = self.api.urls[1]._callback
self.assertIsInstance(view.cls_instance, DetailAPIView)
self.assertIsInstance(view.cls_instance, ModelResource)
def test_with_default_serializer_class(self):
self.api.register(Company)
view = self.api.urls[0]._callback
self.assertIs(view.cls_instance.serializer_class, ModelSerializer)
def test_with_different_serializer_class(self):
class CompanySerializer(Serializer):
pass
class CompanyResource(ModelResource):
serializer_class = CompanySerializer
self.api.register(Company, CompanyResource)
view = self.api.urls[0]._callback
self.assertIs(view.cls_instance.serializer_class, CompanySerializer)

View File

@ -1,36 +1,72 @@
serializers.py
class BlogPostSerializer(URLModelSerializer):
class Meta:
model = BlogPost
class CommentSerializer(URLModelSerializer):
class Meta:
model = Comment
## Registering Resources
resources.py
class BlogPostResource(ModelResource):
serializer_class = BlogPostSerializer
model = BlogPost
permissions = [AdminOrAnonReadonly()]
throttles = [AnonThrottle(rate='5/min')]
from djangorestframework.routers import DefaultResourceRouter
from models import BlogPost
class CommentResource(ModelResource):
serializer_class = CommentSerializer
model = Comment
permissions = [AdminOrAnonReadonly()]
throttles = [AnonThrottle(rate='5/min')]
class BlogPostResource (ModelResource):
pass
Now that we're using Resources rather than Views, we don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls are handled automatically. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file.
class CommentResource (ModelResource):
pass
from blog import resources
from djangorestframework.routers import DefaultRouter
api = DefaultResourceRouter()
api.register(BlogPost, BlogPostResource)
api.register(Comment, CommentResource)
router = DefaultRouter()
router.register(resources.BlogPostResource)
router.register(resources.CommentResource)
urlpatterns = router.urlpatterns
urls.py
from resources import api
urlpatterns = patterns('',
url('^', import(api.urls))
)
### Do you need a resource at all?
In the preceding example, the `Resource` classes don't define any custom values
(yet). As a result, the default model resource will be provided. If you are
happy with the default resource, you don't need to define a `Resource`
object at all -- you can register the resource without providing a `Resource`
description. The preceding example could be simplified to:
from djangorestframework.routers import DefaultResourceRouter
from models import BlogPost
api = DefaultResourceRouter()
api.register(BlogPost)
api.register(CommentPost)
## ModelResource options
*ModelResource.serializer_class*
Defaults to `ModelSerializer`.
*ModelResource.permissions*
Defaults to `DEFAULT_PERMISSIONS`.
*ModelResource.throttles*
Defaults to `DEFAULT_THROTTLES`.
*ModelResource.list_view_class*
Defaults to `RootAPIView`. Set to `ListAPIView` for read-only.
*ModelResource.instance_view_class*
Defaults to `InstanceAPIView`. Set to `DetailAPIView` for read-only.
*ModelResource.collection_name*
If `None`, the model's `verbose_name_plural` will be used.
*ModelResource.id_field_name*
Defaults to `'pk'`.
## Trade-offs between views vs resources.
@ -46,4 +82,4 @@ We've reached the end of our tutorial. If you want to get more involved in the
* Join the REST framework group, and help build the community.
* Follow me [on Twitter](https://twitter.com/_tomchristie) and say hi.
Now go build something great.
Now go build something great.