From 9f6d104dad90ae733ddb45eede691abf3ca53bb3 Mon Sep 17 00:00:00 2001 From: Craig Blaszczyk Date: Mon, 29 Aug 2011 02:33:58 +0100 Subject: [PATCH] initial work on autodiscover --- djangorestframework/__init__.py | 30 +++++++++ djangorestframework/builtins.py | 95 +++++++++++++++++++++++++++++ djangorestframework/resources.py | 10 +++ djangorestframework/tests/api.py | 85 ++++++++++++++++++++++++++ djangorestframework/tests/models.py | 8 ++- 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 djangorestframework/builtins.py create mode 100644 djangorestframework/tests/api.py diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index b1ef6ddaa..d9fc245ce 100644 --- a/djangorestframework/__init__.py +++ b/djangorestframework/__init__.py @@ -1,3 +1,33 @@ __version__ = '0.2.3' VERSION = __version__ # synonym + +from djangorestframework.builtins import DjangoRestFrameworkApi +from django.utils.importlib import import_module + +import imp + +__all__ = ('autodiscover','api', '__version__', 'VERSION') + +api = DjangoRestFrameworkApi() + +def autodiscover(): + """ + Auto-discover INSTALLED_APPS api.py modules and fail silently when + not present. This forces an import on them to register any api entries they + may want. + """ + import copy + from django.conf import settings + from django.utils.importlib import import_module + + for app in settings.INSTALLED_APPS: + # Attempt to import the app's gargoyle module. + before_import_registry = copy.copy(api._registry) + try: + import_module('%s.api' % app) + except: + # Reset the model registry to the state before the last import as + # this import will have to reoccur on the next request and this + # could raise NotRegistered and AlreadyRegistered exceptions + api._registry = before_import_registry diff --git a/djangorestframework/builtins.py b/djangorestframework/builtins.py new file mode 100644 index 000000000..b9257d1c6 --- /dev/null +++ b/djangorestframework/builtins.py @@ -0,0 +1,95 @@ +from django.conf.urls.defaults import patterns, url, include + +class ApiEntry(object): + """ + Hold information about a Resource in the api + """ + + def __init__(self, resource, view, name, namespace=None): + self.resource, self.view, self.name = resource, view, name + self.namespace = namespace is not None and namespace or '' + + def get_urls(self): + """ + Create the URLs corresponding to this view. + """ + from djangorestframework.mixins import ListModelMixin, InstanceMixin + if self.namespace == '': + namespaced_name = self.name + else: + namespaced_name = '%s/%s' % (self.namespace, self.name) + + if issubclass(self.view, ListModelMixin): + urlpatterns = patterns('', + url(r'^%s/$' % (namespaced_name), + self.view.as_view(resource=self.resource), + name=self.name, + ) + ) + elif issubclass(self.view, InstanceMixin): + urlpatterns = patterns('', + url(r'^%s/(?P[0-9a-zA-Z]+)/$' % (namespaced_name), + self.view.as_view(resource=self.resource), + name=self.name + '_change', + ) + ) + return urlpatterns + + + def urls(self): + return self.get_urls(), 'api', self.namespace + urls = property(urls) + +class DjangoRestFrameworkApi(object): + app_name = 'api' + namespace = 'api' + + def __init__(self, *args, **kwargs): + self._registry = {} + super(DjangoRestFrameworkApi, self).__init__(*args, **kwargs) + + def register(self, view, resource, namespace=None, name=None): + """ + Register a resource and a view into the API, optionally giving an + override for the resource's name and a namespace for the URLs. + """ + if name is None: + if hasattr(resource, 'model'): + # Use the model's name as the resource_name + name = resource.model.__name__.lower() + else: + # Use the Resource's name as the resource_name + name = resource.__name__.lower() + + resource.api_name = name + + if namespace not in self._registry: + self._registry[namespace] = {} + + if name not in self._registry[namespace]: + self._registry[namespace][name] = [] + + api_entry = ApiEntry(resource, view, name, namespace) + self._registry[namespace][name].append(api_entry) + + @property + def urls(self): + return self.get_urls(), self.app_name, self.namespace + + def get_urls(self): + """ + Return all of the urls for this API + """ + + # Site-wide views. + urlpatterns = patterns('',) + + # Add in each resource's views. + for namespace in self._registry.keys(): + for resource_name in self._registry[namespace].keys(): + for api_entry in self._registry[namespace][resource_name]: + urlpatterns += patterns('', + url(r'^', include(api_entry.urls)) + ) + + return urlpatterns diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index 5770d07f9..5102863be 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -379,6 +379,16 @@ class ModelResource(FormResource): return reverse(self.view_callable[0], kwargs=instance_attrs) except NoReverseMatch: pass + + if hasattr(self, 'api_name'): + # Get the URL from the API + try: + return reverse( + '%s:%s_change' % ('api', self.api_name), args=(instance.pk,) + ) + except NoReverseMatch: + pass + raise _SkipField diff --git a/djangorestframework/tests/api.py b/djangorestframework/tests/api.py new file mode 100644 index 000000000..6d5c3f43b --- /dev/null +++ b/djangorestframework/tests/api.py @@ -0,0 +1,85 @@ +from django.test.testcases import TestCase +from djangorestframework.builtins import DjangoRestFrameworkApi +from djangorestframework.resources import Resource, ModelResource +from djangorestframework.tests.models import Company, Employee +from django.conf.urls.defaults import patterns, url, include +from djangorestframework.views import ListOrCreateModelView, InstanceModelView,\ + ListModelView +from django.core.urlresolvers import reverse + +__all__ = ('ApiTestCase',) + +class CompanyResource(ModelResource): + model = Company + +class EmployeeResource(ModelResource): + model = Employee + +class UrlConfModule(object): + + def __init__(self, api): + self.api = api + + def _get_urlpatterns(self): + return patterns('', + url(r'^', include(self.api.urls)), + ) + + urlpatterns = property(_get_urlpatterns) + + +class ApiTestCase(TestCase): + + def setUp(self): + self.api = DjangoRestFrameworkApi() + self.urlconfmodule = UrlConfModule(self.api) + + def test_list_view(self): + # Check that the URL gets registered + self.api.register(ListModelView, CompanyResource) + reverse('api:company', urlconf=self.urlconfmodule) + + def test_instance_view(self): + self.api.register(InstanceModelView, CompanyResource) + company = Company(name='Acme Ltd') + company.save() + # Check that the URL gets registered + reverse( + 'api:company_change', urlconf=self.urlconfmodule, + kwargs={'pk':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(InstanceModelView, EmployeeResource) + employee = Employee(employee_id='EMP001') + employee.save() + reverse( + 'api:employee_change', urlconf=self.urlconfmodule, + kwargs={'pk':employee.employee_id} + ) + + def test_with_different_name(self): + self.api.register(InstanceModelView, CompanyResource, name='abcdef') + company = Company(name='Acme Ltd') + company.save() + # Check that the URL gets registered + reverse( + 'api:abcdef_change', urlconf=self.urlconfmodule, + kwargs={'pk':company.id}, + ) + + def test_with_prefix(self): + self.api.register( + InstanceModelView, CompanyResource, namespace='abcdef' + ) + company = Company(name='Acme Ltd') + company.save() + # Check that the URL gets registered + reverse( + 'api:abcdef:company_change', urlconf=self.urlconfmodule, + kwargs={'pk':company.id}, + ) \ No newline at end of file diff --git a/djangorestframework/tests/models.py b/djangorestframework/tests/models.py index 61da1d457..6eb28af4f 100644 --- a/djangorestframework/tests/models.py +++ b/djangorestframework/tests/models.py @@ -25,4 +25,10 @@ class UserGroupMap(models.Model): def get_absolute_url(self): return ('user_group_map', (), { 'pk': self.id - }) \ No newline at end of file + }) + +class Company(models.Model): + name = models.CharField(max_length=20) + +class Employee(models.Model): + employee_id = models.CharField(max_length=20, primary_key=True) \ No newline at end of file