aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml6
-rw-r--r--docs/topics/release-notes.md6
-rw-r--r--requirements.txt2
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authentication.py2
-rw-r--r--rest_framework/fields.py8
-rw-r--r--rest_framework/filters.py1
-rw-r--r--rest_framework/pagination.py9
-rw-r--r--rest_framework/parsers.py20
-rw-r--r--rest_framework/templates/rest_framework/base.html28
-rw-r--r--rest_framework/templates/rest_framework/login_base.html35
-rw-r--r--rest_framework/templatetags/rest_framework.py21
-rw-r--r--rest_framework/utils/formatting.py6
-rw-r--r--rest_framework/views.py10
-rw-r--r--tests/browsable_api/__init__.py0
-rw-r--r--tests/browsable_api/auth_urls.py10
-rw-r--r--tests/browsable_api/no_auth_urls.py9
-rw-r--r--tests/browsable_api/test_browsable_api.py65
-rw-r--r--tests/browsable_api/views.py15
-rw-r--r--tests/test_description.py24
-rw-r--r--tests/test_fields.py12
-rw-r--r--tests/test_filters.py69
-rw-r--r--tests/test_pagination.py19
-rw-r--r--tests/test_parsers.py24
-rw-r--r--tox.ini8
25 files changed, 345 insertions, 66 deletions
diff --git a/.travis.yml b/.travis.yml
index ececf3e9..e768e146 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,7 @@ python:
- "3.4"
env:
- - DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
+ - DJANGO="django==1.7"
- DJANGO="django==1.6.5"
- DJANGO="django==1.5.8"
- DJANGO="django==1.4.13"
@@ -25,7 +25,7 @@ install:
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
- - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7c2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
+ - "if [[ ${DJANGO} == 'django==1.7' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
- export PYTHONPATH=.
script:
@@ -34,7 +34,7 @@ script:
matrix:
exclude:
- python: "2.6"
- env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
+ env: DJANGO="django==1.7"
- python: "3.2"
env: DJANGO="django==1.4.13"
- python: "3.3"
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index f0e9f210..d758ae6a 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,12 @@ You can determine your currently installed version using `pip freeze`:
## 2.4.x series
+### 2.4.2
+
+**Date**: 3rd September 2014
+
+* Bugfix: Fix broken pagination for 2.4.x series.
+
### 2.4.1
**Date**: 1st September 2014
diff --git a/requirements.txt b/requirements.txt
index 730c1d07..8a698230 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1 @@
-Django>=1.3
+Django>=1.4.2
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 7c187639..8d82a4b9 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '2.4.1'
+__version__ = '2.4.2'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie'
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index 5721a869..f3fec05e 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -344,7 +344,7 @@ class OAuth2Authentication(BaseAuthentication):
user = token.user
if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.username
+ msg = 'User inactive or deleted: %s' % user.get_username()
raise exceptions.AuthenticationFailed(msg)
return (user, token)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 9d707c9b..8e15345d 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -475,8 +475,12 @@ class CharField(WritableField):
if isinstance(value, six.string_types):
return value
- if value is None and not self.allow_none:
- return ''
+ if value is None:
+ if not self.allow_none:
+ return ''
+ else:
+ # Return None explicitly because smart_text(None) == 'None'. See #1834 for details
+ return None
return smart_text(value)
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index e2080013..c580f935 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -56,7 +56,6 @@ class DjangoFilterBackend(BaseFilterBackend):
class Meta:
model = queryset.model
fields = filter_fields
- order_by = True
return AutoFilterSet
return None
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index d51ea929..1f5749f1 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -43,8 +43,9 @@ class DefaultObjectSerializer(serializers.Field):
as the default.
"""
- def __init__(self, source=None, context=None):
- # Note: Swallow context kwarg - only required for eg. ModelSerializer.
+ def __init__(self, source=None, many=None, context=None):
+ # Note: Swallow context and many kwargs - only required for
+ # eg. ModelSerializer.
super(DefaultObjectSerializer, self).__init__(source=source)
@@ -82,7 +83,9 @@ class BasePaginationSerializer(serializers.Serializer):
else:
context_kwarg = {}
- self.fields[results_field] = object_serializer(source='object_list', **context_kwarg)
+ self.fields[results_field] = object_serializer(source='object_list',
+ many=True,
+ **context_kwarg)
class PaginationSerializer(BasePaginationSerializer):
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index aa4fd3f1..c287908d 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -11,7 +11,7 @@ from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from django.utils import six
-from rest_framework.compat import etree, yaml, force_text
+from rest_framework.compat import etree, yaml, force_text, urlparse
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
@@ -290,6 +290,22 @@ class FileUploadParser(BaseParser):
try:
meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
- return force_text(disposition[1]['filename'])
+ filename_parm = disposition[1]
+ if 'filename*' in filename_parm:
+ return self.get_encoded_filename(filename_parm)
+ return force_text(filename_parm['filename'])
except (AttributeError, KeyError):
pass
+
+ def get_encoded_filename(self, filename_parm):
+ """
+ Handle encoded filenames per RFC6266. See also:
+ http://tools.ietf.org/html/rfc2231#section-4
+ """
+ encoded_filename = force_text(filename_parm['filename*'])
+ try:
+ charset, lang, filename = encoded_filename.split('\'', 2)
+ filename = urlparse.unquote(filename)
+ except (ValueError, LookupError):
+ filename = force_text(filename_parm['filename'])
+ return filename
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e54e3814..a84ccf26 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -5,14 +5,14 @@
<html>
<head>
{% block head %}
-
+
{% block meta %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="robots" content="NONE,NOARCHIVE" />
{% endblock %}
-
+
<title>{% block title %}Django REST framework{% endblock %}</title>
-
+
{% block style %}
{% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
@@ -21,7 +21,7 @@
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %}
-
+
{% endblock %}
</head>
@@ -44,17 +44,9 @@
<ul class="nav pull-right">
{% block userlinks %}
{% if user.is_authenticated %}
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
- {{ user }}
- <b class="caret"></b>
- </a>
- <ul class="dropdown-menu">
- <li>{% optional_logout request %}</li>
- </ul>
- </li>
+ {% optional_logout request user %}
{% else %}
- <li>{% optional_login request %}</li>
+ {% optional_login request %}
{% endif %}
{% endblock %}
</ul>
@@ -85,7 +77,7 @@
<div class="btn-group format-selection">
<a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}'
rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
-
+
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown"
title="Specify a format for the GET request">
<span class="caret"></span>
@@ -144,7 +136,7 @@
</div>
{% if display_edit_forms %}
-
+
{% if post_form or raw_data_post_form %}
<div {% if post_form %}class="tabbable"{% endif %}>
{% if post_form %}
@@ -190,7 +182,7 @@
</div>
</div>
{% endif %}
-
+
{% if put_form or raw_data_put_form or raw_data_patch_form %}
<div {% if put_form %}class="tabbable"{% endif %}>
{% if put_form %}
@@ -246,7 +238,7 @@
{% endif %}
</div>
<!-- END Content -->
-
+
<footer>
{% block footer %}
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p>
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index 43860e53..8ab682ac 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -17,21 +17,44 @@
<div class="row-fluid">
<div>
- <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
+ <form action="{% url 'rest_framework:login' %}" role="form" method="post">
{% csrf_token %}
- <div id="div_id_username" class="clearfix control-group">
+ <div id="div_id_username"
+ class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<div class="controls">
<Label class="span4">Username:</label>
- <input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
+ <input style="height: 25px" type="text" name="username" maxlength="100"
+ autocapitalize="off"
+ autocorrect="off" class="span12 textinput textInput"
+ id="id_username" required
+ {% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
+ {% if form.username.errors %}
+ <p class="text-error">
+ {{ form.username.errors|striptags }}
+ </p>
+ {% endif %}
</div>
</div>
- <div id="div_id_password" class="clearfix control-group">
- <div class="controls">
+ <div id="div_id_password"
+ class="clearfix control-group {% if form.password.errors %}error{% endif %}">
+ <div class="controls">
<Label class="span4">Password:</label>
- <input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
+ <input style="height: 25px" type="password" name="password" maxlength="100"
+ autocapitalize="off" autocorrect="off" class="span12 textinput textInput"
+ id="id_password" required>
+ {% if form.password.errors %}
+ <p class="text-error">
+ {{ form.password.errors|striptags }}
+ </p>
+ {% endif %}
</div>
</div>
<input type="hidden" name="next" value="{{ next }}" />
+ {% if form.non_field_errors %}
+ {% for error in form.non_field_errors %}
+ <div class="well well-small text-error" style="border: none">{{ error }}</div>
+ {% endfor %}
+ {% endif %}
<div class="form-actions-no-box">
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
</div>
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index b80a7d77..864d64dd 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -41,22 +41,31 @@ def optional_login(request):
except NoReverseMatch:
return ''
- snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
+ snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path))
return snippet
@register.simple_tag
-def optional_logout(request):
+def optional_logout(request, user):
"""
Include a logout snippet if REST framework's logout view is in the URLconf.
"""
try:
logout_url = reverse('rest_framework:logout')
except NoReverseMatch:
- return ''
-
- snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
- return snippet
+ return '<li class="navbar-text">{user}</li>'.format(user=user)
+
+ snippet = """<li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ {user}
+ <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href='{href}?next={next}'>Log out</a></li>
+ </ul>
+ </li>"""
+
+ return snippet.format(user=user, href=logout_url, next=escape(request.path))
@register.simple_tag
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 6d53aed1..470af51b 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -2,11 +2,12 @@
Utility functions to return a formatted name and description for a given view.
"""
from __future__ import unicode_literals
+import re
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from rest_framework.compat import apply_markdown
-import re
+
+from rest_framework.compat import apply_markdown, force_text
def remove_trailing_string(content, trailing):
@@ -28,6 +29,7 @@ def dedent(content):
as it fails to dedent multiline docstrings that include
unindented text on the initial line.
"""
+ content = force_text(content)
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 23df3443..38346ab7 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -103,7 +103,9 @@ class APIView(View):
"""
view = super(APIView, cls).as_view(**initkwargs)
view.cls = cls
- return view
+ # Note: session based authentication is explicitly CSRF validated,
+ # all other authentication is CSRF exempt.
+ return csrf_exempt(view)
@property
def allowed_methods(self):
@@ -371,9 +373,9 @@ class APIView(View):
response.exception = True
return response
- # Note: session based authentication is explicitly CSRF validated,
- # all other authentication is CSRF exempt.
- @csrf_exempt
+ # Note: Views are made CSRF exempt from within `as_view` as to prevent
+ # accidental removal of this exemption in cases where `dispatch` needs to
+ # be overridden.
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
diff --git a/tests/browsable_api/__init__.py b/tests/browsable_api/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/browsable_api/__init__.py
diff --git a/tests/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py
new file mode 100644
index 00000000..bce7dcf9
--- /dev/null
+++ b/tests/browsable_api/auth_urls.py
@@ -0,0 +1,10 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
+
+from .views import MockView
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
+)
diff --git a/tests/browsable_api/no_auth_urls.py b/tests/browsable_api/no_auth_urls.py
new file mode 100644
index 00000000..5e3604a6
--- /dev/null
+++ b/tests/browsable_api/no_auth_urls.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns
+
+from .views import MockView
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+)
diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py
new file mode 100644
index 00000000..5f264783
--- /dev/null
+++ b/tests/browsable_api/test_browsable_api.py
@@ -0,0 +1,65 @@
+from __future__ import unicode_literals
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from rest_framework.test import APIClient
+
+
+class DropdownWithAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views enabled."""
+
+ urls = 'tests.browsable_api.auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_logout_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, '>Log out<')
+
+ def test_login_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertContains(response, '>Log in<')
+
+
+class NoDropdownWithoutAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views NOT enabled."""
+
+ urls = 'tests.browsable_api.no_auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_dropdown_not_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertNotContains(response, '<li class="dropdown">')
+
+ def test_dropdown_not_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertNotContains(response, '<li class="dropdown">')
diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py
new file mode 100644
index 00000000..000f4e80
--- /dev/null
+++ b/tests/browsable_api/views.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals
+
+from rest_framework.views import APIView
+from rest_framework import authentication
+from rest_framework import renderers
+from rest_framework.response import Response
+
+
+class MockView(APIView):
+
+ authentication_classes = (authentication.SessionAuthentication,)
+ renderer_classes = (renderers.BrowsableAPIRenderer,)
+
+ def get(self, request):
+ return Response({'a': 1, 'b': 2, 'c': 3})
diff --git a/tests/test_description.py b/tests/test_description.py
index 1e481f06..0675d209 100644
--- a/tests/test_description.py
+++ b/tests/test_description.py
@@ -98,6 +98,30 @@ class TestViewNamesAndDescriptions(TestCase):
pass
self.assertEqual(MockView().get_view_description(), '')
+ def test_view_description_can_be_promise(self):
+ """
+ Ensure a view may have a docstring that is actually a lazily evaluated
+ class that can be converted to a string.
+
+ See: https://github.com/tomchristie/django-rest-framework/issues/1708
+ """
+ # use a mock object instead of gettext_lazy to ensure that we can't end
+ # up with a test case string in our l10n catalog
+ class MockLazyStr(object):
+ def __init__(self, string):
+ self.s = string
+
+ def __str__(self):
+ return self.s
+
+ def __unicode__(self):
+ return self.s
+
+ class MockView(APIView):
+ __doc__ = MockLazyStr("a gettext string")
+
+ self.assertEqual(MockView().get_view_description(), 'a gettext string')
+
def test_markdown(self):
"""
Ensure markdown to HTML works as expected.
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 094ac1eb..0ddbe48b 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1004,6 +1004,18 @@ class BooleanField(TestCase):
self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
+class ModelCharField(TestCase):
+ """
+ Tests for CharField
+ """
+ def test_none_serializing(self):
+ class CharFieldSerializer(serializers.Serializer):
+ char = serializers.CharField(allow_none=True, required=False)
+ serializer = CharFieldSerializer(data={'char': None})
+ self.assertTrue(serializer.is_valid())
+ self.assertIsNone(serializer.object['char'])
+
+
class SerializerMethodFieldTest(TestCase):
"""
Tests for the SerializerMethodField field_to_native() behavior
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 47bffd43..5722fd7c 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -408,16 +408,61 @@ class SearchFilterTests(TestCase):
)
-class OrdringFilterModel(models.Model):
+class OrderingFilterModel(models.Model):
title = models.CharField(max_length=20)
text = models.CharField(max_length=100)
class OrderingFilterRelatedModel(models.Model):
- related_object = models.ForeignKey(OrdringFilterModel,
+ related_object = models.ForeignKey(OrderingFilterModel,
related_name="relateds")
+class DjangoFilterOrderingModel(models.Model):
+ date = models.DateField()
+ text = models.CharField(max_length=10)
+
+ class Meta:
+ ordering = ['-date']
+
+
+class DjangoFilterOrderingTests(TestCase):
+ def setUp(self):
+ data = [{
+ 'date': datetime.date(2012, 10, 8),
+ 'text': 'abc'
+ }, {
+ 'date': datetime.date(2013, 10, 8),
+ 'text': 'bcd'
+ }, {
+ 'date': datetime.date(2014, 10, 8),
+ 'text': 'cde'
+ }]
+
+ for d in data:
+ DjangoFilterOrderingModel.objects.create(**d)
+
+ def test_default_ordering(self):
+ class DjangoFilterOrderingView(generics.ListAPIView):
+ model = DjangoFilterOrderingModel
+ filter_backends = (filters.DjangoFilterBackend,)
+ filter_fields = ['text']
+ ordering = ('-date',)
+
+ view = DjangoFilterOrderingView.as_view()
+ request = factory.get('/')
+ response = view(request)
+
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'date': datetime.date(2014, 10, 8), 'text': 'cde'},
+ {'id': 2, 'date': datetime.date(2013, 10, 8), 'text': 'bcd'},
+ {'id': 1, 'date': datetime.date(2012, 10, 8), 'text': 'abc'}
+ ]
+ )
+
+
class OrderingFilterTests(TestCase):
def setUp(self):
# Sequence of title/text is:
@@ -436,11 +481,11 @@ class OrderingFilterTests(TestCase):
chr(idx + ord('b')) +
chr(idx + ord('c'))
)
- OrdringFilterModel(title=title, text=text).save()
+ OrderingFilterModel(title=title, text=text).save()
def test_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -459,7 +504,7 @@ class OrderingFilterTests(TestCase):
def test_reverse_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -478,7 +523,7 @@ class OrderingFilterTests(TestCase):
def test_incorrectfield_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -497,7 +542,7 @@ class OrderingFilterTests(TestCase):
def test_default_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
oredering_fields = ('text',)
@@ -516,7 +561,7 @@ class OrderingFilterTests(TestCase):
def test_default_ordering_using_string(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
ordering_fields = ('text',)
@@ -536,7 +581,7 @@ class OrderingFilterTests(TestCase):
def test_ordering_by_aggregate_field(self):
# create some related models to aggregate order by
num_objs = [2, 5, 3]
- for obj, num_relateds in zip(OrdringFilterModel.objects.all(),
+ for obj, num_relateds in zip(OrderingFilterModel.objects.all(),
num_objs):
for _ in range(num_relateds):
new_related = OrderingFilterRelatedModel(
@@ -545,11 +590,11 @@ class OrderingFilterTests(TestCase):
new_related.save()
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
ordering_fields = '__all__'
- queryset = OrdringFilterModel.objects.all().annotate(
+ queryset = OrderingFilterModel.objects.all().annotate(
models.Count("relateds"))
view = OrderingListView.as_view()
@@ -567,7 +612,7 @@ class OrderingFilterTests(TestCase):
def test_ordering_with_nonstandard_ordering_param(self):
with temporary_setting('ORDERING_PARAM', 'order', filters):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 80c33e2e..e1c2528b 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -412,6 +412,15 @@ class CustomPaginationSerializer(pagination.BasePaginationSerializer):
results_field = 'objects'
+class CustomFooSerializer(serializers.Serializer):
+ foo = serializers.CharField()
+
+
+class CustomFooPaginationSerializer(pagination.PaginationSerializer):
+ class Meta:
+ object_serializer_class = CustomFooSerializer
+
+
class TestCustomPaginationSerializer(TestCase):
def setUp(self):
objects = ['john', 'paul', 'george', 'ringo']
@@ -434,6 +443,16 @@ class TestCustomPaginationSerializer(TestCase):
}
self.assertEqual(serializer.data, expected)
+ def test_custom_pagination_serializer_with_custom_object_serializer(self):
+ objects = [
+ {'foo': 'bar'},
+ {'foo': 'spam'}
+ ]
+ paginator = Paginator(objects, 1)
+ page = paginator.page(1)
+ serializer = CustomFooPaginationSerializer(page)
+ serializer.data
+
class NonIntegerPage(object):
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
index 8af90677..3f2672df 100644
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
from __future__ import unicode_literals
from rest_framework.compat import StringIO
from django import forms
@@ -113,3 +115,25 @@ class TestFileUploadParser(TestCase):
parser = FileUploadParser()
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'file.txt')
+
+ def test_get_encoded_filename(self):
+ parser = FileUploadParser()
+
+ self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'fallback.txt')
+
+ def __replace_content_disposition(self, disposition):
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
diff --git a/tox.ini b/tox.ini
index 6f588de1..d40a7079 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,7 +18,7 @@ commands = ./runtests.py --lintonly
[testenv:py3.4-django1.7]
basepython = python3.4
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -26,7 +26,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
[testenv:py3.3-django1.7]
basepython = python3.3
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -34,7 +34,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
[testenv:py3.2-django1.7]
basepython = python3.2
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -42,7 +42,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
[testenv:py2.7-django1.7]
basepython = python2.7
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
# django-oauth-plus==2.2.1