aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml3
-rw-r--r--docs/api-guide/filtering.md7
-rw-r--r--docs/api-guide/generic-views.md18
-rw-r--r--docs/api-guide/settings.md22
-rw-r--r--docs/index.md4
-rw-r--r--docs/topics/release-notes.md2
-rwxr-xr-xmkdocs.py13
-rw-r--r--rest_framework/generics.py35
-rw-r--r--rest_framework/mixins.py27
-rw-r--r--rest_framework/settings.py9
-rw-r--r--rest_framework/tests/pagination.py82
-rw-r--r--tox.ini12
12 files changed, 192 insertions, 42 deletions
diff --git a/.travis.yml b/.travis.yml
index 800ba241..ccfdeacb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,8 +11,7 @@ env:
install:
- pip install $DJANGO
- - pip install -r requirements.txt --use-mirrors
- - pip install -e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+ - pip install django-filter==0.5.4 --use-mirrors
- export PYTHONPATH=.
script:
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 14ab9a26..53ea7cbc 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -71,7 +71,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
by filtering against a `username` query parameter in the URL.
"""
queryset = Purchase.objects.all()
- username = self.request.QUERY_PARAMS.get('username', None):
+ username = self.request.QUERY_PARAMS.get('username', None)
if username is not None:
queryset = queryset.filter(purchaser__username=username)
return queryset
@@ -84,9 +84,9 @@ As well as being able to override the default queryset, REST framework also incl
REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package.
-To use REST framework's default filtering backend, first install `django-filter`.
+To use REST framework's filtering backend, first install `django-filter`.
- pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter
+ pip install django-filter
You must also set the filter backend to `DjangoFilterBackend` in your settings:
@@ -94,7 +94,6 @@ You must also set the filter backend to `DjangoFilterBackend` in your settings:
'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend'
}
-**Note**: The currently supported version of `django-filter` is the `master` branch. A PyPI release is expected to be coming soon.
## Specifying filter fields
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index 360ef1a2..33ec89d2 100644
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -123,18 +123,36 @@ Each of the generic views provided is built by combining one of the base views b
Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
+**Attributes**:
+
+* `model` - The model that should be used for this view. Used as a fallback for determining the serializer if `serializer_class` is not set, and as a fallback for determining the queryset if `queryset` is not set. Otherwise not required.
+* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. If unset, this defaults to creating a serializer class using `self.model`, with the `DEFAULT_MODEL_SERIALIZER_CLASS` setting as the base serializer class.
+
## MultipleObjectAPIView
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
**See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
+**Attributes**:
+
+* `queryset` - The queryset that should be used for returning objects from this view. If unset, defaults to the default queryset manager for `self.model`.
+* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
+* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
+
## SingleObjectAPIView
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
**See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy].
+**Attributes**:
+
+* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`.
+* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
+* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+]
+* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
+
---
# Mixins
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 4f87b30d..7884d096 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -96,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer`
Default: `rest_framework.pagination.PaginationSerializer`
-## FORMAT_SUFFIX_KWARG
+## FILTER_BACKEND
-**TODO**
+The filter backend class that should be used for generic filtering. If set to `None` then generic filtering is disabled.
-Default: `'format'`
+## PAGINATE_BY
+
+The default page size to use for pagination. If set to `None`, pagination is disabled by default.
+
+Default: `None`
+
+## PAGINATE_BY_KWARG
+
+The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size.
+
+Default: `None`
## UNAUTHENTICATED_USER
@@ -150,4 +160,10 @@ Default: `'accept'`
Default: `'format'`
+## FORMAT_SUFFIX_KWARG
+
+**TODO**
+
+Default: `'format'`
+
[cite]: http://www.python.org/dev/peps/pep-0020/
diff --git a/docs/index.md b/docs/index.md
index fd834540..cc0f2a13 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -34,7 +34,7 @@ The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API.
* [PyYAML][yaml] (3.10+) - YAML content-type support.
-* [django-filter][django-filter] (master) - Filtering support.
+* [django-filter][django-filter] (0.5.4+) - Filtering support.
## Installation
@@ -43,7 +43,7 @@ Install using `pip`, including any optional packages you want...
pip install djangorestframework
pip install markdown # Markdown support for the browseable API.
pip install pyyaml # YAML content-type support.
- pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter # Filtering support
+ pip install django-filter # Filtering support
...or clone the project from github.
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 35e8a8b3..869cabc8 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -7,6 +7,8 @@
## Master
* Support for `read_only_fields` on `ModelSerializer` classes.
+* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view.
+* 201 Responses now return a 'Location' header.
## 2.1.2
diff --git a/mkdocs.py b/mkdocs.py
index 8106e8e2..2918f7d3 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -11,6 +11,7 @@ docs_dir = os.path.join(root_dir, 'docs')
html_dir = os.path.join(root_dir, 'html')
local = not '--deploy' in sys.argv
+preview = '-p' in sys.argv
if local:
base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
@@ -80,3 +81,15 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
open(output_path, 'w').write(output.encode('utf-8'))
+
+if preview:
+ import subprocess
+
+ url = 'html/index.html'
+
+ try:
+ subprocess.Popen(["open", url]) # Mac
+ except OSError:
+ subprocess.Popen(["xdg-open", url]) # Linux
+ except:
+ os.startfile(url) # Windows
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 87c2de70..be225d0a 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -14,6 +14,7 @@ class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
+ model = None
serializer_class = None
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
@@ -30,8 +31,10 @@ class GenericAPIView(views.APIView):
def get_serializer_class(self):
"""
Return the class to use for the serializer.
- Use `self.serializer_class`, falling back to constructing a
- model serializer class from `self.model_serializer_class`
+
+ Defaults to using `self.serializer_class`, falls back to constructing a
+ model serializer class using `self.model_serializer_class`, with
+ `self.model` as the model.
"""
serializer_class = self.serializer_class
@@ -57,32 +60,42 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
paginate_by = api_settings.PAGINATE_BY
+ paginate_by_param = api_settings.PAGINATE_BY_PARAM
filter_backend = api_settings.FILTER_BACKEND
def filter_queryset(self, queryset):
+ """
+ Given a queryset, filter it with whichever filter backend is in use.
+ """
if not self.filter_backend:
return queryset
backend = self.filter_backend()
return backend.filter_queryset(self.request, queryset, self)
- def get_filtered_queryset(self):
- return self.filter_queryset(self.get_queryset())
-
- def get_pagination_serializer_class(self):
+ def get_pagination_serializer(self, page=None):
"""
- Return the class to use for the pagination serializer.
+ Return a serializer instance to use with paginated data.
"""
class SerializerClass(self.pagination_serializer_class):
class Meta:
object_serializer_class = self.get_serializer_class()
- return SerializerClass
-
- def get_pagination_serializer(self, page=None):
- pagination_serializer_class = self.get_pagination_serializer_class()
+ pagination_serializer_class = SerializerClass
context = self.get_serializer_context()
return pagination_serializer_class(instance=page, context=context)
+ def get_paginate_by(self, queryset):
+ """
+ Return the size of pages to use with pagination.
+ """
+ if self.paginate_by_param:
+ params = self.request.QUERY_PARAMS
+ try:
+ return int(params[self.paginate_by_param])
+ except (KeyError, ValueError):
+ pass
+ return self.paginate_by
+
class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
"""
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index fbaaa96d..1edcfa5c 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -22,13 +22,13 @@ class CreateModelMixin(object):
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
+
def get_success_headers(self, data):
- if 'url' in data:
- return {'Location': data.get('url')}
- else:
+ try:
+ return {'Location': data['url']}
+ except (TypeError, KeyError):
return {}
-
+
def pre_save(self, obj):
pass
@@ -41,14 +41,16 @@ class ListModelMixin(object):
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
def list(self, request, *args, **kwargs):
- self.object_list = self.get_filtered_queryset()
+ queryset = self.get_queryset()
+ self.object_list = self.filter_queryset(queryset)
# Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets.
allow_empty = self.get_allow_empty()
- if not allow_empty and len(self.object_list) == 0:
- error_args = {'class_name': self.__class__.__name__}
- raise Http404(self.empty_error % error_args)
+ if not allow_empty and not self.object_list:
+ class_name = self.__class__.__name__
+ error_msg = self.empty_error % {'class_name': class_name}
+ raise Http404(error_msg)
# Pagination size is set by the `.paginate_by` attribute,
# which may be `None` to disable pagination.
@@ -82,17 +84,18 @@ class UpdateModelMixin(object):
def update(self, request, *args, **kwargs):
try:
self.object = self.get_object()
- success_status = status.HTTP_200_OK
+ created = False
except Http404:
self.object = None
- success_status = status.HTTP_201_CREATED
+ created = True
serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save()
- return Response(serializer.data, status=success_status)
+ status_code = created and status.HTTP_201_CREATED or status.HTTP_200_OK
+ return Response(serializer.data, status=status_code)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 4f10481d..ee24a4ad 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -54,19 +54,26 @@ DEFAULTS = {
'user': None,
'anon': None,
},
+
+ # Pagination
'PAGINATE_BY': None,
+ 'PAGINATE_BY_PARAM': None,
+
+ # Filtering
'FILTER_BACKEND': None,
+ # Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
+ # Browser enhancements
'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': 'accept',
'URL_FORMAT_OVERRIDE': 'format',
- 'FORMAT_SUFFIX_KWARG': 'format'
+ 'FORMAT_SUFFIX_KWARG': 'format',
}
diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py
index 713a7255..3062007d 100644
--- a/rest_framework/tests/pagination.py
+++ b/rest_framework/tests/pagination.py
@@ -34,6 +34,21 @@ if django_filters:
filter_backend = filters.DjangoFilterBackend
+class DefaultPageSizeKwargView(generics.ListAPIView):
+ """
+ View for testing default paginate_by_param usage
+ """
+ model = BasicModel
+
+
+class PaginateByParamView(generics.ListAPIView):
+ """
+ View for testing custom paginate_by_param usage
+ """
+ model = BasicModel
+ paginate_by_param = 'page_size'
+
+
class IntegrationTestPagination(TestCase):
"""
Integration tests for paginated list views.
@@ -135,7 +150,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
class UnitTestPagination(TestCase):
"""
- Unit tests for pagination of primative objects.
+ Unit tests for pagination of primitive objects.
"""
def setUp(self):
@@ -156,3 +171,68 @@ class UnitTestPagination(TestCase):
self.assertEquals(serializer.data['next'], None)
self.assertEquals(serializer.data['previous'], '?page=2')
self.assertEquals(serializer.data['results'], self.objects[20:])
+
+
+class TestUnpaginated(TestCase):
+ """
+ Tests for list views without pagination.
+ """
+
+ def setUp(self):
+ """
+ Create 13 BasicModel instances.
+ """
+ for i in range(13):
+ BasicModel(text=i).save()
+ self.objects = BasicModel.objects
+ self.data = [
+ {'id': obj.id, 'text': obj.text}
+ for obj in self.objects.all()
+ ]
+ self.view = DefaultPageSizeKwargView.as_view()
+
+ def test_unpaginated(self):
+ """
+ Tests the default page size for this view.
+ no page size --> no limit --> no meta data
+ """
+ request = factory.get('/')
+ response = self.view(request)
+ self.assertEquals(response.data, self.data)
+
+
+class TestCustomPaginateByParam(TestCase):
+ """
+ Tests for list views with default page size kwarg
+ """
+
+ def setUp(self):
+ """
+ Create 13 BasicModel instances.
+ """
+ for i in range(13):
+ BasicModel(text=i).save()
+ self.objects = BasicModel.objects
+ self.data = [
+ {'id': obj.id, 'text': obj.text}
+ for obj in self.objects.all()
+ ]
+ self.view = PaginateByParamView.as_view()
+
+ def test_default_page_size(self):
+ """
+ Tests the default page size for this view.
+ no page size --> no limit --> no meta data
+ """
+ request = factory.get('/')
+ response = self.view(request).render()
+ self.assertEquals(response.data, self.data)
+
+ def test_paginate_by_param(self):
+ """
+ If paginate_by_param is set, the new kwarg should limit per view requests.
+ """
+ request = factory.get('/?page_size=5')
+ response = self.view(request).render()
+ self.assertEquals(response.data['count'], 13)
+ self.assertEquals(response.data['results'], self.data[:5])
diff --git a/tox.ini b/tox.ini
index 3596bbdc..69eb3823 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,29 +8,29 @@ commands = {envpython} rest_framework/runtests/runtests.py
[testenv:py2.7-django1.5]
basepython = python2.7
deps = https://github.com/django/django/zipball/master
- git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+ django-filter==0.5.4
[testenv:py2.7-django1.4]
basepython = python2.7
deps = django==1.4.1
- git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+ django-filter==0.5.4
[testenv:py2.7-django1.3]
basepython = python2.7
deps = django==1.3.3
- git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+ django-filter==0.5.4
[testenv:py2.6-django1.5]
basepython = python2.6
deps = https://github.com/django/django/zipball/master
- git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+ django-filter==0.5.4
[testenv:py2.6-django1.4]
basepython = python2.6
deps = django==1.4.1
- git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+ django-filter==0.5.4
[testenv:py2.6-django1.3]
basepython = python2.6
deps = django==1.3.3
- git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+ django-filter==0.5.4