Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Philip Douglas 2013-10-03 11:12:55 +01:00
commit a2ae469f6b
19 changed files with 120 additions and 73 deletions

View File

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

View File

@ -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'

View File

@ -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 youre 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.

View File

@ -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.

View File

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

View File

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

View File

@ -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"

View File

@ -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.

View File

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

View File

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

View File

@ -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]]

View File

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

View File

@ -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]

View File

@ -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 %}>

View File

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

View File

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

View File

@ -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 = {

View File

@ -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):
"""

View File

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