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() 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' 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. 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/docs/topics/credits.md b/docs/topics/credits.md index 8269580ef..b5dce5040 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -168,6 +168,8 @@ The following people have helped make REST framework great. * Mathieu Pillard - [diox] * Edmond Wong - [edmondwong] * Ben Reilly - [bwreilly] +* Tai Lee - [mrmachine] +* Markus Kaiserswerth - [mkai] Many thanks to everyone who's contributed to the project. @@ -372,3 +374,5 @@ 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 +[mkai]: https://github.com/mkai diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3b35d9ed6..3df8869a2 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 @@ -53,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 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" 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. 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/renderers.py b/rest_framework/renderers.py index fca67eeeb..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, @@ -564,6 +571,49 @@ class BrowsableAPIRenderer(BaseRenderer): def get_breadcrumbs(self, request): return get_breadcrumbs(request.path) + def get_context(self, data, accepted_media_type, renderer_context): + """ + Returns the context used to render. + """ + view = renderer_context['view'] + request = renderer_context['request'] + response = renderer_context['response'] + + renderer = self.get_default_renderer(view) + + raw_data_put_form = self.get_raw_data_form(view, 'PUT', 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 + + context = { + 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), + 'view': view, + 'request': request, + 'response': response, + 'description': self.get_description(view), + 'name': self.get_name(view), + 'version': VERSION, + 'breadcrumblist': self.get_breadcrumbs(request), + 'allowed_methods': view.allowed_methods, + 'available_formats': [renderer.format for renderer in view.renderer_classes], + + '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': 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. @@ -571,60 +621,15 @@ class BrowsableAPIRenderer(BaseRenderer): 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, - 'view': view, - 'request': request, - 'response': response, - 'description': description, - 'name': name, - 'version': VERSION, - 'breadcrumblist': breadcrumb_list, - '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, - - 'raw_data_put_form': raw_data_put_form, - 'raw_data_post_form': raw_data_post_form, - 'raw_data_patch_form': raw_data_patch_form, - 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, - - 'api_settings': api_settings - }) - + 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 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/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index aa90e90c4..2776d5500 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 display_edit_forms %} {% if post_form or raw_data_post_form %}
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 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 = { diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..8d246b010 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 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)