mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 00:04:16 +03:00
Merge remote-tracking branch 'reference/master' into feature/django_1_7
This commit is contained in:
commit
3d7cb72e0a
|
@ -8,7 +8,7 @@ python:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/"
|
- DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/"
|
||||||
- DJANGO="django==1.6"
|
- DJANGO="django==1.6.2"
|
||||||
- DJANGO="django==1.5.5"
|
- DJANGO="django==1.5.5"
|
||||||
- DJANGO="django==1.4.10"
|
- DJANGO="django==1.4.10"
|
||||||
- DJANGO="django==1.3.7"
|
- DJANGO="django==1.3.7"
|
||||||
|
|
|
@ -393,6 +393,14 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
|
||||||
|
|
||||||
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password.
|
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password.
|
||||||
|
|
||||||
|
## Hawk HTTP Authentication
|
||||||
|
|
||||||
|
The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let you work with [Hawk][hawk] signed requests and responses in your API. [Hawk][hawk] lets two parties securely communicate with each other using messages signed by a shared key. It is based on [HTTP MAC access authentication][mac] (which was based on parts of [OAuth 1.0][oauth-1.0a]).
|
||||||
|
|
||||||
|
## HTTP Signature Authentication
|
||||||
|
|
||||||
|
HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism.
|
||||||
|
|
||||||
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
||||||
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||||
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
||||||
|
@ -419,3 +427,11 @@ JSON Web Token is a fairly new standard which can be used for token-based authen
|
||||||
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
|
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
|
||||||
[blimp]: https://github.com/GetBlimp
|
[blimp]: https://github.com/GetBlimp
|
||||||
[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
|
[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
|
||||||
|
[etoccalino]: https://github.com/etoccalino/
|
||||||
|
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
|
||||||
|
[amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
||||||
|
[http-signature-ietf-draft]: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
|
||||||
|
[hawkrest]: http://hawkrest.readthedocs.org/en/latest/
|
||||||
|
[hawk]: https://github.com/hueniverse/hawk
|
||||||
|
[mohawk]: http://mohawk.readthedocs.org/en/latest/
|
||||||
|
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
|
||||||
|
|
|
@ -147,4 +147,14 @@ Alternatively, to set your custom pagination serializer on a per-view basis, use
|
||||||
pagination_serializer_class = CustomPaginationSerializer
|
pagination_serializer_class = CustomPaginationSerializer
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
|
|
||||||
|
# Third party packages
|
||||||
|
|
||||||
|
The following third party packages are also available.
|
||||||
|
|
||||||
|
## DRF-extensions
|
||||||
|
|
||||||
|
The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size.
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
||||||
|
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
|
||||||
|
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
|
|
@ -161,7 +161,7 @@ To do any other validation that requires access to multiple fields, add a method
|
||||||
"""
|
"""
|
||||||
Check that the start is before the stop.
|
Check that the start is before the stop.
|
||||||
"""
|
"""
|
||||||
if attrs['start'] < attrs['finish']:
|
if attrs['start'] > attrs['finish']:
|
||||||
raise serializers.ValidationError("finish must occur after start")
|
raise serializers.ValidationError("finish must occur after start")
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ To run the tests, clone the repository, and then:
|
||||||
# Run the tests
|
# Run the tests
|
||||||
rest_framework/runtests/runtests.py
|
rest_framework/runtests/runtests.py
|
||||||
|
|
||||||
You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
||||||
|
|
||||||
tox
|
tox
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai
|
||||||
|
|
||||||
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
|
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
|
||||||
|
|
||||||
We can add a login view for use with the browsable API, by editing our URLconf once more.
|
We can add a login view for use with the browsable API, by editing the URLconf in our project-level urls.py file.
|
||||||
|
|
||||||
Add the following import at the top of the file:
|
Add the following import at the top of the file:
|
||||||
|
|
||||||
|
|
|
@ -116,30 +116,27 @@ class UpdateModelMixin(object):
|
||||||
partial = kwargs.pop('partial', False)
|
partial = kwargs.pop('partial', False)
|
||||||
self.object = self.get_object_or_none()
|
self.object = self.get_object_or_none()
|
||||||
|
|
||||||
if self.object is None:
|
|
||||||
created = True
|
|
||||||
save_kwargs = {'force_insert': True}
|
|
||||||
success_status_code = status.HTTP_201_CREATED
|
|
||||||
else:
|
|
||||||
created = False
|
|
||||||
save_kwargs = {'force_update': True}
|
|
||||||
success_status_code = status.HTTP_200_OK
|
|
||||||
|
|
||||||
serializer = self.get_serializer(self.object, data=request.DATA,
|
serializer = self.get_serializer(self.object, data=request.DATA,
|
||||||
files=request.FILES, partial=partial)
|
files=request.FILES, partial=partial)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(serializer.object)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
# full_clean on model instance may be called in pre_save, so we
|
# full_clean on model instance may be called in pre_save,
|
||||||
# have to handle eventual errors.
|
# so we have to handle eventual errors.
|
||||||
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
|
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
|
||||||
self.object = serializer.save(**save_kwargs)
|
|
||||||
self.post_save(self.object, created=created)
|
|
||||||
return Response(serializer.data, status=success_status_code)
|
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
if self.object is None:
|
||||||
|
self.object = serializer.save(force_insert=True)
|
||||||
|
self.post_save(self.object, created=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
self.object = serializer.save(force_update=True)
|
||||||
|
self.post_save(self.object, created=False)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
kwargs['partial'] = True
|
kwargs['partial'] = True
|
||||||
|
|
|
@ -33,6 +33,7 @@ class RelatedField(WritableField):
|
||||||
many_widget = widgets.SelectMultiple
|
many_widget = widgets.SelectMultiple
|
||||||
form_field_class = forms.ChoiceField
|
form_field_class = forms.ChoiceField
|
||||||
many_form_field_class = forms.MultipleChoiceField
|
many_form_field_class = forms.MultipleChoiceField
|
||||||
|
null_values = (None, '', 'None')
|
||||||
|
|
||||||
cache_choices = False
|
cache_choices = False
|
||||||
empty_label = None
|
empty_label = None
|
||||||
|
@ -168,9 +169,9 @@ class RelatedField(WritableField):
|
||||||
return
|
return
|
||||||
value = [] if self.many else None
|
value = [] if self.many else None
|
||||||
|
|
||||||
if value in (None, '') and self.required:
|
if value in self.null_values:
|
||||||
|
if self.required:
|
||||||
raise ValidationError(self.error_messages['required'])
|
raise ValidationError(self.error_messages['required'])
|
||||||
elif value in (None, ''):
|
|
||||||
into[(self.source or field_name)] = None
|
into[(self.source or field_name)] = None
|
||||||
elif self.many:
|
elif self.many:
|
||||||
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
||||||
|
|
|
@ -544,6 +544,14 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', 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
|
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||||
|
|
||||||
|
response_headers = dict(response.items())
|
||||||
|
renderer_content_type = ''
|
||||||
|
if renderer:
|
||||||
|
renderer_content_type = '%s' % renderer.media_type
|
||||||
|
if renderer.charset:
|
||||||
|
renderer_content_type += ' ;%s' % renderer.charset
|
||||||
|
response_headers['Content-Type'] = renderer_content_type
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||||
'view': view,
|
'view': view,
|
||||||
|
@ -555,6 +563,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
'breadcrumblist': self.get_breadcrumbs(request),
|
'breadcrumblist': self.get_breadcrumbs(request),
|
||||||
'allowed_methods': view.allowed_methods,
|
'allowed_methods': view.allowed_methods,
|
||||||
'available_formats': [renderer.format for renderer in view.renderer_classes],
|
'available_formats': [renderer.format for renderer in view.renderer_classes],
|
||||||
|
'response_headers': response_headers,
|
||||||
|
|
||||||
'put_form': self.get_rendered_html_form(view, 'PUT', request),
|
'put_form': self.get_rendered_html_form(view, 'PUT', request),
|
||||||
'post_form': self.get_rendered_html_form(view, 'POST', request),
|
'post_form': self.get_rendered_html_form(view, 'POST', request),
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="response-info">
|
<div class="response-info">
|
||||||
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
||||||
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
|
|
8
rest_framework/tests/serializers.py
Normal file
8
rest_framework/tests/serializers.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from rest_framework.tests.models import NullableForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
|
class NullableFKSourceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = NullableForeignKeySource
|
30
rest_framework/tests/test_nullable_fields.py
Normal file
30
rest_framework/tests/test_nullable_fields.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from rest_framework.compat import patterns, url
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from rest_framework.tests.models import NullableForeignKeySource
|
||||||
|
from rest_framework.tests.serializers import NullableFKSourceSerializer
|
||||||
|
from rest_framework.tests.views import NullableFKSourceDetail
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns(
|
||||||
|
'',
|
||||||
|
url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NullableForeignKeyTests(APITestCase):
|
||||||
|
"""
|
||||||
|
DRF should be able to handle nullable foreign keys when a test
|
||||||
|
Client POST/PUT request is made with its own serialized object.
|
||||||
|
"""
|
||||||
|
urls = 'rest_framework.tests.test_nullable_fields'
|
||||||
|
|
||||||
|
def test_updating_object_with_null_fk(self):
|
||||||
|
obj = NullableForeignKeySource(name='example', target=None)
|
||||||
|
obj.save()
|
||||||
|
serialized_data = NullableFKSourceSerializer(obj).data
|
||||||
|
|
||||||
|
response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
|
||||||
|
|
||||||
|
self.assertEqual(response.data, serialized_data)
|
|
@ -256,6 +256,18 @@ class RendererEndToEndTests(TestCase):
|
||||||
self.assertEqual(resp.get('Content-Type', None), None)
|
self.assertEqual(resp.get('Content-Type', None), None)
|
||||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
def test_contains_headers_of_api_response(self):
|
||||||
|
"""
|
||||||
|
Issue #1437
|
||||||
|
|
||||||
|
Test we display the headers of the API response and not those from the
|
||||||
|
HTML response
|
||||||
|
"""
|
||||||
|
resp = self.client.get('/html1')
|
||||||
|
self.assertContains(resp, '>GET, HEAD, OPTIONS<')
|
||||||
|
self.assertContains(resp, '>application/json<')
|
||||||
|
self.assertNotContains(resp, '>text/html; charset=utf-8<')
|
||||||
|
|
||||||
|
|
||||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||||
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
||||||
|
|
8
rest_framework/tests/views.py
Normal file
8
rest_framework/tests/views.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from rest_framework import generics
|
||||||
|
from rest_framework.tests.models import NullableForeignKeySource
|
||||||
|
from rest_framework.tests.serializers import NullableFKSourceSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
model = NullableForeignKeySource
|
||||||
|
model_serializer_class = NullableFKSourceSerializer
|
|
@ -136,6 +136,8 @@ class SimpleRateThrottle(BaseThrottle):
|
||||||
remaining_duration = self.duration
|
remaining_duration = self.duration
|
||||||
|
|
||||||
available_requests = self.num_requests - len(self.history) + 1
|
available_requests = self.num_requests - len(self.history) + 1
|
||||||
|
if available_requests <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
return remaining_duration / float(available_requests)
|
return remaining_duration / float(available_requests)
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ class APIView(View):
|
||||||
"""
|
"""
|
||||||
If request is not permitted, determine what kind of exception to raise.
|
If request is not permitted, determine what kind of exception to raise.
|
||||||
"""
|
"""
|
||||||
if not self.request.successful_authenticator:
|
if not request.successful_authenticator:
|
||||||
raise exceptions.NotAuthenticated()
|
raise exceptions.NotAuthenticated()
|
||||||
raise exceptions.PermissionDenied()
|
raise exceptions.PermissionDenied()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user