diff options
| -rw-r--r-- | .travis.yml | 55 | ||||
| -rw-r--r-- | docs/api-guide/filtering.md | 4 | ||||
| -rw-r--r-- | rest_framework/filters.py | 1 | ||||
| -rw-r--r-- | rest_framework/parsers.py | 20 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 28 | ||||
| -rw-r--r-- | rest_framework/test.py | 2 | ||||
| -rw-r--r-- | rest_framework/utils/formatting.py | 6 | ||||
| -rw-r--r-- | tests/test_description.py | 24 | ||||
| -rw-r--r-- | tests/test_filters.py | 69 | ||||
| -rw-r--r-- | tests/test_parsers.py | 24 | 
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 | 
