mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-03-03 19:00:17 +03:00
Merge pull request #383 from tomchristie/filtering
Support for filtering backends
This commit is contained in:
commit
c7df9694b5
|
@ -11,6 +11,8 @@ 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
|
||||
- export PYTHONPATH=.
|
||||
|
||||
script:
|
||||
|
|
179
docs/api-guide/filtering.md
Normal file
179
docs/api-guide/filtering.md
Normal file
|
@ -0,0 +1,179 @@
|
|||
<a class="github" href="filters.py"></a>
|
||||
|
||||
# Filtering
|
||||
|
||||
> The root QuerySet provided by the Manager describes all objects in the database table. Usually, though, you'll need to select only a subset of the complete set of objects.
|
||||
>
|
||||
> — [Django documentation][cite]
|
||||
|
||||
The default behavior of REST framework's generic list views is to return the entire queryset for a model manager. Often you will want your API to restrict the items that are returned by the queryset.
|
||||
|
||||
The simplest way to filter the queryset of any view that subclasses `MultipleObjectAPIView` is to override the `.get_queryset()` method.
|
||||
|
||||
Overriding this method allows you to customize the queryset returned by the view in a number of different ways.
|
||||
|
||||
## Filtering against the current user
|
||||
|
||||
You might want to filter the queryset to ensure that only results relevant to the currently authenticated user making the request are returned.
|
||||
|
||||
You can do so by filtering based on the value of `request.user`.
|
||||
|
||||
For example:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
model = Purchase
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
This view should return a list of all the purchases
|
||||
for the currently authenticated user.
|
||||
"""
|
||||
user = self.request.user
|
||||
return Purchase.objects.filter(purchaser=user)
|
||||
|
||||
|
||||
## Filtering against the URL
|
||||
|
||||
Another style of filtering might involve restricting the queryset based on some part of the URL.
|
||||
|
||||
For example if your URL config contained an entry like this:
|
||||
|
||||
url('^purchases/(?P<username>.+)/$', PurchaseList.as_view()),
|
||||
|
||||
You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
model = Purchase
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
This view should return a list of all the purchases for
|
||||
the user as determined by the username portion of the URL.
|
||||
"""
|
||||
username = self.kwargs['username']
|
||||
return Purchase.objects.filter(purchaser__username=username)
|
||||
|
||||
## Filtering against query parameters
|
||||
|
||||
A final example of filtering the initial queryset would be to determine the initial queryset based on query parameters in the url.
|
||||
|
||||
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
model = Purchase
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Optionally restricts the returned purchases to a given user,
|
||||
by filtering against a `username` query parameter in the URL.
|
||||
"""
|
||||
queryset = Purchase.objects.all()
|
||||
username = self.request.QUERY_PARAMS.get('username', None):
|
||||
if username is not None:
|
||||
queryset = queryset.filter(purchaser__username=username)
|
||||
return queryset
|
||||
|
||||
---
|
||||
|
||||
# Generic Filtering
|
||||
|
||||
As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex filters that can be specified by the client using query parameters.
|
||||
|
||||
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`.
|
||||
|
||||
pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter
|
||||
|
||||
You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'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
|
||||
|
||||
If all you need is simple equality-based filtering, you can set a `filter_fields` attribute on the view, listing the set of fields you wish to filter against.
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
filter_fields = ('category', 'in_stock')
|
||||
|
||||
This will automatically create a `FilterSet` class for the given fields, and will allow you to make requests such as:
|
||||
|
||||
http://example.com/api/products?category=clothing&in_stock=True
|
||||
|
||||
## Specifying a FilterSet
|
||||
|
||||
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example:
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
min_price = django_filters.NumberFilter(lookup_type='gte')
|
||||
max_price = django_filters.NumberFilter(lookup_type='lte')
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'min_price', 'max_price']
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
filter_class = ProductFilter
|
||||
|
||||
Which will allow you to make requests such as:
|
||||
|
||||
http://example.com/api/products?category=clothing&max_price=10.00
|
||||
|
||||
For more details on using filter sets see the [django-filter documentation][django-filter-docs].
|
||||
|
||||
---
|
||||
|
||||
**Hints & Tips**
|
||||
|
||||
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'FILTER_BACKEND'` setting.
|
||||
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
|
||||
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
|
||||
|
||||
---
|
||||
|
||||
## Overriding the intial queryset
|
||||
|
||||
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
|
||||
|
||||
class PurchasedProductsList(generics.ListAPIView):
|
||||
"""
|
||||
Return a list of all the products that the authenticated
|
||||
user has ever purchased, with optional filtering.
|
||||
"""
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
filter_class = ProductFilter
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return user.purchase_set.all()
|
||||
---
|
||||
|
||||
# Custom generic filtering
|
||||
|
||||
You can also provide your own generic filtering backend, or write an installable app for other developers to use.
|
||||
|
||||
To do so override `BaseFilterBackend`, and override the `.filter_queryset(self, request, queryset, view)` method.
|
||||
|
||||
To install the filter backend, set the `'FILTER_BACKEND'` key in your `'REST_FRAMEWORK'` setting, using the dotted import path of the filter backend class.
|
||||
|
||||
For example:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'FILTER_BACKEND': 'custom_filters.CustomFilterBackend'
|
||||
}
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
|
||||
[django-filter]: https://github.com/alex/django-filter
|
||||
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
|
||||
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
|
|
@ -34,6 +34,7 @@ The following packages are optional:
|
|||
|
||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the self describing API.
|
||||
* [PyYAML][yaml] (3.10+) - YAML content-type support.
|
||||
* [django-filter][django-filter] (master) - Filtering support.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -95,6 +96,7 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
* [Authentication][authentication]
|
||||
* [Permissions][permissions]
|
||||
* [Throttling][throttling]
|
||||
* [Filtering][filtering]
|
||||
* [Pagination][pagination]
|
||||
* [Content negotiation][contentnegotiation]
|
||||
* [Format suffixes][formatsuffixes]
|
||||
|
@ -162,6 +164,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[yaml]: http://pypi.python.org/pypi/PyYAML
|
||||
[django-filter]: https://github.com/alex/django-filter
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[image]: img/quickstart.png
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
|
@ -184,6 +187,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[authentication]: api-guide/authentication.md
|
||||
[permissions]: api-guide/permissions.md
|
||||
[throttling]: api-guide/throttling.md
|
||||
[filtering]: api-guide/filtering.md
|
||||
[pagination]: api-guide/pagination.md
|
||||
[contentnegotiation]: api-guide/content-negotiation.md
|
||||
[formatsuffixes]: api-guide/format-suffixes.md
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
<li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/filtering{{ suffix }}">Filtering</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/pagination{{ suffix }}">Pagination</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/content-negotiation{{ suffix }}">Content negotiation</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/format-suffixes{{ suffix }}">Format suffixes</a></li>
|
||||
|
|
|
@ -57,6 +57,7 @@ The following people have helped make REST framework great.
|
|||
* Osiloke Harold Emoekpere - [osiloke]
|
||||
* Michael Shepanski - [mjs7231]
|
||||
* Toni Michel - [tonimichel]
|
||||
* Ben Konrath - [benkonrath]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -148,4 +149,5 @@ To contact the author directly:
|
|||
[jmagnusson]: https://github.com/jmagnusson
|
||||
[osiloke]: https://github.com/osiloke
|
||||
[mjs7231]: https://github.com/mjs7231
|
||||
[tonimichel]: https://github.com/tonimichel
|
||||
[tonimichel]: https://github.com/tonimichel
|
||||
[benkonrath]: https://github.com/benkonrath
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
markdown>=2.1.0
|
||||
PyYAML>=3.10
|
||||
-e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
|
|
@ -5,6 +5,13 @@ versions of django/python, and compatbility wrappers around optional packages.
|
|||
# flake8: noqa
|
||||
import django
|
||||
|
||||
# django-filter is optional
|
||||
try:
|
||||
import django_filters
|
||||
except:
|
||||
django_filters = None
|
||||
|
||||
|
||||
# cStringIO only if it's available, otherwise StringIO
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
|
@ -348,33 +355,6 @@ except ImportError:
|
|||
yaml = None
|
||||
|
||||
|
||||
import unittest
|
||||
try:
|
||||
import unittest.skip
|
||||
except ImportError: # python < 2.7
|
||||
from unittest import TestCase
|
||||
import functools
|
||||
|
||||
def skip(reason):
|
||||
# Pasted from py27/lib/unittest/case.py
|
||||
"""
|
||||
Unconditionally skip a test.
|
||||
"""
|
||||
def decorator(test_item):
|
||||
if not (isinstance(test_item, type) and issubclass(test_item, TestCase)):
|
||||
@functools.wraps(test_item)
|
||||
def skip_wrapper(*args, **kwargs):
|
||||
pass
|
||||
test_item = skip_wrapper
|
||||
|
||||
test_item.__unittest_skip__ = True
|
||||
test_item.__unittest_skip_why__ = reason
|
||||
return test_item
|
||||
return decorator
|
||||
|
||||
unittest.skip = skip
|
||||
|
||||
|
||||
# xml.etree.parse only throws ParseError for python >= 2.7
|
||||
try:
|
||||
from xml.etree import ParseError as ETParseError
|
||||
|
|
59
rest_framework/filters.py
Normal file
59
rest_framework/filters.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from rest_framework.compat import django_filters
|
||||
|
||||
FilterSet = django_filters and django_filters.FilterSet or None
|
||||
|
||||
|
||||
class BaseFilterBackend(object):
|
||||
"""
|
||||
A base class from which all filter backend classes should inherit.
|
||||
"""
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
"""
|
||||
Return a filtered queryset.
|
||||
"""
|
||||
raise NotImplementedError(".filter_queryset() must be overridden.")
|
||||
|
||||
|
||||
class DjangoFilterBackend(BaseFilterBackend):
|
||||
"""
|
||||
A filter backend that uses django-filter.
|
||||
"""
|
||||
default_filter_set = FilterSet
|
||||
|
||||
def __init__(self):
|
||||
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
|
||||
|
||||
def get_filter_class(self, view):
|
||||
"""
|
||||
Return the django-filters `FilterSet` used to filter the queryset.
|
||||
"""
|
||||
filter_class = getattr(view, 'filter_class', None)
|
||||
filter_fields = getattr(view, 'filter_fields', None)
|
||||
view_model = getattr(view, 'model', None)
|
||||
|
||||
if filter_class:
|
||||
filter_model = filter_class.Meta.model
|
||||
|
||||
assert issubclass(filter_model, view_model), \
|
||||
'FilterSet model %s does not match view model %s' % \
|
||||
(filter_model, view_model)
|
||||
|
||||
return filter_class
|
||||
|
||||
if filter_fields:
|
||||
class AutoFilterSet(self.default_filter_set):
|
||||
class Meta:
|
||||
model = view_model
|
||||
fields = filter_fields
|
||||
return AutoFilterSet
|
||||
|
||||
return None
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
filter_class = self.get_filter_class(view)
|
||||
|
||||
if filter_class:
|
||||
return filter_class(request.GET, queryset=queryset)
|
||||
|
||||
return queryset
|
|
@ -58,6 +58,16 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
|||
|
||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
paginate_by = api_settings.PAGINATE_BY
|
||||
filter_backend = api_settings.FILTER_BACKEND
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -34,7 +34,7 @@ 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_queryset()
|
||||
self.object_list = self.get_filtered_queryset()
|
||||
|
||||
# Default is to allow empty querysets. This can be altered by setting
|
||||
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.templatetags.rest_framework import replace_query_param
|
||||
|
||||
# TODO: Support URLconf kwarg-style paging
|
||||
|
||||
|
@ -7,30 +8,30 @@ class NextPageField(serializers.Field):
|
|||
"""
|
||||
Field that returns a link to the next page in paginated results.
|
||||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_native(self, value):
|
||||
if not value.has_next():
|
||||
return None
|
||||
page = value.next_page_number()
|
||||
request = self.context.get('request')
|
||||
relative_url = '?page=%d' % page
|
||||
if request:
|
||||
return request.build_absolute_uri(relative_url)
|
||||
return relative_url
|
||||
url = request and request.build_absolute_uri() or ''
|
||||
return replace_query_param(url, self.page_field, page)
|
||||
|
||||
|
||||
class PreviousPageField(serializers.Field):
|
||||
"""
|
||||
Field that returns a link to the previous page in paginated results.
|
||||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_native(self, value):
|
||||
if not value.has_previous():
|
||||
return None
|
||||
page = value.previous_page_number()
|
||||
request = self.context.get('request')
|
||||
relative_url = '?page=%d' % page
|
||||
if request:
|
||||
return request.build_absolute_uri('?page=%d' % page)
|
||||
return relative_url
|
||||
url = request and request.build_absolute_uri() or ''
|
||||
return replace_query_param(url, self.page_field, page)
|
||||
|
||||
|
||||
class PaginationSerializerOptions(serializers.SerializerOptions):
|
||||
|
|
|
@ -107,6 +107,7 @@ import django
|
|||
if django.VERSION < (1, 3):
|
||||
INSTALLED_APPS += ('staticfiles',)
|
||||
|
||||
|
||||
# If we're running on the Jenkins server we want to archive the coverage reports as XML.
|
||||
import os
|
||||
if os.environ.get('HUDSON_URL', None):
|
||||
|
|
|
@ -55,6 +55,7 @@ DEFAULTS = {
|
|||
'anon': None,
|
||||
},
|
||||
'PAGINATE_BY': None,
|
||||
'FILTER_BACKEND': None,
|
||||
|
||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
@ -79,6 +80,7 @@ IMPORT_STRINGS = (
|
|||
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
|
||||
'DEFAULT_MODEL_SERIALIZER_CLASS',
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||
'FILTER_BACKEND',
|
||||
'UNAUTHENTICATED_USER',
|
||||
'UNAUTHENTICATED_TOKEN',
|
||||
)
|
||||
|
@ -142,8 +144,15 @@ class APISettings(object):
|
|||
if val and attr in self.import_strings:
|
||||
val = perform_import(val, attr)
|
||||
|
||||
self.validate_setting(attr, val)
|
||||
|
||||
# Cache the result
|
||||
setattr(self, attr, val)
|
||||
return val
|
||||
|
||||
def validate_setting(self, attr, val):
|
||||
if attr == 'FILTER_BACKEND' and val is not None:
|
||||
# Make sure we can initilize the class
|
||||
val()
|
||||
|
||||
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
||||
|
|
|
@ -11,6 +11,18 @@ import string
|
|||
register = template.Library()
|
||||
|
||||
|
||||
def replace_query_param(url, key, val):
|
||||
"""
|
||||
Given a URL and a key/val pair, set or replace an item in the query
|
||||
parameters of the URL, and return the new URL.
|
||||
"""
|
||||
(scheme, netloc, path, query, fragment) = urlsplit(url)
|
||||
query_dict = QueryDict(query).copy()
|
||||
query_dict[key] = val
|
||||
query = query_dict.urlencode()
|
||||
return urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
# Regex for adding classes to html snippets
|
||||
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
|
||||
|
||||
|
@ -31,19 +43,6 @@ hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|
|
|||
trailing_empty_content_re = re.compile(r'(?:<p>(?: |\s|<br \/>)*?</p>\s*)+\Z')
|
||||
|
||||
|
||||
# Helper function for 'add_query_param'
|
||||
def replace_query_param(url, key, val):
|
||||
"""
|
||||
Given a URL and a key/val pair, set or replace an item in the query
|
||||
parameters of the URL, and return the new URL.
|
||||
"""
|
||||
(scheme, netloc, path, query, fragment) = urlsplit(url)
|
||||
query_dict = QueryDict(query).copy()
|
||||
query_dict[key] = val
|
||||
query = query_dict.urlencode()
|
||||
return urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
# And the template tags themselves...
|
||||
|
||||
@register.simple_tag
|
||||
|
|
168
rest_framework/tests/filterset.py
Normal file
168
rest_framework/tests/filterset.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, status, filters
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.tests.models import FilterableItem, BasicModel
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
||||
if django_filters:
|
||||
# Basic filter on a list view.
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
filter_fields = ['decimal', 'date']
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
# These class are used to test a filter class.
|
||||
class SeveralFieldsFilter(django_filters.FilterSet):
|
||||
text = django_filters.CharFilter(lookup_type='icontains')
|
||||
decimal = django_filters.NumberFilter(lookup_type='lt')
|
||||
date = django_filters.DateFilter(lookup_type='gt')
|
||||
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
fields = ['text', 'decimal', 'date']
|
||||
|
||||
class FilterClassRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
filter_class = SeveralFieldsFilter
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
# These classes are used to test a misconfigured filter class.
|
||||
class MisconfiguredFilter(django_filters.FilterSet):
|
||||
text = django_filters.CharFilter(lookup_type='icontains')
|
||||
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
fields = ['text']
|
||||
|
||||
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
filter_class = MisconfiguredFilter
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
|
||||
class IntegrationTestFiltering(TestCase):
|
||||
"""
|
||||
Integration tests for filtered list views.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 10 FilterableItem instances.
|
||||
"""
|
||||
base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
|
||||
for i in range(10):
|
||||
text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
|
||||
decimal = base_data[1] + i
|
||||
date = base_data[2] - datetime.timedelta(days=i * 2)
|
||||
FilterableItem(text=text, decimal=decimal, date=date).save()
|
||||
|
||||
self.objects = FilterableItem.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_get_filtered_fields_root_view(self):
|
||||
"""
|
||||
GET requests to paginated ListCreateAPIView should return paginated results.
|
||||
"""
|
||||
view = FilterFieldsRootView.as_view()
|
||||
|
||||
# Basic test with no filter.
|
||||
request = factory.get('/')
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data, self.data)
|
||||
|
||||
# Tests that the decimal filter works.
|
||||
search_decimal = Decimal('2.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that the date filter works.
|
||||
search_date = datetime.date(2012, 9, 22)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22'
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] == search_date]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_get_filtered_class_root_view(self):
|
||||
"""
|
||||
GET requests to filtered ListCreateAPIView that have a filter_class set
|
||||
should return filtered results.
|
||||
"""
|
||||
view = FilterClassRootView.as_view()
|
||||
|
||||
# Basic test with no filter.
|
||||
request = factory.get('/')
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data, self.data)
|
||||
|
||||
# Tests that the decimal filter set with 'lt' in the filter class works.
|
||||
search_decimal = Decimal('4.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] < search_decimal]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that the date filter set with 'gt' in the filter class works.
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02'
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that the text filter set with 'icontains' in the filter class works.
|
||||
search_text = 'ff'
|
||||
request = factory.get('/?text=%s' % search_text)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if search_text in f['text'].lower()]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
# Tests that multiple filters works.
|
||||
search_decimal = Decimal('5.25')
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date))
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date and
|
||||
f['decimal'] < search_decimal]
|
||||
self.assertEquals(response.data, expected_data)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_incorrectly_configured_filter(self):
|
||||
"""
|
||||
An error should be displayed when the filter class is misconfigured.
|
||||
"""
|
||||
view = IncorrectlyConfiguredRootView.as_view()
|
||||
|
||||
request = factory.get('/')
|
||||
self.assertRaises(AssertionError, view, request)
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_unknown_filter(self):
|
||||
"""
|
||||
GET requests with filters that aren't configured should return 200.
|
||||
"""
|
||||
view = FilterFieldsRootView.as_view()
|
||||
|
||||
search_integer = 10
|
||||
request = factory.get('/?integer=%s' % search_integer)
|
||||
response = view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
|
@ -95,6 +95,13 @@ class Bookmark(RESTFrameworkModel):
|
|||
tags = GenericRelation(TaggedItem)
|
||||
|
||||
|
||||
# Model to test filtering.
|
||||
class FilterableItem(RESTFrameworkModel):
|
||||
text = models.CharField(max_length=100)
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
|
||||
|
||||
# Model for regression test for #285
|
||||
|
||||
class Comment(RESTFrameworkModel):
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.core.paginator import Paginator
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import generics, status, pagination
|
||||
from rest_framework.tests.models import BasicModel
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, status, pagination, filters
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.tests.models import BasicModel, FilterableItem
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
@ -15,6 +19,21 @@ class RootView(generics.ListCreateAPIView):
|
|||
paginate_by = 10
|
||||
|
||||
|
||||
if django_filters:
|
||||
class DecimalFilter(django_filters.FilterSet):
|
||||
decimal = django_filters.NumberFilter(lookup_type='lt')
|
||||
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
fields = ['text', 'decimal', 'date']
|
||||
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
paginate_by = 10
|
||||
filter_class = DecimalFilter
|
||||
filter_backend = filters.DjangoFilterBackend
|
||||
|
||||
|
||||
class IntegrationTestPagination(TestCase):
|
||||
"""
|
||||
Integration tests for paginated list views.
|
||||
|
@ -22,7 +41,7 @@ class IntegrationTestPagination(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 26 BasicModel intances.
|
||||
Create 26 BasicModel instances.
|
||||
"""
|
||||
for char in 'abcdefghijklmnopqrstuvwxyz':
|
||||
BasicModel(text=char * 3).save()
|
||||
|
@ -62,6 +81,58 @@ class IntegrationTestPagination(TestCase):
|
|||
self.assertNotEquals(response.data['previous'], None)
|
||||
|
||||
|
||||
class IntegrationTestPaginationAndFiltering(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 50 FilterableItem instances.
|
||||
"""
|
||||
base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
|
||||
for i in range(26):
|
||||
text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
|
||||
decimal = base_data[1] + i
|
||||
date = base_data[2] - datetime.timedelta(days=i * 2)
|
||||
FilterableItem(text=text, decimal=decimal, date=date).save()
|
||||
|
||||
self.objects = FilterableItem.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
self.view = FilterFieldsRootView.as_view()
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||
def test_get_paginated_filtered_root_view(self):
|
||||
"""
|
||||
GET requests to paginated filtered ListCreateAPIView should return
|
||||
paginated results. The next and previous links should preserve the
|
||||
filtered parameters.
|
||||
"""
|
||||
request = factory.get('/?decimal=15.20')
|
||||
response = self.view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data['count'], 15)
|
||||
self.assertEquals(response.data['results'], self.data[:10])
|
||||
self.assertNotEquals(response.data['next'], None)
|
||||
self.assertEquals(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
response = self.view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data['count'], 15)
|
||||
self.assertEquals(response.data['results'], self.data[10:15])
|
||||
self.assertEquals(response.data['next'], None)
|
||||
self.assertNotEquals(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['previous'])
|
||||
response = self.view(request).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data['count'], 15)
|
||||
self.assertEquals(response.data['results'], self.data[:10])
|
||||
self.assertNotEquals(response.data['next'], None)
|
||||
self.assertEquals(response.data['previous'], None)
|
||||
|
||||
|
||||
class UnitTestPagination(TestCase):
|
||||
"""
|
||||
Unit tests for pagination of primative objects.
|
||||
|
|
|
@ -131,12 +131,6 @@ class RendererIntegrationTests(TestCase):
|
|||
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
@unittest.skip('can\'t pass because view is a simple Django view and response is an ImmediateResponse')
|
||||
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
|
||||
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
|
||||
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
|
||||
self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE)
|
||||
|
||||
def test_specified_renderer_serializes_content_on_format_query(self):
|
||||
"""If a 'format' query is specified, the renderer with the matching
|
||||
format attribute should serialize the response."""
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.utils.encoding import smart_unicode
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
from rest_framework.compat import StringIO
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
|
6
tox.ini
6
tox.ini
|
@ -8,23 +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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
|
Loading…
Reference in New Issue
Block a user