From d489c5c88144a25ef0d61fb8deb0b77f3a061480 Mon Sep 17 00:00:00 2001 From: David Pretty Date: Fri, 13 Sep 2013 13:36:18 +1000 Subject: [PATCH 01/17] Let JSONEncoder handle Numpy data types. json.JSONEncoder cannot serialize Numpy data types. Numpy arrays and array scalars have a tolist() method which casts the object to a standard python data type. --- rest_framework/utils/encoders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index b26a2085a..7efd5417b 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) From 0de1a1a0ad0ff96292b14d707730dc47b1943c26 Mon Sep 17 00:00:00 2001 From: Rajiv Bose Date: Fri, 13 Sep 2013 11:55:16 +0100 Subject: [PATCH 02/17] Typo in strings referring to Python package, django-filter. On skip of django_filters related unit-tests the reason given states the Python package 'django-filters' is not install. However, the Python package required to run django_filters related tests is 'django-filter'. --- rest_framework/tests/test_filters.py | 14 +++++++------- rest_framework/tests/test_pagination.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index c9d9e7ffa..379db29d8 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 4170d4b64..d6bc7895c 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 From bb3261ca489104e3dea434aa11d76f370e938ca8 Mon Sep 17 00:00:00 2001 From: Tai Lee Date: Fri, 13 Sep 2013 22:51:11 +1000 Subject: [PATCH 03/17] Fixed #1105 -- Add hook for custom context in `BrowsableAPIRenderer`. Replace hard coded response status check with `allow_form` context variable, so that it can be overridden in a custom renderer class. --- rest_framework/renderers.py | 47 +++++++++---------- .../templates/rest_framework/base.html | 2 +- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fca67eeeb..6597123f8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -564,9 +564,9 @@ 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 {} @@ -576,55 +576,52 @@ class BrowsableAPIRenderer(BaseRenderer): 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, + '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, + 'allow_form': 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. + """ + template = loader.get_template(self.template) + context = self.get_context(data, accepted_media_type, renderer_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/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index aa90e90c4..88e58deb0 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -122,7 +122,7 @@ - {% if response.status_code != 403 %} + {% if allow_form %} {% if post_form or raw_data_post_form %}
From a9dbd46c9470003a1dd41e66a113d50b0217a110 Mon Sep 17 00:00:00 2001 From: Tai Lee Date: Sat, 14 Sep 2013 00:54:44 +1000 Subject: [PATCH 04/17] Refs #1109 -- Update docs. Integrate changes from feedback. --- docs/topics/browsable-api.md | 3 +++ rest_framework/renderers.py | 13 +++++++------ rest_framework/templates/rest_framework/base.html | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index b2c78f3c6..e32db6958 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -115,6 +115,7 @@ The context that's available to the template: * `name` : The name of the resource * `post_form` : A form instance for use by the POST form (if allowed) * `put_form` : A form instance for use by the PUT form (if allowed) +* `display_edit_forms` : A boolean indicating whether or not POST, PUT and PATCH forms will be displayed * `request` : The request object * `response` : The response object * `version` : The version of Django REST Framework @@ -122,6 +123,8 @@ The context that's available to the template: * `FORMAT_PARAM` : The view can accept a format override * `METHOD_PARAM` : The view can accept a method override +You can override the `BrowsableAPIRenderer.get_context()` method to customise the context that gets passed to the template. + #### Not using base.html For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 6597123f8..2ce51e97b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -568,9 +568,6 @@ class BrowsableAPIRenderer(BaseRenderer): """ 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'] @@ -581,7 +578,7 @@ class BrowsableAPIRenderer(BaseRenderer): 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 - context = RequestContext(request, { + context = { 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'view': view, 'request': request, @@ -604,18 +601,22 @@ class BrowsableAPIRenderer(BaseRenderer): 'raw_data_patch_form': raw_data_patch_form, 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, - 'allow_form': bool(response.status_code != 403), + '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 diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 88e58deb0..2776d5500 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -122,7 +122,7 @@
- {% if allow_form %} + {% if display_edit_forms %} {% if post_form or raw_data_post_form %}
From d75ecb3d69d01849685864341c89d59e6a3121cd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Sep 2013 19:40:58 +0100 Subject: [PATCH 05/17] Added @mrmachine. Thanks! For work on #1109. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 8269580ef..4483f1707 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -168,6 +168,7 @@ The following people have helped make REST framework great. * Mathieu Pillard - [diox] * Edmond Wong - [edmondwong] * Ben Reilly - [bwreilly] +* Tai Lee - [mrmachine] Many thanks to everyone who's contributed to the project. @@ -372,3 +373,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [diox]: https://github.com/diox [edmondwong]: https://github.com/edmondwong [bwreilly]: https://github.com/bwreilly +[mrmachine]: https://github.com/mrmachine From e8c6cd5622f62fcf2d4cf2b28b504fe5ff5228f9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Sep 2013 19:43:15 +0100 Subject: [PATCH 06/17] Update release notes. --- docs/topics/release-notes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3b35d9ed6..e4294ae33 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* Added JSON renderer support for numpy scalars. +* Added `get_context` hook in `BrowsableAPIRenderer`. + ### 2.3.8 **Date**: 11th September 2013 From 3d8fad04446110db93ed2a13866e91beb9604934 Mon Sep 17 00:00:00 2001 From: Braulio Soncco Date: Wed, 18 Sep 2013 00:33:05 -0500 Subject: [PATCH 07/17] Fixing simple typo --- docs/tutorial/2-requests-and-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 30966a106..6ff97f37d 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -147,7 +147,7 @@ Similarly, we can control the format of the request that we send, using the `Con # POST using form data curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123" - {"id": 3, "title": "", "code": "123", "linenos": false, "language": "python", "style": "friendly"} + {"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"} # POST using JSON curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json" From f07a4f4ca3812fbe45d698e4ba0f9ff9099b6887 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 20 Sep 2013 14:10:16 +0200 Subject: [PATCH 08/17] Clear cached serializer data on `save()` + test. Fixes #1116. --- rest_framework/serializers.py | 3 +++ rest_framework/tests/test_serializer.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c2..8d2e0feb8 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/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..43d244119 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 is `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): """ From b82c44af48f25c0a60880b8d702caf6a74d80baa Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 20 Sep 2013 14:20:21 +0200 Subject: [PATCH 09/17] Correct typo in doc string. --- rest_framework/tests/test_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 43d244119..8d246b010 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -271,7 +271,7 @@ class BasicTests(TestCase): Check _data attribute is cleared on `save()` Regression test for #1116 - — id field is not populated is `data` is accessed prior to `save()` + — 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.') From de6e7accef0f144f4bcae9618604d462b1d9d321 Mon Sep 17 00:00:00 2001 From: John Mee Date: Mon, 23 Sep 2013 14:03:09 +1000 Subject: [PATCH 10/17] Mindnumbingly trivial single-char typo. --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 06eec3c4e..80bb9abb4 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -85,7 +85,7 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty queryset = Group.objects.all() serializer_class = GroupSerializer -Rather that write multiple views we're grouping together all the common behavior into classes called `ViewSets`. +Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`. We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise. From fda0c520bd3fb81c7eec06da70bb7abfed519d93 Mon Sep 17 00:00:00 2001 From: Grzegorz Kapkowski Date: Mon, 23 Sep 2013 16:10:46 +0200 Subject: [PATCH 11/17] Match docs to current code. --- docs/api-guide/filtering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 859e8d52a..784aa585e 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -195,9 +195,9 @@ For more details on using filter sets see the [django-filter documentation][djan ## SearchFilter -The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. +The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. -The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. +The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. class UserListView(generics.ListAPIView): queryset = User.objects.all() From abbe9213f98b5e1d3b53db2c1711d9221c5b257f Mon Sep 17 00:00:00 2001 From: Markus Kaiserswerth Date: Mon, 23 Sep 2013 17:48:25 +0200 Subject: [PATCH 12/17] Address pending deprecation of Model._meta.module_name in Django 1.6 --- rest_framework/compat.py | 8 ++++++++ rest_framework/filters.py | 4 ++-- rest_framework/permissions.py | 7 ++++--- rest_framework/tests/test_permissions.py | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b9d1dae6b..581e29fc7 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 b8fe7f77e..e287a1683 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 531847988..ab6655e7b 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/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index d08124f48..6e3a63034 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 = { From 57a51f790bdff6c1cf34f3c6d64b0bef3033d89e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Sep 2013 09:21:55 +0100 Subject: [PATCH 13/17] Added @mkai, for work on #1126. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 4483f1707..b5dce5040 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -169,6 +169,7 @@ The following people have helped make REST framework great. * Edmond Wong - [edmondwong] * Ben Reilly - [bwreilly] * Tai Lee - [mrmachine] +* Markus Kaiserswerth - [mkai] Many thanks to everyone who's contributed to the project. @@ -374,3 +375,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [edmondwong]: https://github.com/edmondwong [bwreilly]: https://github.com/bwreilly [mrmachine]: https://github.com/mrmachine +[mkai]: https://github.com/mkai From 75d6446c8799763dccde0f5f03fbcae39c18dc7f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Sep 2013 16:09:08 +0100 Subject: [PATCH 14/17] Allow .template_name attribute specified on view. Closes #1000 --- rest_framework/renderers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2ce51e97b..a27160d46 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, From 49847584a8524883181e4118a9df9bb62f597271 Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Sun, 29 Sep 2013 12:49:38 +0200 Subject: [PATCH 15/17] Minor documentation fix. --- docs/topics/ajax-csrf-cors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md index 0555b84dd..97dd4710b 100644 --- a/docs/topics/ajax-csrf-cors.md +++ b/docs/topics/ajax-csrf-cors.md @@ -6,7 +6,7 @@ ## Javascript clients -If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. +If you’re building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website. From 8a1d3275795a6eea931cb0b67465c88d745bd2b6 Mon Sep 17 00:00:00 2001 From: Doron Pearl Date: Mon, 30 Sep 2013 14:08:46 -0400 Subject: [PATCH 16/17] corrected doc for throttle_classes decorator the decorator actually expects an array and otherwise raise an exception. --- docs/api-guide/throttling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index cc4692171..fc1525df6 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -59,7 +59,7 @@ using the `APIView` class based views. Or, if you're using the `@api_view` decorator with function based views. @api_view('GET') - @throttle_classes(UserRateThrottle) + @throttle_classes([UserRateThrottle]) def example_view(request, format=None): content = { 'status': 'request was permitted' From 61dd9ce914382ad7b26e2654a590215885308828 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 1 Oct 2013 15:47:47 -0600 Subject: [PATCH 17/17] Update release-notes.md Simple typo fix. --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e4294ae33..3df8869a2 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -58,7 +58,7 @@ You can determine your currently installed version using `pip freeze`: * 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. -* Bugfix: Client sending emptry string instead of file now clears `FileField`. +* Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. ### 2.3.7