diff --git a/.travis.yml b/.travis.yml index ececf3e9d..a5b6d7d91 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="https://www.djangoproject.com/download/1.7c2/tarball/" - - 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} == '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" - - export PYTHONPATH=. + - "pip install tox --download-cache $HOME/.pip-cache" script: - - ./runtests.py - -matrix: - exclude: - - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/" - - 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/CONTRIBUTING.md b/CONTRIBUTING.md index ff6018b82..a6dd05a0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,10 +62,10 @@ To run the tests, clone the repository, and then: virtualenv env env/bin/activate pip install -r requirements.txt - pip install -r optionals.txt + pip install -r requirements-test.txt # Run the tests - py.test + ./runtests.py You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: diff --git a/MANIFEST.in b/MANIFEST.in index 15c4d0b08..d407865fb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ recursive-include rest_framework/static *.js *.css *.png recursive-include rest_framework/templates *.html +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/README.md b/README.md index d33177399..63513f758 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Startup up a new project like so... pip install django pip install djangorestframework - django-admin startproject example . + django-admin.py startproject example . ./manage.py syncdb Now edit the `example/urls.py` module in your project: diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index ec5ab61fe..cfeb43349 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/docs/topics/2.4-announcement.md b/docs/topics/2.4-announcement.md index 5f90319ab..09294b910 100644 --- a/docs/topics/2.4-announcement.md +++ b/docs/topics/2.4-announcement.md @@ -17,7 +17,7 @@ The optional authtoken application now includes support for *both* Django 1.7 sc ## Deprecation of `.model` view attribute -The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. It's usage results in more implicit, less obvious behavior. +The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. Its usage results in more implicit, less obvious behavior. The documentation has previously stated that usage of the more explicit style is prefered, and we're now taking that one step further and deprecating the usage of the `.model` shortcut. @@ -128,7 +128,7 @@ There are also a number of other features and bugfixes as [listed in the release Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting. -Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Trottle-Wait-Seconds` header which will be fully deprecated in 3.0. +Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Throttle-Wait-Seconds` header which will be fully deprecated in 3.0. ## Deprecations @@ -163,10 +163,10 @@ The next planned release will be 3.0, featuring an improved and simplified seria Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible! [lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases -[2-4-release-notes]: ./topics/release-notes/#240 +[2-4-release-notes]: release-notes#240 [view-name-and-description-settings]: ../api-guide/settings/#view-names-and-descriptions [client-ip-identification]: ../api-guide/throttling/#how-clients-are-identified -[2-3-announcement]: ./topics/2.3-announcement +[2-3-announcement]: 2.3-announcement [github-labels]: https://github.com/tomchristie/django-rest-framework/issues [github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones -[kickstarter-sponsors]: ./topics/kickstarter-announcement/#sponsors +[kickstarter-sponsors]: kickstarter-announcement#sponsors diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 29a0afcd3..d758ae6af 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,18 @@ 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 + +* Bugfix: Fix broken login template for browsable API. + ### 2.4.0 **Date**: 29th August 2014 diff --git a/requirements-test.txt b/requirements-test.txt index 411daeba2..d6ee5c6fd 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,6 +8,7 @@ flake8==2.2.2 markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 +django-guardian==1.2.4 django-filter>=0.5.4 django-oauth-plus>=2.2.1 oauth2>=1.5.211 diff --git a/requirements.txt b/requirements.txt index 730c1d07a..8a6982305 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 f95bdc22e..8d82a4b90 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '2.4.0' +__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 5721a869e..f3fec05ec 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 3ec28908d..7496a629e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -51,8 +51,11 @@ def get_attribute(instance, attrs): for attr in attrs: try: instance = getattr(instance, attr) - except AttributeError: - return instance[attr] + except AttributeError as exc: + try: + return instance[attr] + except (KeyError, TypeError): + raise exc return instance diff --git a/rest_framework/filters.py b/rest_framework/filters.py index e20800130..c580f9351 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 478d32b49..9cf31629f 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) @@ -61,6 +62,7 @@ class BasePaginationSerializer(serializers.Serializer): """ super(BasePaginationSerializer, self).__init__(*args, **kwargs) results_field = self.results_field + try: object_serializer = self.Meta.object_serializer_class except AttributeError: @@ -70,7 +72,7 @@ class BasePaginationSerializer(serializers.Serializer): child=object_serializer(), source='object_list' ) - self.fields[results_field].bind(results_field, self, self) # TODO: Support automatic binding + self.fields[results_field].bind(results_field, self, self) class PaginationSerializer(BasePaginationSerializer): diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index aa4fd3f11..c287908dc 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/routers.py b/rest_framework/routers.py index ae56673d2..8f1ab6fa7 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -284,10 +284,10 @@ class DefaultRouter(SimpleRouter): class APIRoot(views.APIView): _ignore_model_permissions = True - def get(self, request, format=None): + def get(self, request, *args, **kwargs): ret = {} for key, url_name in api_root_dict.items(): - ret[key] = reverse(url_name, request=request, format=format) + ret[key] = reverse(url_name, request=request, format=kwargs.get('format', None)) return Response(ret) return APIRoot.as_view() diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 13e579398..8fe999aec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -317,6 +317,19 @@ class ModelSerializerOptions(object): self.depth = getattr(meta, 'depth', 0) +def lookup_class(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): field_mapping = { models.AutoField: IntegerField, @@ -580,13 +593,20 @@ class ModelSerializer(Serializer): if decimal_places is not None: kwargs['decimal_places'] = decimal_places + if isinstance(model_field, models.BooleanField): + # models.BooleanField has `blank=True`, but *is* actually + # required *unless* a default is provided. + # Also note that <1.6 `default=False`, >=1.6 `default=None`. + kwargs.pop('required', None) + if validator_kwarg: kwargs['validators'] = validator_kwarg - try: - return self.field_mapping[model_field.__class__](**kwargs) - except KeyError: - return ModelField(model_field=model_field, **kwargs) + cls = lookup_class(self.field_mapping, model_field) + if cls is None: + cls = ModelField + kwargs['model_field'] = model_field + return cls(**kwargs) class HyperlinkedModelSerializerOptions(ModelSerializerOptions): diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index cee9724d5..a84ccf269 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -5,14 +5,14 @@ {% block head %} - + {% block meta %} {% endblock %} - + {% block title %}Django REST framework{% endblock %} - + {% block style %} {% block bootstrap_theme %} @@ -21,10 +21,11 @@ {% endblock %} - + {% endblock %} + {% block body %}
@@ -43,17 +44,9 @@ @@ -84,7 +77,7 @@
GET - +
{% if display_edit_forms %} - + {% if post_form or raw_data_post_form %}
{% if post_form %} @@ -189,7 +182,7 @@
{% endif %} - + {% if put_form or raw_data_put_form or raw_data_patch_form %}
{% if put_form %} @@ -245,7 +238,7 @@ {% endif %}
- +