mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 10:03:57 +03:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
a2ae469f6b
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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 %}>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user