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)