diff options
| -rw-r--r-- | .travis.yml | 3 | ||||
| -rw-r--r-- | docs/api-guide/filtering.md | 7 | ||||
| -rw-r--r-- | docs/api-guide/generic-views.md | 18 | ||||
| -rw-r--r-- | docs/api-guide/settings.md | 22 | ||||
| -rw-r--r-- | docs/index.md | 4 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 2 | ||||
| -rwxr-xr-x | mkdocs.py | 13 | ||||
| -rw-r--r-- | rest_framework/generics.py | 35 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 27 | ||||
| -rw-r--r-- | rest_framework/settings.py | 9 | ||||
| -rw-r--r-- | rest_framework/tests/pagination.py | 82 | ||||
| -rw-r--r-- | tox.ini | 12 |
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 @@ -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]) @@ -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 |
