mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-05 04:50:12 +03:00
Merge branch 'master' into docs
This commit is contained in:
commit
f3841cfb16
|
@ -95,7 +95,7 @@ The following packages are optional:
|
||||||
|
|
||||||
* [coreapi][coreapi] (1.32.0+) - Schema generation support.
|
* [coreapi][coreapi] (1.32.0+) - Schema generation support.
|
||||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
|
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
|
||||||
* [django-filter][django-filter] (0.9.2+) - Filtering support.
|
* [django-filter][django-filter] (1.0.1+) - Filtering support.
|
||||||
* [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
|
* [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
|
||||||
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
||||||
|
|
||||||
|
|
|
@ -207,4 +207,4 @@ To implement a hypermedia API you'll need to decide on an appropriate media type
|
||||||
[image-django-rest-swagger]: ../img/django-rest-swagger.png
|
[image-django-rest-swagger]: ../img/django-rest-swagger.png
|
||||||
[image-apiary]: ../img/apiary.png
|
[image-apiary]: ../img/apiary.png
|
||||||
[image-self-describing-api]: ../img/self-describing.png
|
[image-self-describing-api]: ../img/self-describing.png
|
||||||
[schemas-examples]: api-guide/schemas/#examples
|
[schemas-examples]: ../api-guide/schemas/#examples
|
||||||
|
|
|
@ -64,7 +64,7 @@ There are a wide range of resources available for learning and using Django REST
|
||||||
* [Classy Django REST Framework][cdrf.co]
|
* [Classy Django REST Framework][cdrf.co]
|
||||||
* [DRF-schema-adapter][drf-schema]
|
* [DRF-schema-adapter][drf-schema]
|
||||||
|
|
||||||
Want your Django REST Framework talk/tutorial/article to be added to our website? Or know of a resource that's not yet included here? Please [submit a pull request][submit-pr] or [email us][mailto:anna@django-rest-framework.org]!
|
Want your Django REST Framework talk/tutorial/article to be added to our website? Or know of a resource that's not yet included here? Please [submit a pull request][submit-pr] or [email us][anna-email]!
|
||||||
|
|
||||||
|
|
||||||
[beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786
|
[beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786
|
||||||
|
@ -107,3 +107,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
||||||
[drf-tutorial]: https://tests4geeks.com/django-rest-framework-tutorial/
|
[drf-tutorial]: https://tests4geeks.com/django-rest-framework-tutorial/
|
||||||
[building-a-restful-api-with-drf]: http://agiliq.com/blog/2014/12/building-a-restful-api-with-django-rest-framework/
|
[building-a-restful-api-with-drf]: http://agiliq.com/blog/2014/12/building-a-restful-api-with-django-rest-framework/
|
||||||
[submit-pr]: https://github.com/tomchristie/django-rest-framework
|
[submit-pr]: https://github.com/tomchristie/django-rest-framework
|
||||||
|
[anna-email]: mailto:anna@django-rest-framework.org
|
||||||
|
|
|
@ -216,26 +216,15 @@ It's important to remember that `ModelSerializer` classes don't do anything part
|
||||||
Let's see how we can write some API views using our new Serializer class.
|
Let's see how we can write some API views using our new Serializer class.
|
||||||
For the moment we won't use any of REST framework's other features, we'll just write the views as regular Django views.
|
For the moment we won't use any of REST framework's other features, we'll just write the views as regular Django views.
|
||||||
|
|
||||||
We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`.
|
|
||||||
|
|
||||||
Edit the `snippets/views.py` file, and add the following.
|
Edit the `snippets/views.py` file, and add the following.
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from snippets.models import Snippet
|
from snippets.models import Snippet
|
||||||
from snippets.serializers import SnippetSerializer
|
from snippets.serializers import SnippetSerializer
|
||||||
|
|
||||||
class JSONResponse(HttpResponse):
|
|
||||||
"""
|
|
||||||
An HttpResponse that renders its content into JSON.
|
|
||||||
"""
|
|
||||||
def __init__(self, data, **kwargs):
|
|
||||||
content = JSONRenderer().render(data)
|
|
||||||
kwargs['content_type'] = 'application/json'
|
|
||||||
super(JSONResponse, self).__init__(content, **kwargs)
|
|
||||||
|
|
||||||
The root of our API is going to be a view that supports listing all the existing snippets, or creating a new snippet.
|
The root of our API is going to be a view that supports listing all the existing snippets, or creating a new snippet.
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -246,15 +235,15 @@ The root of our API is going to be a view that supports listing all the existing
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
snippets = Snippet.objects.all()
|
snippets = Snippet.objects.all()
|
||||||
serializer = SnippetSerializer(snippets, many=True)
|
serializer = SnippetSerializer(snippets, many=True)
|
||||||
return JSONResponse(serializer.data)
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
data = JSONParser().parse(request)
|
data = JSONParser().parse(request)
|
||||||
serializer = SnippetSerializer(data=data)
|
serializer = SnippetSerializer(data=data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return JSONResponse(serializer.data, status=201)
|
return JsonResponse(serializer.data, status=201)
|
||||||
return JSONResponse(serializer.errors, status=400)
|
return JsonResponse(serializer.errors, status=400)
|
||||||
|
|
||||||
Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
|
Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
|
||||||
|
|
||||||
|
@ -272,15 +261,15 @@ We'll also need a view which corresponds to an individual snippet, and can be us
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
serializer = SnippetSerializer(snippet)
|
serializer = SnippetSerializer(snippet)
|
||||||
return JSONResponse(serializer.data)
|
return JsonResponse(serializer.data)
|
||||||
|
|
||||||
elif request.method == 'PUT':
|
elif request.method == 'PUT':
|
||||||
data = JSONParser().parse(request)
|
data = JSONParser().parse(request)
|
||||||
serializer = SnippetSerializer(snippet, data=data)
|
serializer = SnippetSerializer(snippet, data=data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return JSONResponse(serializer.data)
|
return JsonResponse(serializer.data)
|
||||||
return JSONResponse(serializer.errors, status=400)
|
return JsonResponse(serializer.errors, status=400)
|
||||||
|
|
||||||
elif request.method == 'DELETE':
|
elif request.method == 'DELETE':
|
||||||
snippet.delete()
|
snippet.delete()
|
||||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
|
from rest_framework import exceptions
|
||||||
from rest_framework.compat import is_authenticated
|
from rest_framework.compat import is_authenticated
|
||||||
|
|
||||||
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
||||||
|
@ -107,6 +108,10 @@ class DjangoModelPermissions(BasePermission):
|
||||||
'app_label': model_cls._meta.app_label,
|
'app_label': model_cls._meta.app_label,
|
||||||
'model_name': model_cls._meta.model_name
|
'model_name': model_cls._meta.model_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if method not in self.perms_map:
|
||||||
|
raise exceptions.MethodNotAllowed(method)
|
||||||
|
|
||||||
return [perm % kwargs for perm in self.perms_map[method]]
|
return [perm % kwargs for perm in self.perms_map[method]]
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
@ -168,6 +173,10 @@ class DjangoObjectPermissions(DjangoModelPermissions):
|
||||||
'app_label': model_cls._meta.app_label,
|
'app_label': model_cls._meta.app_label,
|
||||||
'model_name': model_cls._meta.model_name
|
'model_name': model_cls._meta.model_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if method not in self.perms_map:
|
||||||
|
raise exceptions.MethodNotAllowed(method)
|
||||||
|
|
||||||
return [perm % kwargs for perm in self.perms_map[method]]
|
return [perm % kwargs for perm in self.perms_map[method]]
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for key, value in results.items %}
|
{% for key, value in results|items %}
|
||||||
{% if key in details %}
|
{% if key in details %}
|
||||||
<tr><th>{{ key|capfirst }}</th><td {{ value|add_nested_class }}>{{ value|format_value }}</td></tr>
|
<tr><th>{{ key|capfirst }}</th><td {{ value|add_nested_class }}>{{ value|format_value }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for key, value in value.items %}
|
{% for k, v in value|items %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ key|format_value }}</th>
|
<th>{{ k|format_value }}</th>
|
||||||
<td>{{ value|format_value }}</td>
|
<td>{{ v|format_value }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in results %}
|
{% for row in results %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for key, value in row.items %}
|
{% for key, value in row|items %}
|
||||||
{% if key in columns %}
|
{% if key in columns %}
|
||||||
<td {{ value|add_nested_class }} >
|
<td {{ value|add_nested_class }} >
|
||||||
{{ value|format_value }}
|
{{ value|format_value }}
|
||||||
|
|
|
@ -255,6 +255,17 @@ def format_value(value):
|
||||||
return six.text_type(value)
|
return six.text_type(value)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def items(value):
|
||||||
|
"""
|
||||||
|
Simple filter to return the items of the dict. Useful when the dict may
|
||||||
|
have a key 'items' which is resolved first in Django tempalte dot-notation
|
||||||
|
lookup. See issue #4931
|
||||||
|
Also see: https://stackoverflow.com/questions/15416662/django-template-loop-over-dictionary-items-with-items-as-key
|
||||||
|
"""
|
||||||
|
return value.items()
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def add_nested_class(value):
|
def add_nested_class(value):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
|
|
|
@ -200,6 +200,15 @@ class ModelPermissionsIntegrationTests(TestCase):
|
||||||
response = empty_list_view(request, pk=1)
|
response = empty_list_view(request, pk=1)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_calling_method_not_allowed(self):
|
||||||
|
request = factory.generic('METHOD_NOT_ALLOWED', '/')
|
||||||
|
response = root_view(request)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
|
request = factory.generic('METHOD_NOT_ALLOWED', '/1')
|
||||||
|
response = instance_view(request, pk='1')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
|
|
||||||
class BasicPermModel(models.Model):
|
class BasicPermModel(models.Model):
|
||||||
text = models.CharField(max_length=100)
|
text = models.CharField(max_length=100)
|
||||||
|
@ -384,6 +393,11 @@ class ObjectPermissionsIntegrationTests(TestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertListEqual(response.data, [])
|
self.assertListEqual(response.data, [])
|
||||||
|
|
||||||
|
def test_cannot_method_not_allowed(self):
|
||||||
|
request = factory.generic('METHOD_NOT_ALLOWED', '/')
|
||||||
|
response = object_permissions_list_view(request)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
|
|
||||||
class BasicPerm(permissions.BasePermission):
|
class BasicPerm(permissions.BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
|
@ -648,3 +648,47 @@ class AdminRendererTests(TestCase):
|
||||||
assert result == ''
|
assert result == ''
|
||||||
assert response.status_code == status.HTTP_303_SEE_OTHER
|
assert response.status_code == status.HTTP_303_SEE_OTHER
|
||||||
assert response['Location'] == 'http://example.com'
|
assert response['Location'] == 'http://example.com'
|
||||||
|
|
||||||
|
def test_render_dict(self):
|
||||||
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
class DummyView(APIView):
|
||||||
|
renderer_classes = (AdminRenderer, )
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response({'foo': 'a string'})
|
||||||
|
view = DummyView.as_view()
|
||||||
|
request = factory.get('/')
|
||||||
|
response = view(request)
|
||||||
|
response.render()
|
||||||
|
self.assertInHTML('<tr><th>Foo</th><td>a string</td></tr>', str(response.content))
|
||||||
|
|
||||||
|
def test_render_dict_with_items_key(self):
|
||||||
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
class DummyView(APIView):
|
||||||
|
renderer_classes = (AdminRenderer, )
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response({'items': 'a string'})
|
||||||
|
|
||||||
|
view = DummyView.as_view()
|
||||||
|
request = factory.get('/')
|
||||||
|
response = view(request)
|
||||||
|
response.render()
|
||||||
|
self.assertInHTML('<tr><th>Items</th><td>a string</td></tr>', str(response.content))
|
||||||
|
|
||||||
|
def test_render_dict_with_iteritems_key(self):
|
||||||
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
class DummyView(APIView):
|
||||||
|
renderer_classes = (AdminRenderer, )
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response({'iteritems': 'a string'})
|
||||||
|
|
||||||
|
view = DummyView.as_view()
|
||||||
|
request = factory.get('/')
|
||||||
|
response = view(request)
|
||||||
|
response.render()
|
||||||
|
self.assertInHTML('<tr><th>Iteritems</th><td>a string</td></tr>', str(response.content))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user