From 6cd046cfdf67bab93be64194b43f652aa9763959 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Poe Date: Sat, 8 Sep 2012 16:25:04 -0400 Subject: [PATCH 1/6] Updated tutorial for routers and resources. --- .../6-resource-orientated-projects.md | 88 +++++++++++++------ 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index 4282c25d5..db30f02b5 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -1,36 +1,70 @@ -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 router + + urlpatterns = api.urlpatterns + +### Do you need a serializer at all? + +In the preceding example, the `Serializer` classes don't define any custom values +(yet). As a result, the default model serializer will be provided. If you are +happy with the default serializer, you don't need to define a `Serializer` +object at all -- you can register the resource without providing a `Serializer` +description. The preceding example could be simplified to: + + from djangorestframework.routers import DefaultResourceRouter + from models import BlogPost + + router = DefaultResourceRouter() + router.register(BlogPost) + router.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 +80,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. \ No newline at end of file +Now go build something great. From f8a1d38074c2da33db3d66f50eb9c54e318188e0 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Poe Date: Sat, 8 Sep 2012 16:31:36 -0400 Subject: [PATCH 2/6] Correct 'serializer' to 'resource' in the docs --- docs/tutorial/6-resource-orientated-projects.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index db30f02b5..589039f8a 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -21,12 +21,12 @@ urls.py urlpatterns = api.urlpatterns -### Do you need a serializer at all? +### Do you need a resource at all? -In the preceding example, the `Serializer` classes don't define any custom values -(yet). As a result, the default model serializer will be provided. If you are -happy with the default serializer, you don't need to define a `Serializer` -object at all -- you can register the resource without providing a `Serializer` +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 d4f5c9cf5afdfbeed720d3705b26ee275d10e071 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Poe Date: Sat, 8 Sep 2012 16:34:33 -0400 Subject: [PATCH 3/6] Correct 'router' to 'api' in the docs --- docs/tutorial/6-resource-orientated-projects.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index 589039f8a..312a6e760 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -17,9 +17,11 @@ resources.py urls.py - from resources import router + from resources import api - urlpatterns = api.urlpatterns + urlpatterns = patterns('', + url('^', import(api.urls)) + ) ### Do you need a resource at all? @@ -32,9 +34,9 @@ description. The preceding example could be simplified to: from djangorestframework.routers import DefaultResourceRouter from models import BlogPost - router = DefaultResourceRouter() - router.register(BlogPost) - router.register(CommentPost) + api = DefaultResourceRouter() + api.register(BlogPost) + api.register(CommentPost) ## ModelResource options From ca5a3abca3877504823676bc1541df929986e6a5 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Poe Date: Sat, 8 Sep 2012 18:25:17 -0400 Subject: [PATCH 4/6] Implement routers and resources according to the documentation --- djangorestframework/resources.py | 11 ++ djangorestframework/routers.py | 91 +++++++++++++++ djangorestframework/runtests/settings.py | 3 + djangorestframework/tests/models.py | 12 +- djangorestframework/tests/routers.py | 140 +++++++++++++++++++++++ 5 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 djangorestframework/resources.py create mode 100644 djangorestframework/routers.py create mode 100644 djangorestframework/tests/routers.py diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py new file mode 100644 index 000000000..a8c32ac37 --- /dev/null +++ b/djangorestframework/resources.py @@ -0,0 +1,11 @@ +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 + + collection_name = None + instance_name = None + id_field_name = 'pk' diff --git a/djangorestframework/routers.py b/djangorestframework/routers.py new file mode 100644 index 000000000..4d8db0f97 --- /dev/null +++ b/djangorestframework/routers.py @@ -0,0 +1,91 @@ +from django.conf.urls import patterns, url +from djangorestframework.resources import ModelResource + +class DefaultResourceRouter (object): + + def __init__(self, resource=ModelResource): + self.resource = resource + self._registry = [] + + @property + def urls(self): + return self.get_urls() + + def get_urls(self): + urls = [] + for model, resource in self._registry: + urls += self.make_patterns(model, resource, + id_field_name=resource.id_field_name, + collection_name=resource.collection_name, + instance_name=resource.instance_name) + return patterns('', *urls) + + def make_patterns(self, model, resource, id_field_name=None, + collection_name=None, instance_name=None): + patterns = [] + + # 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. + # + if collection_name is None: + collection_name = unicode(model._meta.verbose_name_plural) + + # 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[^/])/)', instance_view, name='dog_instance'), + # + if instance_name is None: + instance_name = unicode(model._meta.verbose_name) + + # 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[^/])/)', instance_view, name='dog_instance'), + # + # and: + # + # http://api.example.com/dogs/fido/ + # + 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): + resource = resource or self.resource + self._registry.append((model, resource)) diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py index 1fc6b47b0..158416996 100644 --- a/djangorestframework/runtests/settings.py +++ b/djangorestframework/runtests/settings.py @@ -91,6 +91,9 @@ INSTALLED_APPS = ( # 'django.contrib.admindocs', 'djangorestframework', 'djangorestframework.tokenauth', + + # Load up the models + 'djangorestframework.tests' ) STATIC_URL = '/static/' diff --git a/djangorestframework/tests/models.py b/djangorestframework/tests/models.py index 4cae68b61..dc095f918 100644 --- a/djangorestframework/tests/models.py +++ b/djangorestframework/tests/models.py @@ -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) diff --git a/djangorestframework/tests/routers.py b/djangorestframework/tests/routers.py new file mode 100644 index 000000000..6e1ca64db --- /dev/null +++ b/djangorestframework/tests/routers.py @@ -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) From c946bf25663223127bdb07e7457b953529ef95d7 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Poe Date: Sat, 8 Sep 2012 18:35:26 -0400 Subject: [PATCH 5/6] Better doumentation in the code --- djangorestframework/resources.py | 29 ++++++++++++++ djangorestframework/routers.py | 66 ++++++++++++++++---------------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index a8c32ac37..d578586e9 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -6,6 +6,35 @@ class ModelResource (object): 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[^/])/)', 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[^/])/)', instance_view, name='dog_instance'), + # + # and: + # + # http://api.example.com/dogs/fido/ + # + # The default value is 'pk'. + # id_field_name = 'pk' diff --git a/djangorestframework/routers.py b/djangorestframework/routers.py index 4d8db0f97..9febc9607 100644 --- a/djangorestframework/routers.py +++ b/djangorestframework/routers.py @@ -3,58 +3,52 @@ from djangorestframework.resources import ModelResource class DefaultResourceRouter (object): - def __init__(self, resource=ModelResource): - self.resource = resource + def __init__(self, default_resource=ModelResource): + self.default_resource = default_resource self._registry = [] @property def urls(self): - return self.get_urls() + """ + 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, - id_field_name=resource.id_field_name, - collection_name=resource.collection_name, - instance_name=resource.instance_name) - return patterns('', *urls) + 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 = [] - # 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. - # if collection_name is None: collection_name = unicode(model._meta.verbose_name_plural) - # 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[^/])/)', instance_view, name='dog_instance'), - # if instance_name is None: instance_name = unicode(model._meta.verbose_name) - # 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[^/])/)', instance_view, name='dog_instance'), - # - # and: - # - # http://api.example.com/dogs/fido/ - # if id_field_name is None: id_field_name = u'pk' @@ -87,5 +81,9 @@ class DefaultResourceRouter (object): return patterns def register(self, model, resource=None): - resource = resource or self.resource + """ + 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)) From 3f81cde88309848f5d602515d84edf7c2dd179d3 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Poe Date: Sat, 8 Sep 2012 19:00:02 -0400 Subject: [PATCH 6/6] Get url tools from django.conf.urls.defaults --- djangorestframework/routers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/djangorestframework/routers.py b/djangorestframework/routers.py index 9febc9607..68140280c 100644 --- a/djangorestframework/routers.py +++ b/djangorestframework/routers.py @@ -1,4 +1,7 @@ -from django.conf.urls import patterns, url +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):