mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-23 06:29:58 +03:00
Merge 5b399a844b
into 4e7805cb24
This commit is contained in:
commit
14a29de3ff
|
@ -11,6 +11,7 @@ env:
|
|||
|
||||
install:
|
||||
- pip install $DJANGO
|
||||
- pip install -r requirements.txt --use-mirrors
|
||||
- export PYTHONPATH=.
|
||||
|
||||
script:
|
||||
|
|
|
@ -88,13 +88,13 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[twitter]: https://twitter.com/_tomchristie
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
|
||||
|
||||
[docs]: http://tomchristie.github.com/django-rest-framework/
|
||||
[docs]: http://django-rest-framework.org/
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[pyyaml]: http://pypi.python.org/pypi/PyYAML
|
||||
|
|
|
@ -30,7 +30,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.UserBasicAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
)
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
|
|||
You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
|
||||
|
||||
class ExampleView(APIView):
|
||||
authentication_classes = (SessionAuthentication, UserBasicAuthentication)
|
||||
authentication_classes = (SessionAuthentication, BasicAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
@ -51,7 +51,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
|
|||
Or, if you're using the `@api_view` decorator with function based views.
|
||||
|
||||
@api_view(['GET'])
|
||||
@authentication_classes((SessionAuthentication, UserBasicAuthentication))
|
||||
@authentication_classes((SessionAuthentication, BasicAuthentication))
|
||||
@permissions_classes((IsAuthenticated,))
|
||||
def example_view(request, format=None):
|
||||
content = {
|
||||
|
|
|
@ -13,7 +13,7 @@ For example your project's `settings.py` file might include something like this:
|
|||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.YAMLRenderer',
|
||||
)
|
||||
),
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.YAMLParser',
|
||||
)
|
||||
|
|
|
@ -31,8 +31,8 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttles.AnonThrottle',
|
||||
'rest_framework.throttles.UserThrottle'
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle'
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/day',
|
||||
|
@ -136,7 +136,7 @@ For example, given the following views...
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttles.ScopedRateThrottle'
|
||||
'rest_framework.throttling.ScopedRateThrottle'
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'contacts': '1000/day',
|
||||
|
|
|
@ -159,7 +159,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
|
|
|
@ -51,6 +51,7 @@ The following people have helped make REST framework great.
|
|||
* Daniel Vaca Araujo - [diviei]
|
||||
* Madis Väin - [madisvain]
|
||||
* Stephan Groß - [minddust]
|
||||
* Pavel Savchenko - [asfaltboy]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -137,3 +138,4 @@ To contact the author directly:
|
|||
[diviei]: https://github.com/diviei
|
||||
[madisvain]: https://github.com/madisvain
|
||||
[minddust]: https://github.com/minddust
|
||||
[asfaltboy]: https://github.com/asfaltboy
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
>
|
||||
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
||||
|
||||
## Master
|
||||
|
||||
* If PUT creates an instance return '201 Created', instead of '200 OK'.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* **Fix all of the things.** (Well, almost.)
|
||||
|
|
|
@ -201,7 +201,7 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer`
|
|||
class SnippetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Snippet
|
||||
fields = ('pk', 'title', 'code', 'linenos', 'language', 'style')
|
||||
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -130,8 +130,6 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][
|
|||
|
||||
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]."
|
||||
|
||||
**Note: Right now the Browseable API only works with the CBV's. Need to fix that.**
|
||||
|
||||
### Browsability
|
||||
|
||||
Because the API chooses a return format based on what the client asks for, it will, by default, return an HTML-formatted representation of the resource when that resource is requested by a browser. This allows for the API to be easily browsable and usable by humans.
|
||||
|
|
|
@ -69,8 +69,8 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
|
|||
from snippetpost import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', views.SnippetList.as_view()),
|
||||
url(r'^(?P<pk>[0-9]+)$', views.SnippetDetail.as_view())
|
||||
url(r'^snippets/$', views.SnippetList.as_view()),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view())
|
||||
)
|
||||
|
||||
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||
|
|
|
@ -59,7 +59,7 @@ Now that we've got some users to work with, we'd better add representations of t
|
|||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('pk', 'username', 'snippets')
|
||||
fields = ('id', 'username', 'snippets')
|
||||
|
||||
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we've needed to add an explicit field for it.
|
||||
|
||||
|
@ -85,7 +85,7 @@ Right now, if we created a code snippet, there'd be no way of associating the us
|
|||
|
||||
The way we deal with that is by overriding a `.pre_save()` method on our snippet views, that allows us to handle any information that is implicit in the incoming request or requested URL.
|
||||
|
||||
On **both** the `SnippetList` and `SnippetInstance` view classes, add the following method:
|
||||
On **both** the `SnippetList` and `SnippetDetail` view classes, add the following method:
|
||||
|
||||
def pre_save(self, obj):
|
||||
obj.owner = self.request.user
|
||||
|
@ -112,7 +112,11 @@ Now that code snippets are associated with users we want to make sure that only
|
|||
|
||||
REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access.
|
||||
|
||||
Add the following property to **both** the `SnippetList` and `SnippetInstance` view classes.
|
||||
First add the following import in the views module
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
Then, add the following property to **both** the `SnippetList` and `SnippetDetail` view classes.
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
|
@ -169,7 +173,7 @@ In the snippets app, create a new file, `permissions.py`
|
|||
# Write permissions are only allowed to the owner of the snippet
|
||||
return obj.owner == request.user
|
||||
|
||||
Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetInstance` class:
|
||||
Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class:
|
||||
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
|
||||
IsOwnerOrReadOnly,)
|
||||
|
|
|
@ -27,9 +27,12 @@ The other obvious thing that's still missing from our pastebin API is the code h
|
|||
|
||||
Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two style of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.
|
||||
|
||||
The other thing we need to consider when creating the code highlight view is that there's no existing concreate generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
|
||||
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
|
||||
|
||||
Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method.
|
||||
Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your snippets.views add:
|
||||
|
||||
from rest_framework import renderers
|
||||
from rest_framework.response import Response
|
||||
|
||||
class SnippetHighlight(generics.SingleObjectAPIView):
|
||||
model = Snippet
|
||||
|
@ -111,7 +114,7 @@ After adding all those names into our URLconf, our final `'urls.py'` file should
|
|||
views.SnippetList.as_view(),
|
||||
name='snippet-list'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/$',
|
||||
views.SnippetInstance.as_view(),
|
||||
views.SnippetDetail.as_view(),
|
||||
name='snippet-detail'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$'
|
||||
views.SnippetHighlight.as_view(),
|
||||
|
|
|
@ -18,7 +18,7 @@ if local:
|
|||
index = 'index.html'
|
||||
else:
|
||||
base_url = 'http://django-rest-framework.org'
|
||||
suffix = ''
|
||||
suffix = '.html'
|
||||
index = ''
|
||||
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
Django>=1.3
|
||||
-e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
|
|
|
@ -211,9 +211,9 @@ class ModelField(WritableField):
|
|||
def from_native(self, value):
|
||||
try:
|
||||
rel = self.model_field.rel
|
||||
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
||||
except:
|
||||
return self.model_field.to_python(value)
|
||||
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = self.model_field._get_val_from_obj(obj)
|
||||
|
|
|
@ -6,7 +6,7 @@ from rest_framework import views, mixins
|
|||
from rest_framework.settings import api_settings
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.generic.list import MultipleObjectMixin
|
||||
|
||||
import django_filters
|
||||
|
||||
### Base classes for the generic views ###
|
||||
|
||||
|
@ -58,6 +58,37 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
|||
|
||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
paginate_by = api_settings.PAGINATE_BY
|
||||
filter_class = None
|
||||
filter_fields = None
|
||||
|
||||
def get_filter_class(self):
|
||||
"""
|
||||
Return the django-filters `FilterSet` used to filter the queryset.
|
||||
"""
|
||||
if self.filter_class:
|
||||
return self.filter_class
|
||||
|
||||
if self.filter_fields:
|
||||
class AutoFilterSet(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = self.model
|
||||
fields = self.filter_fields
|
||||
return AutoFilterSet
|
||||
|
||||
return None
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
filter_class = self.get_filter_class()
|
||||
|
||||
if filter_class:
|
||||
assert issubclass(filter_class.Meta.model, self.model), \
|
||||
"%s is not a subclass of %s" % (filter_class.Meta.model, self.model)
|
||||
return filter_class(self.request.GET, queryset=queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_filtered_queryset(self):
|
||||
return self.filter_queryset(self.get_queryset())
|
||||
|
||||
def get_pagination_serializer_class(self):
|
||||
"""
|
||||
|
|
|
@ -3,9 +3,6 @@ Basic building blocks for generic class based views.
|
|||
|
||||
We don't bind behaviour to http method handlers yet,
|
||||
which allows mixin classes to be composed in interesting ways.
|
||||
|
||||
Eg. Use mixins to build a Resource class, and have a Router class
|
||||
perform the binding of http methods to actions for us.
|
||||
"""
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
|
@ -37,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.
|
||||
|
@ -78,15 +75,17 @@ class UpdateModelMixin(object):
|
|||
def update(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
success_status = status.HTTP_200_OK
|
||||
except Http404:
|
||||
self.object = None
|
||||
success_status = status.HTTP_201_CREATED
|
||||
|
||||
serializer = self.get_serializer(data=request.DATA, instance=self.object)
|
||||
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.data, status=success_status)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
|
|
@ -3,7 +3,11 @@ from rest_framework import serializers
|
|||
# TODO: Support URLconf kwarg-style paging
|
||||
|
||||
|
||||
class NextPageField(serializers.Field):
|
||||
class PageField(serializers.Field):
|
||||
page_field = 'page'
|
||||
|
||||
|
||||
class NextPageField(PageField):
|
||||
"""
|
||||
Field that returns a link to the next page in paginated results.
|
||||
"""
|
||||
|
@ -12,13 +16,16 @@ class NextPageField(serializers.Field):
|
|||
return None
|
||||
page = value.next_page_number()
|
||||
request = self.context.get('request')
|
||||
relative_url = '?page=%d' % page
|
||||
relative_url = '?%s=%d' % (self.page_field, page)
|
||||
if request:
|
||||
for field, value in request.QUERY_PARAMS.iteritems():
|
||||
if field != self.page_field:
|
||||
relative_url += '&%s=%s' % (field, value)
|
||||
return request.build_absolute_uri(relative_url)
|
||||
return relative_url
|
||||
|
||||
|
||||
class PreviousPageField(serializers.Field):
|
||||
class PreviousPageField(PageField):
|
||||
"""
|
||||
Field that returns a link to the previous page in paginated results.
|
||||
"""
|
||||
|
@ -27,9 +34,12 @@ class PreviousPageField(serializers.Field):
|
|||
return None
|
||||
page = value.previous_page_number()
|
||||
request = self.context.get('request')
|
||||
relative_url = '?page=%d' % page
|
||||
relative_url = '?%s=%d' % (self.page_field, page)
|
||||
if request:
|
||||
return request.build_absolute_uri('?page=%d' % page)
|
||||
for field, value in request.QUERY_PARAMS.iteritems():
|
||||
if field != self.page_field:
|
||||
relative_url += '&%s=%s' % (field, value)
|
||||
return request.build_absolute_uri(relative_url)
|
||||
return relative_url
|
||||
|
||||
|
||||
|
|
160
rest_framework/tests/filterset.py
Normal file
160
rest_framework/tests/filterset.py
Normal file
|
@ -0,0 +1,160 @@
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.tests.models import FilterableItem, BasicModel
|
||||
import django_filters
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
# Basic filter on a list view.
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
model = FilterableItem
|
||||
filter_fields = ['decimal', 'date']
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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()
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
|
@ -236,7 +236,7 @@ class TestInstanceView(TestCase):
|
|||
request = factory.put('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
|
||||
updated = self.objects.get(id=1)
|
||||
self.assertEquals(updated.text, 'foobar')
|
||||
|
@ -251,7 +251,7 @@ class TestInstanceView(TestCase):
|
|||
request = factory.put('/5', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.view(request, pk=5).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
new_obj = self.objects.get(pk=5)
|
||||
self.assertEquals(new_obj.text, 'foobar')
|
||||
|
||||
|
@ -264,7 +264,7 @@ class TestInstanceView(TestCase):
|
|||
request = factory.put('/test_slug', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.slug_based_view(request, slug='test_slug').render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'})
|
||||
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
||||
self.assertEquals(new_obj.text, 'foobar')
|
||||
|
|
|
@ -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,11 @@
|
|||
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 rest_framework.tests.models import BasicModel, FilterableItem
|
||||
import django_filters
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
@ -15,6 +18,19 @@ class RootView(generics.ListCreateAPIView):
|
|||
paginate_by = 10
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class IntegrationTestPagination(TestCase):
|
||||
"""
|
||||
Integration tests for paginated list views.
|
||||
|
@ -22,7 +38,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 +78,57 @@ 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()
|
||||
|
||||
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.
|
||||
|
|
10
setup.py
10
setup.py
|
@ -52,7 +52,7 @@ if sys.argv[-1] == 'publish':
|
|||
|
||||
|
||||
setup(
|
||||
name='rest_framework',
|
||||
name='djangorestframework',
|
||||
version=version,
|
||||
url='http://django-rest-framework.org',
|
||||
download_url='http://pypi.python.org/pypi/rest_framework/',
|
||||
|
@ -63,7 +63,13 @@ setup(
|
|||
packages=get_packages('rest_framework'),
|
||||
package_data=get_package_data('rest_framework'),
|
||||
test_suite='rest_framework.runtests.runtests.main',
|
||||
install_requires=[],
|
||||
install_requires=[
|
||||
'Django>=1.3.0',
|
||||
'django-filter',
|
||||
],
|
||||
dependency_links = [
|
||||
'git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter',
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
|
|
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