aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml55
-rw-r--r--docs/api-guide/filtering.md4
-rw-r--r--rest_framework/filters.py1
-rw-r--r--rest_framework/parsers.py20
-rw-r--r--rest_framework/serializers.py28
-rw-r--r--rest_framework/test.py2
-rw-r--r--rest_framework/utils/formatting.py6
-rw-r--r--tests/test_description.py24
-rw-r--r--tests/test_filters.py69
-rw-r--r--tests/test_parsers.py24
10 files changed, 172 insertions, 61 deletions
diff --git a/.travis.yml b/.travis.yml
index e768e146..a5b6d7d9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,43 +1,28 @@
language: python
-python:
- - "2.6"
- - "2.7"
- - "3.2"
- - "3.3"
- - "3.4"
+python: 2.7
env:
- - DJANGO="django==1.7"
- - DJANGO="django==1.6.5"
- - DJANGO="django==1.5.8"
- - DJANGO="django==1.4.13"
+ - TOX_ENV=flake8
+ - TOX_ENV=py3.4-django1.7
+ - TOX_ENV=py3.3-django1.7
+ - TOX_ENV=py3.2-django1.7
+ - TOX_ENV=py2.7-django1.7
+ - TOX_ENV=py3.4-django1.6
+ - TOX_ENV=py3.3-django1.6
+ - TOX_ENV=py3.2-django1.6
+ - TOX_ENV=py2.7-django1.6
+ - TOX_ENV=py2.6-django1.6
+ - TOX_ENV=py3.4-django1.5
+ - TOX_ENV=py3.3-django1.5
+ - TOX_ENV=py3.2-django1.5
+ - TOX_ENV=py2.7-django1.5
+ - TOX_ENV=py2.6-django1.5
+ - TOX_ENV=py2.7-django1.4
+ - TOX_ENV=py2.6-django1.4
install:
- - pip install $DJANGO
- - pip install defusedxml==0.3
- - pip install Pillow==2.3.0
- - pip install django-guardian==1.2.3
- - pip install pytest-django==2.6.1
- - pip install flake8==2.2.2
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi"
- - "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} == '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=.
+ - "pip install tox --download-cache $HOME/.pip-cache"
script:
- - ./runtests.py
-
-matrix:
- exclude:
- - python: "2.6"
- env: DJANGO="django==1.7"
- - python: "3.2"
- env: DJANGO="django==1.4.13"
- - python: "3.3"
- env: DJANGO="django==1.4.13"
- - python: "3.4"
- env: DJANGO="django==1.4.13"
+ - tox -e $TOX_ENV
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index ec5ab61f..cfeb4334 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -193,7 +193,7 @@ filters using `Manufacturer` name. For example:
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
- fields = ['category', 'in_stock', 'manufacturer__name`]
+ fields = ['category', 'in_stock', 'manufacturer__name']
This enables us to make queries like:
@@ -211,7 +211,7 @@ This is nice, but it exposes the Django's double underscore convention as part o
class Meta:
model = Product
- fields = ['category', 'in_stock', 'manufacturer`]
+ fields = ['category', 'in_stock', 'manufacturer']
And now you can execute:
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/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/serializers.py b/rest_framework/serializers.py
index be8ad3f2..b3db3582 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -625,6 +625,20 @@ class ModelSerializerOptions(SerializerOptions):
self.write_only_fields = getattr(meta, 'write_only_fields', ())
+def _get_class_mapping(mapping, obj):
+ """
+ Takes a dictionary with classes as keys, and an object.
+ Traverses the object's inheritance hierarchy in method
+ resolution order, and returns the first matching value
+ from the dictionary or None.
+
+ """
+ return next(
+ (mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping),
+ None
+ )
+
+
class ModelSerializer(Serializer):
"""
A serializer that deals with model instances and querysets.
@@ -899,15 +913,17 @@ class ModelSerializer(Serializer):
models.URLField: ['max_length'],
}
- if model_field.__class__ in attribute_dict:
- attributes = attribute_dict[model_field.__class__]
+ attributes = _get_class_mapping(attribute_dict, model_field)
+ if attributes:
for attribute in attributes:
kwargs.update({attribute: getattr(model_field, attribute)})
- try:
- return self.field_mapping[model_field.__class__](**kwargs)
- except KeyError:
- return ModelField(model_field=model_field, **kwargs)
+ serializer_field_class = _get_class_mapping(
+ self.field_mapping, model_field)
+
+ if serializer_field_class:
+ return serializer_field_class(**kwargs)
+ return ModelField(model_field=model_field, **kwargs)
def get_validation_exclusions(self, instance=None):
"""
diff --git a/rest_framework/test.py b/rest_framework/test.py
index f89a6dcd..9b40353a 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
Encode the data returning a two tuple of (bytes, content_type)
"""
- if not data:
+ if data is None:
return ('', content_type)
assert format is None or content_type is None, (
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/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_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_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