aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorPhilip Douglas2013-10-03 11:12:55 +0100
committerPhilip Douglas2013-10-03 11:12:55 +0100
commita2ae469f6ba6ed7e69e9c6a7741876ae14a36057 (patch)
tree399110e75bc1c3b2fbddd314526c3c17f9d68574 /rest_framework
parente5da0ff5e530c8ea0e2cf4dff0723ede6234860e (diff)
parentc3175900bc0681965d07d85c8b9010534e0ff901 (diff)
downloaddjango-rest-framework-a2ae469f6ba6ed7e69e9c6a7741876ae14a36057.tar.bz2
Merge remote-tracking branch 'upstream/master'
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/compat.py8
-rw-r--r--rest_framework/filters.py4
-rw-r--r--rest_framework/permissions.py7
-rw-r--r--rest_framework/renderers.py69
-rw-r--r--rest_framework/serializers.py3
-rw-r--r--rest_framework/templates/rest_framework/base.html2
-rw-r--r--rest_framework/tests/test_filters.py14
-rw-r--r--rest_framework/tests/test_pagination.py2
-rw-r--r--rest_framework/tests/test_permissions.py4
-rw-r--r--rest_framework/tests/test_serializer.py16
-rw-r--r--rest_framework/utils/encoders.py2
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)