aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS4
-rw-r--r--djangorestframework/compat.py2
-rw-r--r--djangorestframework/mixins.py142
-rw-r--r--djangorestframework/permissions.py24
-rw-r--r--djangorestframework/serializer.py3
-rw-r--r--djangorestframework/tests/content.py110
-rw-r--r--djangorestframework/tests/mixins.py162
-rw-r--r--djangorestframework/tests/renderers.py3
-rw-r--r--djangorestframework/tests/views.py96
-rw-r--r--djangorestframework/views.py64
-rw-r--r--docs/index.rst12
11 files changed, 478 insertions, 144 deletions
diff --git a/AUTHORS b/AUTHORS
index 2b8af2b9..9ddcb9ad 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -12,11 +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..b1a634a0 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -1,23 +1,20 @@
"""
-The :mod:`mixins` module provides a set of reusable `mixin`
+The :mod:`mixins` module provides a set of reusable `mixin`
classes that can be added to a `View`.
"""
from django.contrib.auth.models import AnonymousUser
-from django.db.models.query import QuerySet
+from django.core.paginator import Paginator
from django.db.models.fields.related import ForeignKey
from django.http import HttpResponse
from djangorestframework import status
-from djangorestframework.parsers import FormParser, MultiPartParser
from djangorestframework.renderers import BaseRenderer
from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
-from decimal import Decimal
-import re
from StringIO import StringIO
@@ -52,7 +49,7 @@ class RequestMixin(object):
"""
The set of request parsers that the view can handle.
-
+
Should be a tuple/list of classes as described in the :mod:`parsers` module.
"""
parsers = ()
@@ -158,7 +155,7 @@ class RequestMixin(object):
# We only need to use form overloading on form POST requests.
if not self._USE_FORM_OVERLOADING or self._method != 'POST' or not is_form_media_type(self._content_type):
return
-
+
# At this point we're committed to parsing the request as form data.
self._data = data = self.request.POST.copy()
self._files = self.request.FILES
@@ -203,12 +200,12 @@ class RequestMixin(object):
"""
return [parser.media_type for parser in self.parsers]
-
+
@property
def _default_parser(self):
"""
Return the view's default parser class.
- """
+ """
return self.parsers[0]
@@ -218,7 +215,7 @@ class RequestMixin(object):
class ResponseMixin(object):
"""
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
-
+
Default behavior is to use standard HTTP Accept header content negotiation.
Also supports overriding the content type by specifying an ``_accept=`` parameter in the URL.
Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead.
@@ -229,8 +226,8 @@ class ResponseMixin(object):
"""
The set of response renderers that the view can handle.
-
- Should be a tuple/list of classes as described in the :mod:`renderers` module.
+
+ Should be a tuple/list of classes as described in the :mod:`renderers` module.
"""
renderers = ()
@@ -253,7 +250,7 @@ class ResponseMixin(object):
# Set the media type of the response
# Note that the renderer *could* override it in .render() if required.
response.media_type = renderer.media_type
-
+
# Serialize the response content
if response.has_content_body:
content = renderer.render(response.cleaned_content, media_type)
@@ -317,7 +314,7 @@ class ResponseMixin(object):
Return an list of all the media types that this view can render.
"""
return [renderer.media_type for renderer in self.renderers]
-
+
@property
def _rendered_formats(self):
"""
@@ -339,18 +336,18 @@ class AuthMixin(object):
"""
Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class.
"""
-
+
"""
The set of authentication types that this view can handle.
-
- Should be a tuple/list of classes as described in the :mod:`authentication` module.
+
+ Should be a tuple/list of classes as described in the :mod:`authentication` module.
"""
authentication = ()
"""
The set of permissions that will be enforced on this view.
-
- Should be a tuple/list of classes as described in the :mod:`permissions` module.
+
+ Should be a tuple/list of classes as described in the :mod:`permissions` module.
"""
permissions = ()
@@ -359,7 +356,7 @@ class AuthMixin(object):
def user(self):
"""
Returns the :obj:`user` for the current request, as determined by the set of
- :class:`authentication` classes applied to the :class:`View`.
+ :class:`authentication` classes applied to the :class:`View`.
"""
if not hasattr(self, '_user'):
self._user = self._authenticate()
@@ -451,7 +448,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
@@ -538,13 +538,13 @@ class CreateModelMixin(object):
for fieldname in m2m_data:
manager = getattr(instance, fieldname)
-
+
if hasattr(manager, 'add'):
manager.add(*m2m_data[fieldname][1])
else:
data = {}
data[manager.source_field_name] = instance
-
+
for related_item in m2m_data[fieldname][1]:
data[m2m_data[fieldname][0]] = related_item
manager.through(**data).save()
@@ -561,11 +561,11 @@ class UpdateModelMixin(object):
"""
def put(self, request, *args, **kwargs):
model = self.resource.model
-
- # 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
+
+ # 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
@@ -637,3 +637,93 @@ class ListModelMixin(object):
return queryset.filter(**kwargs)
+########## Pagination Mixins ##########
+
+class PaginatorMixin(object):
+ """
+ Adds pagination support to GET requests
+ Obviously should only be used on lists :)
+
+ A default limit can be set by setting `limit` on the object. This will also
+ be used as the maximum if the client sets the `limit` GET param
+ """
+ limit = 20
+
+ def get_limit(self):
+ """ Helper method to determine what the `limit` should be """
+ try:
+ limit = int(self.request.GET.get('limit', self.limit))
+ return min(limit, self.limit)
+ except ValueError:
+ return self.limit
+
+ def url_with_page_number(self, page_number):
+ """ Constructs a url used for getting the next/previous urls """
+ url = "%s?page=%d" % (self.request.path, page_number)
+
+ limit = self.get_limit()
+ if limit != self.limit:
+ url = "%s&limit=%d" % (url, limit)
+
+ return url
+
+ def next(self, page):
+ """ Returns a url to the next page of results (if any) """
+ if not page.has_next():
+ return None
+
+ return self.url_with_page_number(page.next_page_number())
+
+ def previous(self, page):
+ """ Returns a url to the previous page of results (if any) """
+ if not page.has_previous():
+ return None
+
+ return self.url_with_page_number(page.previous_page_number())
+
+ def serialize_page_info(self, page):
+ """ This is some useful information that is added to the response """
+ return {
+ 'next': self.next(page),
+ 'page': page.number,
+ 'pages': page.paginator.num_pages,
+ 'per_page': self.get_limit(),
+ 'previous': self.previous(page),
+ 'total': page.paginator.count,
+ }
+
+ def filter_response(self, obj):
+ """
+ Given the response content, paginate and then serialize.
+
+ The response is modified to include to useful data relating to the number
+ of objects, number of pages, next/previous urls etc. etc.
+
+ The serialised objects are put into `results` on this new, modified
+ response
+ """
+
+ # We don't want to paginate responses for anything other than GET requests
+ if self.method.upper() != 'GET':
+ return self._resource.filter_response(obj)
+
+ paginator = Paginator(obj, self.get_limit())
+
+ try:
+ page_num = int(self.request.GET.get('page', '1'))
+ except ValueError:
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND,
+ {'detail': 'That page contains no results'})
+
+ if page_num not in paginator.page_range:
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND,
+ {'detail': 'That page contains no results'})
+
+ page = paginator.page(page_num)
+
+ serialized_object_list = self._resource.filter_response(page.object_list)
+ serialized_page_info = self.serialize_page_info(page)
+
+ serialized_page_info['results'] = serialized_object_list
+
+ return serialized_page_info
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
index 0052a609..945023ce 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.
+ Must 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/serializer.py b/djangorestframework/serializer.py
index 22efa5ed..55b84df1 100644
--- a/djangorestframework/serializer.py
+++ b/djangorestframework/serializer.py
@@ -106,7 +106,8 @@ class Serializer(object):
def __init__(self, depth=None, stack=[], **kwargs):
- self.depth = depth or self.depth
+ if depth is not None:
+ self.depth = depth
self.stack = stack
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/mixins.py b/djangorestframework/tests/mixins.py
index da7c4d86..65cf4a45 100644
--- a/djangorestframework/tests/mixins.py
+++ b/djangorestframework/tests/mixins.py
@@ -1,14 +1,17 @@
-"""Tests for the status module"""
+"""Tests for the mixin module"""
from django.test import TestCase
+from django.utils import simplejson as json
from djangorestframework import status
from djangorestframework.compat import RequestFactory
from django.contrib.auth.models import Group, User
-from djangorestframework.mixins import CreateModelMixin
+from djangorestframework.mixins import CreateModelMixin, PaginatorMixin
from djangorestframework.resources import ModelResource
+from djangorestframework.response import Response
from djangorestframework.tests.models import CustomUser
+from djangorestframework.views import View
-class TestModelCreation(TestCase):
+class TestModelCreation(TestCase):
"""Tests on CreateModelMixin"""
def setUp(self):
@@ -25,23 +28,26 @@ class TestModelCreation(TestCase):
mixin = CreateModelMixin()
mixin.resource = GroupResource
mixin.CONTENT = form_data
-
+
response = mixin.post(request)
self.assertEquals(1, Group.objects.count())
self.assertEquals('foo', response.cleaned_content.name)
-
def test_creation_with_m2m_relation(self):
class UserResource(ModelResource):
model = User
-
+
def url(self, instance):
return "/users/%i" % instance.id
group = Group(name='foo')
group.save()
- form_data = {'username': 'bar', 'password': 'baz', 'groups': [group.id]}
+ form_data = {
+ 'username': 'bar',
+ 'password': 'baz',
+ 'groups': [group.id]
+ }
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = [group]
@@ -53,18 +59,18 @@ class TestModelCreation(TestCase):
self.assertEquals(1, User.objects.count())
self.assertEquals(1, response.cleaned_content.groups.count())
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
-
+
def test_creation_with_m2m_relation_through(self):
"""
Tests creation where the m2m relation uses a through table
"""
class UserResource(ModelResource):
model = CustomUser
-
+
def url(self, instance):
return "/customusers/%i" % instance.id
-
- form_data = {'username': 'bar0', 'groups': []}
+
+ form_data = {'username': 'bar0', 'groups': []}
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = []
@@ -74,12 +80,12 @@ class TestModelCreation(TestCase):
response = mixin.post(request)
self.assertEquals(1, CustomUser.objects.count())
- self.assertEquals(0, response.cleaned_content.groups.count())
+ self.assertEquals(0, response.cleaned_content.groups.count())
group = Group(name='foo1')
group.save()
- form_data = {'username': 'bar1', 'groups': [group.id]}
+ form_data = {'username': 'bar1', 'groups': [group.id]}
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = [group]
@@ -91,12 +97,11 @@ class TestModelCreation(TestCase):
self.assertEquals(2, CustomUser.objects.count())
self.assertEquals(1, response.cleaned_content.groups.count())
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
-
-
+
group2 = Group(name='foo2')
- group2.save()
-
- form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
+ group2.save()
+
+ form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = [group, group2]
@@ -109,5 +114,124 @@ class TestModelCreation(TestCase):
self.assertEquals(2, response.cleaned_content.groups.count())
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
self.assertEquals('foo2', response.cleaned_content.groups.all()[1].name)
-
+
+class MockPaginatorView(PaginatorMixin, View):
+ total = 60
+
+ def get(self, request):
+ return range(0, self.total)
+
+ def post(self, request):
+ return Response(status.CREATED, {'status': 'OK'})
+
+
+class TestPagination(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
+
+ def test_default_limit(self):
+ """ Tests if pagination works without overwriting the limit """
+ request = self.req.get('/paginator')
+ response = MockPaginatorView.as_view()(request)
+
+ content = json.loads(response.content)
+
+ self.assertEqual(response.status_code, status.OK)
+ self.assertEqual(MockPaginatorView.total, content['total'])
+ self.assertEqual(MockPaginatorView.limit, content['per_page'])
+
+ self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
+
+ def test_overwriting_limit(self):
+ """ Tests if the limit can be overwritten """
+ limit = 10
+
+ request = self.req.get('/paginator')
+ response = MockPaginatorView.as_view(limit=limit)(request)
+
+ content = json.loads(response.content)
+
+ self.assertEqual(response.status_code, status.OK)
+ self.assertEqual(content['per_page'], limit)
+
+ self.assertEqual(range(0, limit), content['results'])
+
+ def test_limit_param(self):
+ """ Tests if the client can set the limit """
+ from math import ceil
+
+ limit = 5
+ num_pages = int(ceil(MockPaginatorView.total / float(limit)))
+
+ request = self.req.get('/paginator/?limit=%d' % limit)
+ response = MockPaginatorView.as_view()(request)
+
+ content = json.loads(response.content)
+
+ self.assertEqual(response.status_code, status.OK)
+ self.assertEqual(MockPaginatorView.total, content['total'])
+ self.assertEqual(limit, content['per_page'])
+ self.assertEqual(num_pages, content['pages'])
+
+ def test_exceeding_limit(self):
+ """ Makes sure the client cannot exceed the default limit """
+ from math import ceil
+
+ limit = MockPaginatorView.limit + 10
+ num_pages = int(ceil(MockPaginatorView.total / float(limit)))
+
+ request = self.req.get('/paginator/?limit=%d' % limit)
+ response = MockPaginatorView.as_view()(request)
+
+ content = json.loads(response.content)
+
+ self.assertEqual(response.status_code, status.OK)
+ self.assertEqual(MockPaginatorView.total, content['total'])
+ self.assertNotEqual(limit, content['per_page'])
+ self.assertNotEqual(num_pages, content['pages'])
+ self.assertEqual(MockPaginatorView.limit, content['per_page'])
+
+ def test_only_works_for_get(self):
+ """ Pagination should only work for GET requests """
+ request = self.req.post('/paginator', data={'content': 'spam'})
+ response = MockPaginatorView.as_view()(request)
+
+ content = json.loads(response.content)
+
+ self.assertEqual(response.status_code, status.CREATED)
+ self.assertEqual(None, content.get('per_page'))
+ self.assertEqual('OK', content['status'])
+
+ def test_non_int_page(self):
+ """ Tests that it can handle invalid values """
+ request = self.req.get('/paginator/?page=spam')
+ response = MockPaginatorView.as_view()(request)
+
+ self.assertEqual(response.status_code, status.NOT_FOUND)
+
+ def test_page_range(self):
+ """ Tests that the page range is handle correctly """
+ request = self.req.get('/paginator/?page=0')
+ response = MockPaginatorView.as_view()(request)
+ content = json.loads(response.content)
+ self.assertEqual(response.status_code, status.NOT_FOUND)
+
+ request = self.req.get('/paginator/')
+ response = MockPaginatorView.as_view()(request)
+ content = json.loads(response.content)
+ self.assertEqual(response.status_code, status.OK)
+ self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
+
+ num_pages = content['pages']
+
+ request = self.req.get('/paginator/?page=%d' % num_pages)
+ response = MockPaginatorView.as_view()(request)
+ content = json.loads(response.content)
+ self.assertEqual(response.status_code, status.OK)
+ self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
+
+ request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
+ response = MockPaginatorView.as_view()(request)
+ content = json.loads(response.content)
+ self.assertEqual(response.status_code, status.NOT_FOUND)
diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py
index 5b2d3556..d6a49984 100644
--- a/djangorestframework/tests/renderers.py
+++ b/djangorestframework/tests/renderers.py
@@ -228,6 +228,7 @@ if YAMLRenderer:
(data, files) = parser.parse(StringIO(content))
self.assertEquals(obj, data)
+
class XMLRendererTestCase(TestCase):
"""
@@ -288,4 +289,4 @@ class XMLRendererTestCase(TestCase):
def assertXMLContains(self, xml, string):
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
self.assertTrue(xml.endswith('</root>'))
- self.assertTrue(string in xml, '%r not in %r' % (string, xml)) \ No newline at end of file
+ self.assertTrue(string in xml, '%r not in %r' % (string, xml))
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 5f8e84cd..0a359404 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -5,7 +5,7 @@ be subclassing in your implementation.
By setting or modifying class attributes on your view, you change it's predefined behaviour.
"""
-from django.core.urlresolvers import set_script_prefix
+from django.core.urlresolvers import set_script_prefix, get_script_prefix
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
@@ -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
@@ -113,12 +114,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
self.headers = {}
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
+ orig_prefix = get_script_prefix()
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
- set_script_prefix(prefix)
+ set_script_prefix(prefix + orig_prefix)
try:
self.initial(request, *args, **kwargs)
-
+
# Authenticate and check request has the relevant permissions
self._check_permissions()
@@ -140,23 +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)
-
- return self.render(response)
+
+ set_script_prefix(orig_prefix)
+
+ 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):
@@ -174,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