This commit is contained in:
benkonrath 2012-11-01 12:28:25 -07:00
commit 14a29de3ff
25 changed files with 342 additions and 43 deletions

View File

@ -11,6 +11,7 @@ env:
install:
- pip install $DJANGO
- pip install -r requirements.txt --use-mirrors
- export PYTHONPATH=.
script:

View File

@ -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

View File

@ -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 = {

View File

@ -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',
)

View File

@ -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',

View File

@ -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/

View File

@ -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

View File

@ -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.)

View File

@ -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')

View File

@ -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.

View File

@ -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)

View File

@ -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,)

View File

@ -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(),

View File

@ -18,7 +18,7 @@ if local:
index = 'index.html'
else:
base_url = 'http://django-rest-framework.org'
suffix = ''
suffix = '.html'
index = ''

View File

@ -1 +1,2 @@
Django>=1.3
-e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter

View File

@ -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)

View File

@ -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):
"""

View File

@ -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)

View File

@ -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

View 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)

View File

@ -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')

View File

@ -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):

View File

@ -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.

View File

@ -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',

View File

@ -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