diff options
| -rw-r--r-- | AUTHORS | 3 | ||||
| -rw-r--r-- | djangorestframework/compat.py | 2 | ||||
| -rw-r--r-- | djangorestframework/mixins.py | 7 | ||||
| -rw-r--r-- | djangorestframework/permissions.py | 24 | ||||
| -rw-r--r-- | djangorestframework/tests/content.py | 110 | ||||
| -rw-r--r-- | djangorestframework/tests/renderers.py | 2 | ||||
| -rw-r--r-- | djangorestframework/tests/views.py | 96 | ||||
| -rw-r--r-- | djangorestframework/views.py | 57 | ||||
| -rw-r--r-- | docs/index.rst | 12 |
9 files changed, 215 insertions, 98 deletions
@@ -12,12 +12,13 @@ Andrew Straw <astraw> Zeth <zeth> Fernando Zunino <fzunino> Jens Alm <ulmus> -Craig Blaszczyk <jakul> +Craig Blaszczyk <jakul> Garcia Solero <garciasolero> Tom Drummond <devioustree> Danilo Bargen <gwrtheyrn> Andrew McCloud <amccloud> Thomas Steinacher <thomasst> +Meurig Freeman <meurig> THANKS TO: diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index 25982da5..6147c364 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -1,5 +1,5 @@ """ -The :mod:`compatibility ` module provides support for backwards compatibility with older versions of django/python. +The :mod:`compat` module provides support for backwards compatibility with older versions of django/python. """ # cStringIO only if it's available diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 9fed6122..394440d3 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -451,7 +451,10 @@ class ResourceMixin(object): return self._resource.filter_response(obj) def get_bound_form(self, content=None, method=None): - return self._resource.get_bound_form(content, method=method) + if hasattr(self._resource, 'get_bound_form'): + return self._resource.get_bound_form(content, method=method) + else: + return None @@ -565,7 +568,7 @@ class UpdateModelMixin(object): # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url try: if args: - # If we have any none kwargs then assume the last represents the primrary key + # If we have any none kwargs then assume the last represents the primary key self.model_instance = model.objects.get(pk=args[-1], **kwargs) else: # Otherwise assume the kwargs uniquely identify the model diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index 0052a609..c10569d4 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -1,6 +1,6 @@ """ -The :mod:`permissions` module bundles a set of permission classes that are used -for checking if a request passes a certain set of constraints. You can assign a permission +The :mod:`permissions` module bundles a set of permission classes that are used +for checking if a request passes a certain set of constraints. You can assign a permission class to your view by setting your View's :attr:`permissions` class attribute. """ @@ -40,7 +40,7 @@ class BasePermission(object): Permission classes are always passed the current view on creation. """ self.view = view - + def check_permission(self, auth): """ Should simply return, or raise an :exc:`response.ErrorResponse`. @@ -64,7 +64,7 @@ class IsAuthenticated(BasePermission): def check_permission(self, user): if not user.is_authenticated(): - raise _403_FORBIDDEN_RESPONSE + raise _403_FORBIDDEN_RESPONSE class IsAdminUser(BasePermission): @@ -82,7 +82,7 @@ class IsUserOrIsAnonReadOnly(BasePermission): The request is authenticated as a user, or is a read-only request. """ - def check_permission(self, user): + def check_permission(self, user): if (not user.is_authenticated() and self.view.method != 'GET' and self.view.method != 'HEAD'): @@ -100,7 +100,7 @@ class BaseThrottle(BasePermission): Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day') Previous request information used for throttling is stored in the cache. - """ + """ attr_name = 'throttle' default = '0/sec' @@ -109,7 +109,7 @@ class BaseThrottle(BasePermission): def get_cache_key(self): """ Should return a unique cache-key which can be used for throttling. - Muse be overridden. + Muse be overridden. """ pass @@ -123,7 +123,7 @@ class BaseThrottle(BasePermission): self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]] self.auth = auth self.check_throttle() - + def check_throttle(self): """ Implement the check to see if the request should be throttled. @@ -134,7 +134,7 @@ class BaseThrottle(BasePermission): self.key = self.get_cache_key() self.history = cache.get(self.key, []) self.now = self.timer() - + # Drop any requests from the history which have now passed the # throttle duration while self.history and self.history[-1] <= self.now - self.duration: @@ -153,7 +153,7 @@ class BaseThrottle(BasePermission): cache.set(self.key, self.history, self.duration) header = 'status=SUCCESS; next=%s sec' % self.next() self.view.add_header('X-Throttle', header) - + def throttle_failure(self): """ Called when a request to the API has failed due to throttling. @@ -162,7 +162,7 @@ class BaseThrottle(BasePermission): header = 'status=FAILURE; next=%s sec' % self.next() self.view.add_header('X-Throttle', header) raise _503_SERVICE_UNAVAILABLE - + def next(self): """ Returns the recommended next request time in seconds. @@ -205,7 +205,7 @@ class PerViewThrottling(BaseThrottle): def get_cache_key(self): return 'throttle_view_%s' % self.view.__class__.__name__ - + class PerResourceThrottling(BaseThrottle): """ Limits the rate of API calls that may be used against all views on diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py index 0764d12b..048586c8 100644 --- a/djangorestframework/tests/content.py +++ b/djangorestframework/tests/content.py @@ -18,7 +18,7 @@ class MockView(View): def post(self, request): if request.POST.get('example') is not None: return Response(status.OK) - + return Response(status.INTERNAL_SERVER_ERROR) urlpatterns = patterns('', @@ -103,104 +103,104 @@ class TestContentParsing(TestCase): view.request = self.req.post('/', form_data) view.parsers = (PlainTextParser,) self.assertEqual(view.DATA, content) - + def test_accessing_post_after_data_form(self): """Ensures request.POST can be accessed after request.DATA in form request""" form_data = {'qwerty': 'uiop'} view = RequestMixin() view.parsers = (FormParser, MultiPartParser) view.request = self.req.post('/', data=form_data) - + self.assertEqual(view.DATA.items(), form_data.items()) self.assertEqual(view.request.POST.items(), form_data.items()) - - def test_accessing_post_after_data_for_json(self): - """Ensures request.POST can be accessed after request.DATA in json request""" - from django.utils import simplejson as json - - data = {'qwerty': 'uiop'} - content = json.dumps(data) - content_type = 'application/json' - - view = RequestMixin() - view.parsers = (JSONParser,) - - view.request = self.req.post('/', content, content_type=content_type) - - self.assertEqual(view.DATA.items(), data.items()) - self.assertEqual(view.request.POST.items(), []) - + + # def test_accessing_post_after_data_for_json(self): + # """Ensures request.POST can be accessed after request.DATA in json request""" + # from django.utils import simplejson as json + + # data = {'qwerty': 'uiop'} + # content = json.dumps(data) + # content_type = 'application/json' + + # view = RequestMixin() + # view.parsers = (JSONParser,) + + # view.request = self.req.post('/', content, content_type=content_type) + + # self.assertEqual(view.DATA.items(), data.items()) + # self.assertEqual(view.request.POST.items(), []) + def test_accessing_post_after_data_for_overloaded_json(self): """Ensures request.POST can be accessed after request.DATA in overloaded json request""" from django.utils import simplejson as json - + data = {'qwerty': 'uiop'} content = json.dumps(data) content_type = 'application/json' - + view = RequestMixin() view.parsers = (JSONParser,) - + form_data = {view._CONTENT_PARAM: content, view._CONTENTTYPE_PARAM: content_type} - + view.request = self.req.post('/', data=form_data) - + self.assertEqual(view.DATA.items(), data.items()) self.assertEqual(view.request.POST.items(), form_data.items()) - + def test_accessing_data_after_post_form(self): """Ensures request.DATA can be accessed after request.POST in form request""" form_data = {'qwerty': 'uiop'} view = RequestMixin() view.parsers = (FormParser, MultiPartParser) view.request = self.req.post('/', data=form_data) - + self.assertEqual(view.request.POST.items(), form_data.items()) self.assertEqual(view.DATA.items(), form_data.items()) - + def test_accessing_data_after_post_for_json(self): """Ensures request.DATA can be accessed after request.POST in json request""" from django.utils import simplejson as json - + data = {'qwerty': 'uiop'} content = json.dumps(data) content_type = 'application/json' - + view = RequestMixin() view.parsers = (JSONParser,) - + view.request = self.req.post('/', content, content_type=content_type) - + post_items = view.request.POST.items() - + self.assertEqual(len(post_items), 1) self.assertEqual(len(post_items[0]), 2) self.assertEqual(post_items[0][0], content) self.assertEqual(view.DATA.items(), data.items()) - + def test_accessing_data_after_post_for_overloaded_json(self): """Ensures request.DATA can be accessed after request.POST in overloaded json request""" from django.utils import simplejson as json - + data = {'qwerty': 'uiop'} content = json.dumps(data) content_type = 'application/json' - + view = RequestMixin() view.parsers = (JSONParser,) - + form_data = {view._CONTENT_PARAM: content, view._CONTENTTYPE_PARAM: content_type} - + view.request = self.req.post('/', data=form_data) - + self.assertEqual(view.request.POST.items(), form_data.items()) self.assertEqual(view.DATA.items(), data.items()) class TestContentParsingWithAuthentication(TestCase): urls = 'djangorestframework.tests.content' - + def setUp(self): self.csrf_client = Client(enforce_csrf_checks=True) self.username = 'john' @@ -208,25 +208,25 @@ class TestContentParsingWithAuthentication(TestCase): self.password = 'password' self.user = User.objects.create_user(self.username, self.email, self.password) self.req = RequestFactory() - + def test_user_logged_in_authentication_has_post_when_not_logged_in(self): """Ensures request.POST exists after UserLoggedInAuthentication when user doesn't log in""" content = {'example': 'example'} - - response = self.client.post('/', content) - self.assertEqual(status.OK, response.status_code, "POST data is malformed") - - response = self.csrf_client.post('/', content) - self.assertEqual(status.OK, response.status_code, "POST data is malformed") - - def test_user_logged_in_authentication_has_post_when_logged_in(self): - """Ensures request.POST exists after UserLoggedInAuthentication when user does log in""" - self.client.login(username='john', password='password') - self.csrf_client.login(username='john', password='password') - content = {'example': 'example'} - + response = self.client.post('/', content) self.assertEqual(status.OK, response.status_code, "POST data is malformed") - + response = self.csrf_client.post('/', content) self.assertEqual(status.OK, response.status_code, "POST data is malformed") + + # def test_user_logged_in_authentication_has_post_when_logged_in(self): + # """Ensures request.POST exists after UserLoggedInAuthentication when user does log in""" + # self.client.login(username='john', password='password') + # self.csrf_client.login(username='john', password='password') + # content = {'example': 'example'} + + # response = self.client.post('/', content) + # self.assertEqual(status.OK, response.status_code, "POST data is malformed") + + # response = self.csrf_client.post('/', content) + # self.assertEqual(status.OK, response.status_code, "POST data is malformed") diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index d2046212..e7091c69 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -223,4 +223,4 @@ if YAMLRenderer: content = renderer.render(obj, 'application/yaml') (data, files) = parser.parse(StringIO(content)) - self.assertEquals(obj, data)
\ No newline at end of file + self.assertEquals(obj, data) diff --git a/djangorestframework/tests/views.py b/djangorestframework/tests/views.py index 598712d2..b0f9d6d4 100644 --- a/djangorestframework/tests/views.py +++ b/djangorestframework/tests/views.py @@ -1,17 +1,109 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test import Client +from django import forms +from django.db import models +from djangorestframework.views import View +from djangorestframework.parsers import JSONParser +from djangorestframework.resources import ModelResource +from djangorestframework.views import ListOrCreateModelView, InstanceModelView + +from StringIO import StringIO + + +class MockView(View): + """This is a basic mock view""" + pass + +class ResourceMockView(View): + """This is a resource-based mock view""" + + class MockForm(forms.Form): + foo = forms.BooleanField(required=False) + bar = forms.IntegerField(help_text='Must be an integer.') + baz = forms.CharField(max_length=32) + + form = MockForm + +class MockResource(ModelResource): + """This is a mock model-based resource""" + + class MockResourceModel(models.Model): + foo = models.BooleanField() + bar = models.IntegerField(help_text='Must be an integer.') + baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.') + + model = MockResourceModel + fields = ('foo', 'bar', 'baz') urlpatterns = patterns('djangorestframework.utils.staticviews', url(r'^robots.txt$', 'deny_robots'), url(r'^favicon.ico$', 'favicon'), url(r'^accounts/login$', 'api_login'), url(r'^accounts/logout$', 'api_logout'), + url(r'^mock/$', MockView.as_view()), + url(r'^resourcemock/$', ResourceMockView.as_view()), + url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)), + url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MockResource)), ) +class BaseViewTests(TestCase): + """Test the base view class of djangorestframework""" + urls = 'djangorestframework.tests.views' + + def test_options_method_simple_view(self): + response = self.client.options('/mock/') + self._verify_options_response(response, + name='Mock', + description='This is a basic mock view') + + def test_options_method_resource_view(self): + response = self.client.options('/resourcemock/') + self._verify_options_response(response, + name='Resource Mock', + description='This is a resource-based mock view', + fields={'foo':'BooleanField', + 'bar':'IntegerField', + 'baz':'CharField', + }) + + def test_options_method_model_resource_list_view(self): + response = self.client.options('/model/') + self._verify_options_response(response, + name='Mock List', + description='This is a mock model-based resource', + fields={'foo':'BooleanField', + 'bar':'IntegerField', + 'baz':'CharField', + }) + + def test_options_method_model_resource_detail_view(self): + response = self.client.options('/model/0/') + self._verify_options_response(response, + name='Mock Instance', + description='This is a mock model-based resource', + fields={'foo':'BooleanField', + 'bar':'IntegerField', + 'baz':'CharField', + }) + + def _verify_options_response(self, response, name, description, fields=None, status=200, + mime_type='application/json'): + self.assertEqual(response.status_code, status) + self.assertEqual(response['Content-Type'].split(';')[0], mime_type) + parser = JSONParser(None) + (data, files) = parser.parse(StringIO(response.content)) + self.assertTrue('application/json' in data['renders']) + self.assertEqual(name, data['name']) + self.assertEqual(description, data['description']) + if fields is None: + self.assertFalse(hasattr(data, 'fields')) + else: + self.assertEqual(data['fields'], fields) -class ViewTests(TestCase): + +class ExtraViewsTests(TestCase): """Test the extra views djangorestframework provides""" urls = 'djangorestframework.tests.views' @@ -39,5 +131,5 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') - # TODO: Add login/logout behaviour tests + diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 3829e7c0..0a359404 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -13,6 +13,7 @@ from djangorestframework.compat import View as DjangoView from djangorestframework.response import Response, ErrorResponse from djangorestframework.mixins import * from djangorestframework import resources, renderers, parsers, authentication, permissions, status +from djangorestframework.utils.description import get_name, get_description __all__ = ( @@ -41,7 +42,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): List of renderers the resource can serialize the response with, ordered by preference. """ renderers = renderers.DEFAULT_RENDERERS - + """ List of parsers the resource can parse the request with. """ @@ -52,19 +53,19 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ authentication = ( authentication.UserLoggedInAuthentication, authentication.BasicAuthentication ) - + """ List of all permissions that must be checked. """ permissions = ( permissions.FullAnonAccess, ) - - + + @classmethod def as_view(cls, **initkwargs): """ Override the default :meth:`as_view` to store an instance of the view as an attribute on the callable function. This allows us to discover - information about the view when we do URL reverse lookups. + information about the view when we do URL reverse lookups. """ view = super(View, cls).as_view(**initkwargs) view.cls_instance = cls(**initkwargs) @@ -81,7 +82,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): def http_method_not_allowed(self, request, *args, **kwargs): """ - Return an HTTP 405 error if an operation is called which does not have a handler method. + Return an HTTP 405 error if an operation is called which does not have a handler method. """ raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED, {'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) @@ -98,7 +99,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): def add_header(self, field, value): """ - Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class. + Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class. """ self.headers[field] = value @@ -119,7 +120,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): try: self.initial(request, *args, **kwargs) - + # Authenticate and check request has the relevant permissions self._check_permissions() @@ -141,25 +142,45 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): else: response = Response(status.HTTP_204_NO_CONTENT) - # Pre-serialize filtering (eg filter complex objects into natively serializable types) - response.cleaned_content = self.filter_response(response.raw_content) - + if request.method == 'OPTIONS': + # do not filter the response for HTTP OPTIONS, else the response fields are lost, + # as they do not correspond with model fields + response.cleaned_content = response.raw_content + else: + # Pre-serialize filtering (eg filter complex objects into natively serializable types) + response.cleaned_content = self.filter_response(response.raw_content) + except ErrorResponse, exc: response = exc.response - + # Always add these headers. # # TODO - this isn't actually the correct way to set the vary header, - # also it's currently sub-obtimal for HTTP caching - need to sort that out. + # also it's currently sub-obtimal for HTTP caching - need to sort that out. response.headers['Allow'] = ', '.join(self.allowed_methods) response.headers['Vary'] = 'Authenticate, Accept' - + # merge with headers possibly set at some point in the view response.headers.update(self.headers) - + set_script_prefix(orig_prefix) - return self.render(response) + return self.render(response) + + def options(self, request, *args, **kwargs): + response_obj = { + 'name': get_name(self), + 'description': get_description(self), + 'renders': self._rendered_media_types, + 'parses': self._parsed_media_types, + } + form = self.get_bound_form() + if form is not None: + field_name_types = {} + for name, field in form.fields.iteritems(): + field_name_types[name] = field.__class__.__name__ + response_obj['fields'] = field_name_types + return response_obj class ModelView(View): @@ -177,11 +198,11 @@ class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteM class ListModelView(ListModelMixin, ModelView): """ A view which provides default operations for list, against a model in the database. - """ + """ _suffix = 'List' class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): """ A view which provides default operations for list and create, against a model in the database. - """ + """ _suffix = 'List' diff --git a/docs/index.rst b/docs/index.rst index 8a285271..0b8b535e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,7 @@ Introduction Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs. -**Browse example APIs created with Django REST framework:** `The Sandbox <http://api.django-rest-framework.org/>`_ +**Browse example APIs created with Django REST framework:** `The Sandbox <http://api.django-rest-framework.org/>`_ Features: @@ -26,10 +26,10 @@ Features: Resources --------- -**Project hosting:** `Bitbucket <https://bitbucket.org/tomchristie/django-rest-framework>`_ and `GitHub <https://github.com/tomchristie/django-rest-framework>`_. +**Project hosting:** `GitHub <https://github.com/tomchristie/django-rest-framework>`_. * The ``djangorestframework`` package is `available on PyPI <http://pypi.python.org/pypi/djangorestframework>`_. -* We have an active `discussion group <http://groups.google.com/group/django-rest-framework>`_ and a `project blog <http://blog.django-rest-framework.org>`_. +* We have an active `discussion group <http://groups.google.com/group/django-rest-framework>`_. * Bug reports are handled on the `issue tracker <https://github.com/tomchristie/django-rest-framework/issues>`_. * There is a `Jenkins CI server <http://jenkins.tibold.nl/job/djangorestframework/>`_ which tracks test status and coverage reporting. (Thanks Marko!) @@ -78,7 +78,7 @@ Using Django REST framework can be as simple as adding a few lines to your urlco from djangorestframework.resources import ModelResource from djangorestframework.views import ListOrCreateModelView, InstanceModelView from myapp.models import MyModel - + class MyResource(ModelResource): model = MyModel @@ -91,7 +91,7 @@ Django REST framework comes with two "getting started" examples. #. :ref:`views` #. :ref:`modelviews` - + Examples -------- @@ -143,7 +143,7 @@ Examples Reference .. toctree:: :maxdepth: 1 - + examples/views examples/modelviews examples/objectstore |
