diff options
| author | Philip Douglas | 2013-10-03 11:12:55 +0100 | 
|---|---|---|
| committer | Philip Douglas | 2013-10-03 11:12:55 +0100 | 
| commit | a2ae469f6ba6ed7e69e9c6a7741876ae14a36057 (patch) | |
| tree | 399110e75bc1c3b2fbddd314526c3c17f9d68574 /rest_framework | |
| parent | e5da0ff5e530c8ea0e2cf4dff0723ede6234860e (diff) | |
| parent | c3175900bc0681965d07d85c8b9010534e0ff901 (diff) | |
| download | django-rest-framework-a2ae469f6ba6ed7e69e9c6a7741876ae14a36057.tar.bz2 | |
Merge remote-tracking branch 'upstream/master'
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/compat.py | 8 | ||||
| -rw-r--r-- | rest_framework/filters.py | 4 | ||||
| -rw-r--r-- | rest_framework/permissions.py | 7 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 69 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 3 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 2 | ||||
| -rw-r--r-- | rest_framework/tests/test_filters.py | 14 | ||||
| -rw-r--r-- | rest_framework/tests/test_pagination.py | 2 | ||||
| -rw-r--r-- | rest_framework/tests/test_permissions.py | 4 | ||||
| -rw-r--r-- | rest_framework/tests/test_serializer.py | 16 | ||||
| -rw-r--r-- | rest_framework/utils/encoders.py | 2 | 
11 files changed, 83 insertions, 48 deletions
diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b9d1dae6..581e29fc 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -80,6 +80,14 @@ except ImportError:          Image = None +def get_model_name(model_cls): +    try: +        return model_cls._meta.model_name +    except AttributeError: +        # < 1.6 used module_name instead of model_name +        return model_cls._meta.module_name + +  def get_concrete_model(model_cls):      try:          return model_cls._meta.concrete_model diff --git a/rest_framework/filters.py b/rest_framework/filters.py index b8fe7f77..e287a168 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -4,7 +4,7 @@ returned by list views.  """  from __future__ import unicode_literals  from django.db import models -from rest_framework.compat import django_filters, six, guardian +from rest_framework.compat import django_filters, six, guardian, get_model_name  from functools import reduce  import operator @@ -158,7 +158,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):          model_cls = queryset.model          kwargs = {              'app_label': model_cls._meta.app_label, -            'model_name': model_cls._meta.module_name +            'model_name': get_model_name(model_cls)          }          permission = self.perm_format % kwargs          return guardian.shortcuts.get_objects_for_user(user, permission, queryset) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 53184798..ab6655e7 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,7 +8,8 @@ import warnings  SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']  from django.http import Http404 -from rest_framework.compat import oauth2_provider_scope, oauth2_constants +from rest_framework.compat import (get_model_name, oauth2_provider_scope, +                                   oauth2_constants)  class BasePermission(object): @@ -116,7 +117,7 @@ class DjangoModelPermissions(BasePermission):          """          kwargs = {              'app_label': model_cls._meta.app_label, -            'model_name': model_cls._meta.module_name +            'model_name': get_model_name(model_cls)          }          return [perm % kwargs for perm in self.perms_map[method]] @@ -177,7 +178,7 @@ class DjangoObjectPermissions(DjangoModelPermissions):      def get_required_object_permissions(self, method, model_cls):          kwargs = {              'app_label': model_cls._meta.app_label, -            'model_name': model_cls._meta.module_name +            'model_name': get_model_name(model_cls)          }          return [perm % kwargs for perm in self.perms_map[method]] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fca67eee..a27160d4 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -272,7 +272,9 @@ class TemplateHTMLRenderer(BaseRenderer):              return [self.template_name]          elif hasattr(view, 'get_template_names'):              return view.get_template_names() -        raise ImproperlyConfigured('Returned a template response with no template_name') +        elif hasattr(view, 'template_name'): +            return [view.template_name] +        raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response')      def get_exception_template(self, response):          template_names = [name % {'status_code': response.status_code} @@ -388,7 +390,7 @@ class HTMLFormRenderer(BaseRenderer):          # likely change at some point.          self.renderer_context = renderer_context or {} -        request = renderer_context['request'] +        request = self.renderer_context['request']          # Creating an on the fly form see:          # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python @@ -419,8 +421,13 @@ class BrowsableAPIRenderer(BaseRenderer):          """          renderers = [renderer for renderer in view.renderer_classes                       if not issubclass(renderer, BrowsableAPIRenderer)] +        non_template_renderers = [renderer for renderer in renderers +                                  if not hasattr(renderer, 'get_template_names')] +          if not renderers:              return None +        elif non_template_renderers: +            return non_template_renderers[0]()          return renderers[0]()      def get_content(self, renderer, data, @@ -564,67 +571,65 @@ class BrowsableAPIRenderer(BaseRenderer):      def get_breadcrumbs(self, request):          return get_breadcrumbs(request.path) -    def render(self, data, accepted_media_type=None, renderer_context=None): +    def get_context(self, data, accepted_media_type, renderer_context):          """ -        Render the HTML for the browsable API representation. +        Returns the context used to render.          """ -        self.accepted_media_type = accepted_media_type or '' -        self.renderer_context = renderer_context or {} -          view = renderer_context['view']          request = renderer_context['request']          response = renderer_context['response']          renderer = self.get_default_renderer(view) -        content = self.get_content(renderer, data, accepted_media_type, renderer_context) - -        put_form = self.get_rendered_html_form(view, 'PUT', request) -        post_form = self.get_rendered_html_form(view, 'POST', request) -        patch_form = self.get_rendered_html_form(view, 'PATCH', request) -        delete_form = self.get_rendered_html_form(view, 'DELETE', request) -        options_form = self.get_rendered_html_form(view, 'OPTIONS', request)          raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) -        raw_data_post_form = self.get_raw_data_form(view, 'POST', request)          raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)          raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form -        name = self.get_name(view) -        description = self.get_description(view) -        breadcrumb_list = self.get_breadcrumbs(request) - -        template = loader.get_template(self.template) -        context = RequestContext(request, { -            'content': content, +        context = { +            'content': self.get_content(renderer, data, accepted_media_type, renderer_context),              'view': view,              'request': request,              'response': response, -            'description': description, -            'name': name, +            'description': self.get_description(view), +            'name': self.get_name(view),              'version': VERSION, -            'breadcrumblist': breadcrumb_list, +            'breadcrumblist': self.get_breadcrumbs(request),              'allowed_methods': view.allowed_methods,              'available_formats': [renderer.format for renderer in view.renderer_classes], -            'put_form': put_form, -            'post_form': post_form, -            'patch_form': patch_form, -            'delete_form': delete_form, -            'options_form': options_form, +            'put_form': self.get_rendered_html_form(view, 'PUT', request), +            'post_form': self.get_rendered_html_form(view, 'POST', request), +            'patch_form': self.get_rendered_html_form(view, 'PATCH', request), +            'delete_form': self.get_rendered_html_form(view, 'DELETE', request), +            'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),              'raw_data_put_form': raw_data_put_form, -            'raw_data_post_form': raw_data_post_form, +            'raw_data_post_form': self.get_raw_data_form(view, 'POST', request),              'raw_data_patch_form': raw_data_patch_form,              'raw_data_put_or_patch_form': raw_data_put_or_patch_form, +            'display_edit_forms': bool(response.status_code != 403), +              'api_settings': api_settings -        }) +        } +        return context + +    def render(self, data, accepted_media_type=None, renderer_context=None): +        """ +        Render the HTML for the browsable API representation. +        """ +        self.accepted_media_type = accepted_media_type or '' +        self.renderer_context = renderer_context or {} +        template = loader.get_template(self.template) +        context = self.get_context(data, accepted_media_type, renderer_context) +        context = RequestContext(renderer_context['request'], context)          ret = template.render(context)          # Munge DELETE Response code to allow us to return content          # (Do this *after* we've rendered the template so that we include          # the normal deletion response code in the output) +        response = renderer_context['response']          if response.status_code == status.HTTP_204_NO_CONTENT:              response.status_code = status.HTTP_200_OK diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c..8d2e0feb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -518,6 +518,9 @@ class BaseSerializer(WritableField):          """          Save the deserialized object and return it.          """ +        # Clear cached _data, which may be invalidated by `save()` +        self._data = None +          if isinstance(self.object, list):              [self.save_object(item, **kwargs) for item in self.object] diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index aa90e90c..2776d550 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -122,7 +122,7 @@              </div>          </div> -            {% if response.status_code != 403 %} +            {% if display_edit_forms %}                  {% if post_form or raw_data_post_form %}                  <div {% if post_form %}class="tabbable"{% endif %}> diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index c9d9e7ff..379db29d 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -113,7 +113,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):      Integration tests for filtered list views.      """ -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_get_filtered_fields_root_view(self):          """          GET requests to paginated ListCreateAPIView should return paginated results. @@ -142,7 +142,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):          expected_data = [f for f in self.data if f['date'] == search_date]          self.assertEqual(response.data, expected_data) -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_filter_with_queryset(self):          """          Regression test for #814. @@ -157,7 +157,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):          expected_data = [f for f in self.data if f['decimal'] == search_decimal]          self.assertEqual(response.data, expected_data) -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_filter_with_get_queryset_only(self):          """          Regression test for #834. @@ -168,7 +168,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):          # Used to raise "issubclass() arg 2 must be a class or tuple of classes"          # here when neither `model' nor `queryset' was specified. -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_get_filtered_class_root_view(self):          """          GET requests to filtered ListCreateAPIView that have a filter_class set @@ -216,7 +216,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):                           f['decimal'] < search_decimal]          self.assertEqual(response.data, expected_data) -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_incorrectly_configured_filter(self):          """          An error should be displayed when the filter class is misconfigured. @@ -226,7 +226,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):          request = factory.get('/')          self.assertRaises(AssertionError, view, request) -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_unknown_filter(self):          """          GET requests with filters that aren't configured should return 200. @@ -248,7 +248,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):      def _get_url(self, item):          return reverse('detail-view', kwargs=dict(pk=item.pk)) -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_get_filtered_detail_view(self):          """          GET requests to filtered RetrieveAPIView that have a filter_class set diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index 4170d4b6..d6bc7895 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -122,7 +122,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):              for obj in self.objects.all()          ] -    @unittest.skipUnless(django_filters, 'django-filters not installed') +    @unittest.skipUnless(django_filters, 'django-filter not installed')      def test_get_django_filter_paginated_filtered_root_view(self):          """          GET requests to paginated filtered ListCreateAPIView should return diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index d08124f4..6e3a6303 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -4,7 +4,7 @@ from django.db import models  from django.test import TestCase  from django.utils import unittest  from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING -from rest_framework.compat import guardian +from rest_framework.compat import guardian, get_model_name  from rest_framework.filters import DjangoObjectPermissionsFilter  from rest_framework.test import APIRequestFactory  from rest_framework.tests.models import BasicModel @@ -202,7 +202,7 @@ class ObjectPermissionsIntegrationTests(TestCase):          # give everyone model level permissions, as we are not testing those          everyone = Group.objects.create(name='everyone') -        model_name = BasicPermModel._meta.module_name +        model_name = get_model_name(BasicPermModel)          app_label = BasicPermModel._meta.app_label          f = '{0}_{1}'.format          perms = { diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c2497660..8d246b01 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*-  from __future__ import unicode_literals  from django.db import models  from django.db.models.fields import BLANK_CHOICE_DASH @@ -136,6 +137,7 @@ class BasicTests(TestCase):              'Happy new year!',              datetime.datetime(2012, 1, 1)          ) +        self.actionitem = ActionItem(title='Some to do item',)          self.data = {              'email': 'tom@example.com',              'content': 'Happy new year!', @@ -264,6 +266,20 @@ class BasicTests(TestCase):          """          self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, []) +    def test_serializer_data_is_cleared_on_save(self): +        """ +        Check _data attribute is cleared on `save()` + +        Regression test for #1116 +            — id field is not populated if `data` is accessed prior to `save()` +        """ +        serializer = ActionItemSerializer(self.actionitem) +        self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.') +        serializer.save() +        self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.') + + +  class DictStyleSerializer(serializers.Serializer):      """ diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index b26a2085..7efd5417 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -42,6 +42,8 @@ class JSONEncoder(json.JSONEncoder):              return str(o.total_seconds())          elif isinstance(o, decimal.Decimal):              return str(o) +        elif hasattr(o, 'tolist'): +            return o.tolist()          elif hasattr(o, '__iter__'):              return [i for i in o]          return super(JSONEncoder, self).default(o)  | 
