diff options
30 files changed, 170 insertions, 65 deletions
| diff --git a/.travis.yml b/.travis.yml index 6191e7e2..996c3ae8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,10 @@ env:      - TOX_ENV=py26-django15      - TOX_ENV=py27-django14      - TOX_ENV=py26-django14 +    - TOX_ENV=py34-django18alpha +    - TOX_ENV=py33-django18alpha +    - TOX_ENV=py32-django18alpha +    - TOX_ENV=py27-django18alpha      - TOX_ENV=py34-djangomaster      - TOX_ENV=py33-djangomaster      - TOX_ENV=py32-djangomaster @@ -29,6 +33,10 @@ env:  matrix:    fast_finish: true    allow_failures: +    - env: TOX_ENV=py34-django18alpha +    - env: TOX_ENV=py33-django18alpha +    - env: TOX_ENV=py32-django18alpha +    - env: TOX_ENV=py27-django18alpha      - env: TOX_ENV=py34-djangomaster      - env: TOX_ENV=py33-djangomaster      - env: TOX_ENV=py32-djangomaster @@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox].  # Requirements  * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.5+, 1.6, 1.7) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7)  # Installation @@ -192,16 +192,15 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  [index]: http://www.django-rest-framework.org/  [oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth  [oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit -[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers -[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer -[functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views -[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views.html -[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets.html -[routers]: http://www.django-rest-framework.org/api-guide/routers.html -[serializers]: http://www.django-rest-framework.org/api-guide/serializers.html -[authentication]: http://www.django-rest-framework.org/api-guide/authentication.html - -[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement.html +[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#serializers +[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#modelserializer +[functionview-section]: http://www.django-rest-framework.org/api-guide/views/#function-based-views +[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views/ +[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets/ +[routers]: http://www.django-rest-framework.org/api-guide/routers/ +[serializers]: http://www.django-rest-framework.org/api-guide/serializers/ +[authentication]: http://www.django-rest-framework.org/api-guide/authentication/ +[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement/  [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion  [image]: http://www.django-rest-framework.org/img/quickstart.png diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index bb731817..ba114513 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -34,7 +34,7 @@ The value of `request.user` and `request.auth` for unauthenticated requests can  ## Setting the authentication scheme -The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION` setting.  For example. +The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION_CLASSES` setting.  For example.      REST_FRAMEWORK = {          'DEFAULT_AUTHENTICATION_CLASSES': ( @@ -126,7 +126,6 @@ To use the `TokenAuthentication` scheme you'll need to [configure the authentica          'rest_framework.authtoken'      ) -  ---  **Note:** Make sure to run `manage.py syncdb` after changing your settings. The `rest_framework.authtoken` app provides both Django (from v1.7) and South database migrations. See [Schema migrations](#schema-migrations) below. @@ -249,8 +248,6 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403  If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests.  See the [Django CSRF documentation][csrf-ajax] for more details. ---- -  # Custom authentication  To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method.  The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise. diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 50bd14dd..56811ec3 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -18,7 +18,7 @@ The handled exceptions are:  In each case, REST framework will return a response with an appropriate status code and content-type.  The body of the response will include any additional details regarding the nature of the error. -By default all error responses will include a key `detail` in the body of the response, but other keys may also be included. +Most error responses will include a key `detail` in the body of the response.  For example, the following request: @@ -33,6 +33,16 @@ Might receive an error response indicating that the `DELETE` method is not allow      {"detail": "Method 'DELETE' not allowed."} +Validation errors are handled slightly differently, and will include the field names as the keys in the response. If the validation error was not specific to a particular field then it will use the "non_field_errors" key, or whatever string value has been set for the `NON_FIELD_ERRORS_KEY` setting. + +Any example validation error might look like this: + +    HTTP/1.1 400 Bad Request +    Content-Type: application/json +    Content-Length: 94 + +    {"amount": ["A valid integer is required."], "description": ["This field may not be blank."]} +  ## Custom exception handling  You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects.  This allows you to control the style of error responses used by your API. diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 83977048..e00560c7 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -316,6 +316,7 @@ Typically you'd instead control this by setting `order_by` on the initial querys          queryset = User.objects.all()          serializer_class = UserSerializer          filter_backends = (filters.OrderingFilter,) +        ordering_fields = ('username', 'email')          ordering = ('username',)  The `ordering` attribute may be either a string or a list/tuple of strings. @@ -390,9 +391,9 @@ We could achieve the same behavior by overriding `get_queryset()` on the views,  The following third party packages provide additional filter implementations. -## Django REST framework chain +## Django REST framework filters package -The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field. +The [django-rest-framework-filters package][django-rest-framework-filters] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field.  [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters  [django-filter]: https://github.com/alex/django-filter @@ -402,4 +403,4 @@ The [django-rest-framework-chain package][django-rest-framework-chain] works tog  [view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models  [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py  [search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields -[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain +[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters diff --git a/docs/index.md b/docs/index.md index 16376985..efd99a4e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ Some reasons you might want to use REST framework:  REST framework requires the following:  * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.5+, 1.6, 1.7) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7)  The following packages are optional: diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 68d24782..5dbc5600 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -87,12 +87,12 @@ The resulting API changes are further detailed below.  #### The `.create()` and `.update()` methods. -The `.restore_object()` method is now replaced with two separate methods, `.create()` and `.update()`. - -These methods also replace the optional `.save_object()` method, which no longer exists. +The `.restore_object()` method is now removed, and we instead have two separate methods, `.create()` and `.update()`. These methods work slightly different to the previous `.restore_object()`.  When using the `.create()` and `.update()` methods you should both create *and save* the object instance. This is in contrast to the previous `.restore_object()` behavior that would instantiate the object but not save it. +These methods also replace the optional `.save_object()` method, which no longer exists. +  The following example from the tutorial previously used `restore_object()` to handle both creating and updating object instances.      def restore_object(self, attrs, instance=None): diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md index 7f705196..2a54fb94 100644 --- a/docs/topics/project-management.md +++ b/docs/topics/project-management.md @@ -58,6 +58,8 @@ The following template should be used for the description of the issue, and serv      #### New members.      If you wish to be considered for this or a future date, please comment against this or subsequent issues. +     +    To modify this process for future maintenance cycles make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.  #### Responsibilities of team members @@ -108,6 +110,8 @@ The following template should be used for the description of the issue, and serv      - [ ] Make a release announcement on the [discussion group](https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework).      - [ ] Make a release announcement on twitter.      - [ ] Close the milestone on GitHub. +     +    To modify this process for future releases make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.  When pushing the release to PyPI ensure that your environment has been installed from our development `requirement.txt`, so that documentation and PyPI installs are consistently being built against a pinned set of packages. @@ -176,6 +180,7 @@ The following issues still need to be addressed:  * Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin.  * Document ownership of the [live example][sandbox] API.  * Document ownership of the [mailing list][mailing-list] and IRC channel. +* Document ownership and management of the security mailing list.  [bus-factor]: http://en.wikipedia.org/wiki/Bus_factor  [un-triaged]: https://github.com/tomchristie/django-rest-framework/issues?q=is%3Aopen+no%3Alabel diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index b9216e36..c49dd62c 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,27 @@ You can determine your currently installed version using `pip freeze`:  ## 3.0.x series + +### 3.0.3 + +**Date**: [8th January 2015][3.0.3-milestone]. + +* Fix `MinValueValidator` on `models.DateField`. ([#2369][gh2369]) +* Fix serializer missing context when pagination is used. ([#2355][gh2355]) +* Namespaced router URLs are now supported by the `DefaultRouter`. ([#2351][gh2351]) +* `required=False` allows omission of value for output. ([#2342][gh2342]) +* Use textarea input for `models.TextField`. ([#2340][gh2340]) +* Use custom `ListSerializer` for pagination if required. ([#2331][gh2331], [#2327][gh2327]) +* Better behavior with null and '' for blank HTML fields. ([#2330][gh2330]) +* Ensure fields in `exclude` are model fields. ([#2319][gh2319]) +* Fix `IntegerField` and `max_length` argument incompatibility. ([#2317][gh2317]) +* Fix the YAML encoder for 3.0 serializers. ([#2315][gh2315], [#2283][gh2283]) +* Fix the behavior of empty HTML fields. ([#2311][gh2311], [#1101][gh1101]) +* Fix Metaclass attribute depth ignoring fields attribute. ([#2287][gh2287]) +* Fix `format_suffix_patterns` to work with Django's `i18n_patterns`. ([#2278][gh2278]) +* Ability to customize router URLs for custom actions, using `url_path`. ([#2010][gh2010]) +* Don't install Django REST Framework as egg. ([#2386][gh2386]) +  ### 3.0.2  **Date**: [17th December 2014][3.0.2-milestone]. @@ -680,6 +701,7 @@ For older release notes, [please see the GitHub repo](old-release-notes).  [3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22  [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 +[3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22  <!-- 3.0.1 -->  [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -729,3 +751,22 @@ For older release notes, [please see the GitHub repo](old-release-notes).  [gh2290]: https://github.com/tomchristie/django-rest-framework/issues/2290  [gh2291]: https://github.com/tomchristie/django-rest-framework/issues/2291  [gh2294]: https://github.com/tomchristie/django-rest-framework/issues/2294 +<!-- 3.0.3 --> +[gh1101]: https://github.com/tomchristie/django-rest-framework/issues/1101 +[gh2010]: https://github.com/tomchristie/django-rest-framework/issues/2010 +[gh2278]: https://github.com/tomchristie/django-rest-framework/issues/2278 +[gh2283]: https://github.com/tomchristie/django-rest-framework/issues/2283 +[gh2287]: https://github.com/tomchristie/django-rest-framework/issues/2287 +[gh2311]: https://github.com/tomchristie/django-rest-framework/issues/2311 +[gh2315]: https://github.com/tomchristie/django-rest-framework/issues/2315 +[gh2317]: https://github.com/tomchristie/django-rest-framework/issues/2317 +[gh2319]: https://github.com/tomchristie/django-rest-framework/issues/2319 +[gh2327]: https://github.com/tomchristie/django-rest-framework/issues/2327 +[gh2330]: https://github.com/tomchristie/django-rest-framework/issues/2330 +[gh2331]: https://github.com/tomchristie/django-rest-framework/issues/2331 +[gh2340]: https://github.com/tomchristie/django-rest-framework/issues/2340 +[gh2342]: https://github.com/tomchristie/django-rest-framework/issues/2342 +[gh2351]: https://github.com/tomchristie/django-rest-framework/issues/2351 +[gh2355]: https://github.com/tomchristie/django-rest-framework/issues/2355 +[gh2369]: https://github.com/tomchristie/django-rest-framework/issues/2369 +[gh2386]: https://github.com/tomchristie/django-rest-framework/issues/2386 diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 60a3d989..41ff4d07 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -191,7 +191,7 @@ Our `SnippetSerializer` class is replicating a lot of information that's also co  In the same way that Django provides both `Form` classes and `ModelForm` classes, REST framework includes both `Serializer` classes, and `ModelSerializer` classes.  Let's look at refactoring our serializer using the `ModelSerializer` class. -Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` class. +Open the file `snippets/serializers.py` again, and replace the `SnippetSerializer` class with the following.      class SnippetSerializer(serializers.ModelSerializer):          class Meta: diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 0a9ea3f1..abf82e49 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -64,7 +64,7 @@ That's looking good.  Again, it's still pretty similar to the function based vie  We'll also need to refactor our `urls.py` slightly now we're using class based views. -    from django.conf.urls import patterns, url +    from django.conf.urls import url      from rest_framework.urlpatterns import format_suffix_patterns      from snippets import views diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 592c77e8..887d1e56 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -177,7 +177,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 `SnippetDetail` class: +Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` view class:      permission_classes = (permissions.IsAuthenticatedOrReadOnly,                            IsOwnerOrReadOnly,) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index c21efd7f..2841f03e 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -106,6 +106,8 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p  After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this: +    from django.conf.urls import url, include +      # API endpoints      urlpatterns = format_suffix_patterns([          url(r'^$', views.api_root), diff --git a/requirements.txt b/requirements.txt index fe5edbe7..080206bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@  Django>=1.4.11  # Test requirements -pytest-django==2.6 -pytest==2.5.2 +pytest-django==2.8.0 +pytest==2.6.4  pytest-cov==1.6  flake8==2.2.2 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index dec89b3e..fdcebb7b 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____    __  """  __title__ = 'Django REST framework' -__version__ = '3.0.2' +__version__ = '3.0.3'  __author__ = 'Tom Christie'  __license__ = 'BSD 2-Clause'  __copyright__ = 'Copyright 2011-2015 Tom Christie' diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 7241da27..bd3802ad 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals  from django.core.exceptions import ImproperlyConfigured  from django.conf import settings  from django.utils.encoding import force_text -from django.utils.six.moves.urllib import parse as urlparse +from django.utils.six.moves.urllib.parse import urlparse as _urlparse  from django.utils import six  import django  import inspect @@ -38,10 +38,18 @@ def unicode_http_header(value):      return value +def total_seconds(timedelta): +    # TimeDelta.total_seconds() is only available in Python 2.7 +    if hasattr(timedelta, 'total_seconds'): +        return timedelta.total_seconds() +    else: +        return (timedelta.days * 86400.0) + float(timedelta.seconds) + (timedelta.microseconds / 1000000.0) + +  # OrderedDict only available in Python 2.7.  # This will always be the case in Django 1.7 and above, as these versions  # no longer support Python 2.6. -# For Django <= 1.6 and Python 2.6 fall back to OrderedDict. +# For Django <= 1.6 and Python 2.6 fall back to SortedDict.  try:      from collections import OrderedDict  except ImportError: @@ -187,7 +195,7 @@ except ImportError:  class RequestFactory(DjangoRequestFactory):      def generic(self, method, path,              data='', content_type='application/octet-stream', **extra): -        parsed = urlparse.urlparse(path) +        parsed = _urlparse(path)          data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)          r = {              'PATH_INFO': self._get_path(parsed), @@ -227,6 +235,8 @@ except ImportError:  if six.PY3:      SHORT_SEPARATORS = (',', ':')      LONG_SEPARATORS = (', ', ': ') +    INDENT_SEPARATORS = (',', ': ')  else:      SHORT_SEPARATORS = (b',', b':')      LONG_SEPARATORS = (b', ', b': ') +    INDENT_SEPARATORS = (b',', b': ') diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index f56f55ce..5482788a 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -135,13 +135,13 @@ class BasePagination(object):      display_page_controls = False      def paginate_queryset(self, queryset, request, view=None):  # pragma: no cover -        raise NotImplemented('paginate_queryset() must be implemented.') +        raise NotImplementedError('paginate_queryset() must be implemented.')      def get_paginated_response(self, data):  # pragma: no cover -        raise NotImplemented('get_paginated_response() must be implemented.') +        raise NotImplementedError('get_paginated_response() must be implemented.')      def to_html(self):  # pragma: no cover -        raise NotImplemented('to_html() must be implemented to display page controls.') +        raise NotImplementedError('to_html() must be implemented to display page controls.')  class PageNumberPagination(BasePagination): diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index cb23423d..dd069840 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -179,7 +179,7 @@ class FileUploadParser(BaseParser):          for index, handler in enumerate(upload_handlers):              file_obj = handler.file_complete(counters[index])              if file_obj: -                return DataAndFiles(None, {'file': file_obj}) +                return DataAndFiles({}, {'file': file_obj})          raise ParseError("FileUpload parse error - "                           "none of upload handlers can handle the stream") diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 05ac3d1c..a85edfec 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -7,6 +7,7 @@ from django.utils import six  from django.utils.encoding import smart_text  from django.utils.six.moves.urllib import parse as urlparse  from django.utils.translation import ugettext_lazy as _ +from rest_framework.compat import OrderedDict  from rest_framework.fields import get_attribute, empty, Field  from rest_framework.reverse import reverse  from rest_framework.utils import html @@ -103,7 +104,7 @@ class RelatedField(Field):      @property      def choices(self): -        return dict([ +        return OrderedDict([              (                  six.text_type(self.to_representation(item)),                  six.text_type(item) @@ -364,7 +365,7 @@ class ManyRelatedField(Field):              (item, self.child_relation.to_representation(item))              for item in iterable          ] -        return dict([ +        return OrderedDict([              (                  six.text_type(item_representation),                  six.text_type(item) + ' - ' + six.text_type(item_representation) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4c46b049..f970a363 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -18,7 +18,7 @@ from django.template import Context, RequestContext, loader, Template  from django.test.client import encode_multipart  from django.utils import six  from rest_framework import exceptions, serializers, status, VERSION -from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS +from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS, INDENT_SEPARATORS  from rest_framework.exceptions import ParseError  from rest_framework.settings import api_settings  from rest_framework.request import is_form_media_type, override_method @@ -43,7 +43,7 @@ class BaseRenderer(object):      render_style = 'text'      def render(self, data, accepted_media_type=None, renderer_context=None): -        raise NotImplemented('Renderer class requires .render() to be implemented') +        raise NotImplementedError('Renderer class requires .render() to be implemented')  class JSONRenderer(BaseRenderer): @@ -87,7 +87,11 @@ class JSONRenderer(BaseRenderer):          renderer_context = renderer_context or {}          indent = self.get_indent(accepted_media_type, renderer_context) -        separators = SHORT_SEPARATORS if (indent is None and self.compact) else LONG_SEPARATORS + +        if indent is None: +            separators = SHORT_SEPARATORS if self.compact else LONG_SEPARATORS +        else: +            separators = INDENT_SEPARATORS          ret = json.dumps(              data, cls=self.encoder_class, diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 61f3ccab..827da034 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -65,13 +65,13 @@ class BaseRouter(object):          If `base_name` is not specified, attempt to automatically determine          it from the viewset.          """ -        raise NotImplemented('get_default_base_name must be overridden') +        raise NotImplementedError('get_default_base_name must be overridden')      def get_urls(self):          """          Return a list of URL patterns, given the registered viewsets.          """ -        raise NotImplemented('get_urls must be overridden') +        raise NotImplementedError('get_urls must be overridden')      @property      def urls(self): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 3cce26b1..ca5af86e 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -176,7 +176,7 @@ class APISettings(object):      For example:          from rest_framework.settings import api_settings -        print api_settings.DEFAULT_RENDERER_CLASSES +        print(api_settings.DEFAULT_RENDERER_CLASSES)      Any setting with string import paths will be automatically resolved      and return the class, rather than the string literal. diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 361dbddf..0f10136d 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -32,10 +32,10 @@ class BaseThrottle(object):              if num_proxies == 0 or xff is None:                  return remote_addr              addrs = xff.split(',') -            client_addr = addrs[-min(num_proxies, len(xff))] +            client_addr = addrs[-min(num_proxies, len(addrs))]              return client_addr.strip() -        return xff if xff else remote_addr +        return ''.join(xff.split()) if xff else remote_addr      def wait(self):          """ @@ -173,12 +173,6 @@ class AnonRateThrottle(SimpleRateThrottle):          if request.user.is_authenticated():              return None  # Only throttle unauthenticated requests. -        ident = request.META.get('HTTP_X_FORWARDED_FOR') -        if ident is None: -            ident = request.META.get('REMOTE_ADDR') -        else: -            ident = ''.join(ident.split()) -          return self.cache_format % {              'scope': self.scope,              'ident': self.get_ident(request) diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 0bd24939..2160d18b 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -6,9 +6,11 @@ from django.db.models.query import QuerySet  from django.utils import six, timezone  from django.utils.encoding import force_text  from django.utils.functional import Promise +from rest_framework.compat import total_seconds  import datetime  import decimal  import json +import uuid  class JSONEncoder(json.JSONEncoder): @@ -38,10 +40,12 @@ class JSONEncoder(json.JSONEncoder):                  representation = representation[:12]              return representation          elif isinstance(obj, datetime.timedelta): -            return six.text_type(obj.total_seconds()) +            return six.text_type(total_seconds(obj))          elif isinstance(obj, decimal.Decimal):              # Serializers will coerce decimals to strings by default.              return float(obj) +        elif isinstance(obj, uuid.UUID): +            return six.text_type(obj)          elif isinstance(obj, QuerySet):              return tuple(obj)          elif hasattr(obj, 'tolist'): diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 65a04d06..f9960603 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -16,6 +16,9 @@ class ReturnDict(OrderedDict):      def copy(self):          return ReturnDict(self, serializer=self.serializer) +    def __repr__(self): +        return dict.__repr__(self) +  class ReturnList(list):      """ @@ -27,6 +30,9 @@ class ReturnList(list):          self.serializer = kwargs.pop('serializer')          super(ReturnList, self).__init__(*args, **kwargs) +    def __repr__(self): +        return list.__repr__(self) +  class BoundField(object):      """ diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index e31c71e9..a07b629f 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -17,7 +17,7 @@ class BaseVersioning(object):      def determine_version(self, request, *args, **kwargs):          msg = '{cls}.determine_version() must be implemented.' -        raise NotImplemented(msg.format( +        raise NotImplementedError(msg.format(              cls=self.__class__.__name__          )) @@ -67,6 +67,7 @@ setup(      packages=get_packages('rest_framework'),      package_data=get_package_data('rest_framework'),      install_requires=[], +    zip_safe=False,      classifiers=[          'Development Status :: 5 - Production/Stable',          'Environment :: Web Environment', diff --git a/tests/test_multitable_inheritance.py b/tests/test_multitable_inheritance.py index e1b40cc7..15627e1d 100644 --- a/tests/test_multitable_inheritance.py +++ b/tests/test_multitable_inheritance.py @@ -48,8 +48,8 @@ class InheritedModelSerializationTests(TestCase):          Assert that a model with a onetoone field that is the primary key is          not treated like a derived model          """ -        parent = ParentModel(name1='parent name') -        associate = AssociatedModel(name='hello', ref=parent) +        parent = ParentModel.objects.create(name1='parent name') +        associate = AssociatedModel.objects.create(name='hello', ref=parent)          serializer = AssociatedModelSerializer(associate)          self.assertEqual(set(serializer.data.keys()),                           set(['name', 'ref'])) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 7b78f7ba..3e64d8fe 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -1,6 +1,5 @@  # -*- coding: utf-8 -*-  from __future__ import unicode_literals -  from django.conf.urls import patterns, url, include  from django.core.cache import cache  from django.db import models @@ -8,6 +7,7 @@ from django.test import TestCase  from django.utils import six  from django.utils.translation import ugettext_lazy as _  from rest_framework import status, permissions +from rest_framework.compat import OrderedDict  from rest_framework.response import Response  from rest_framework.views import APIView  from rest_framework.renderers import BaseRenderer, JSONRenderer, BrowsableAPIRenderer @@ -489,3 +489,25 @@ class CacheRenderTest(TestCase):          cached_resp = cache.get(self.cache_key)          self.assertIsInstance(cached_resp, Response)          self.assertEqual(cached_resp.content, resp.content) + + +class TestJSONIndentationStyles: +    def test_indented(self): +        renderer = JSONRenderer() +        data = OrderedDict([('a', 1), ('b', 2)]) +        assert renderer.render(data) == b'{"a":1,"b":2}' + +    def test_compact(self): +        renderer = JSONRenderer() +        data = OrderedDict([('a', 1), ('b', 2)]) +        context = {'indent': 4} +        assert ( +            renderer.render(data, renderer_context=context) == +            b'{\n    "a": 1,\n    "b": 2\n}' +        ) + +    def test_long_form(self): +        renderer = JSONRenderer() +        renderer.compact = False +        data = OrderedDict([('a', 1), ('b', 2)]) +        assert renderer.render(data) == b'{"a": 1, "b": 2}' @@ -3,27 +3,27 @@ envlist =         py27-{flake8,docs},         {py26,py27}-django14,         {py26,py27,py32,py33,py34}-django{15,16}, -       {py27,py32,py33,py34}-django17, -       {py27,py32,py33,py34}-djangomaster +       {py27,py32,py33,py34}-django{17,18alpha,master}  [testenv]  commands = ./runtests.py --fast  setenv =         PYTHONDONTWRITEBYTECODE=1  deps = -       django14: Django==1.4.11 -       django15: Django==1.5.5 -       django16: Django==1.6.8 -       django17: Django==1.7.1 +       django14: Django==1.4.11  # Should track minimum supported +       django15: Django==1.5.6  # Should track minimum supported +       django16: Django==1.6.3  # Should track minimum supported +       django17: Django==1.7.2  # Should track maximum supported +       django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/         djangomaster: https://github.com/django/django/zipball/master         django-guardian==1.2.4 -       pytest-django==2.6.1 +       pytest-django==2.8.0         django-filter==0.9.1         markdown>=2.1.0  [testenv:py27-flake8]  deps = -       pytest==2.5.2 +       pytest==2.6.4         flake8==2.2.2  commands = ./runtests.py --lintonly | 
