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) |
