Added the api_view decorator

This commit is contained in:
Tom Christie 2012-09-03 15:57:43 +01:00
parent 7abef9ac3b
commit 149b00a070
7 changed files with 143 additions and 27 deletions

View File

@ -0,0 +1,43 @@
from functools import wraps
from django.http import Http404
from django.utils.decorators import available_attrs
from django.core.exceptions import PermissionDenied
from djangorestframework import exceptions
from djangorestframework import status
from djangorestframework.response import Response
from djangorestframework.request import Request
def api_view(allowed_methods):
"""
Decorator to make a view only accept particular request methods. Usage::
@api_view(['GET', 'POST'])
def my_view(request):
# request will be an instance of `Request`
# APIException instances will be handled
Note that request methods should be in uppercase.
"""
allowed_methods = [method.upper() for method in allowed_methods]
def decorator(func):
@wraps(func, assigned=available_attrs(func))
def inner(request, *args, **kwargs):
try:
request = Request(request)
if request.method not in allowed_methods:
return exceptions.MethodNotAllowed(request.method)
response = func(request, *args, **kwargs)
response.request = request
return response
except exceptions.APIException as exc:
return Response({'detail': exc.detail}, status=exc.status_code)
except Http404 as exc:
return Response({'detail': 'Not found'},
status=status.HTTP_404_NOT_FOUND)
except PermissionDenied as exc:
return Response({'detail': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN)
return inner
return decorator

View File

@ -23,6 +23,7 @@ from djangorestframework.compat import ETParseError
from xml.parsers.expat import ExpatError
import datetime
import decimal
from io import BytesIO
__all__ = (
@ -63,12 +64,24 @@ class BaseParser(object):
"""
return media_type_matches(self.media_type, content_type)
def parse(self, stream, **opts):
def parse(self, string_or_stream, **opts):
"""
The main entry point to parsers. This is a light wrapper around
`parse_stream`, that instead handles both string and stream objects.
"""
if isinstance(string_or_stream, basestring):
stream = BytesIO(string_or_stream)
else:
stream = string_or_stream
return self.parse_stream(stream, **opts)
def parse_stream(self, stream, **opts):
"""
Given a *stream* to read from, return the deserialized output.
Should return a 2-tuple of (data, files).
Should return parsed data, or a DataAndFiles object consisting of the
parsed data and files.
"""
raise NotImplementedError(".parse() Must be overridden to be implemented.")
raise NotImplementedError(".parse_stream() Must be overridden to be implemented.")
class JSONParser(BaseParser):
@ -78,7 +91,7 @@ class JSONParser(BaseParser):
media_type = 'application/json'
def parse(self, stream, **opts):
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
@ -98,7 +111,7 @@ class YAMLParser(BaseParser):
media_type = 'application/yaml'
def parse(self, stream, **opts):
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
@ -118,7 +131,7 @@ class PlainTextParser(BaseParser):
media_type = 'text/plain'
def parse(self, stream, **opts):
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
@ -135,7 +148,7 @@ class FormParser(BaseParser):
media_type = 'application/x-www-form-urlencoded'
def parse(self, stream, **opts):
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
@ -153,7 +166,7 @@ class MultiPartParser(BaseParser):
media_type = 'multipart/form-data'
def parse(self, stream, **opts):
def parse_stream(self, stream, **opts):
"""
Returns a DataAndFiles object.
@ -177,7 +190,7 @@ class XMLParser(BaseParser):
media_type = 'application/xml'
def parse(self, stream, **opts):
def parse_stream(self, stream, **opts):
try:
tree = ET.parse(stream)
except (ExpatError, ETParseError, ValueError), exc:

View File

@ -14,6 +14,7 @@ from Internet Explorer user agents and use a sensible browser `Accept` header in
from django.template.response import SimpleTemplateResponse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from djangorestframework.settings import api_settings
from djangorestframework.utils.mediatypes import order_by_precedence
from djangorestframework.utils import MSIE_USER_AGENT_REGEX
from djangorestframework import status
@ -53,7 +54,12 @@ class Response(SimpleTemplateResponse):
"""
Instantiates and returns the list of renderers the response will use.
"""
return [renderer(self.view) for renderer in self.renderers]
if self.renderers is None:
renderer_classes = api_settings.DEFAULT_RENDERERS
else:
renderer_classes = self.renderers
return [cls(self.view) for cls in renderer_classes]
@property
def rendered_content(self):

View File

@ -0,0 +1,49 @@
"""
Settings for REST framework are all namespaced in the API_SETTINGS setting.
For example your project's `settings.py` file might look like this:
API_SETTINGS = {
'DEFAULT_RENDERERS': (
'djangorestframework.renderers.JSONRenderer',
'djangorestframework.renderers.YAMLRenderer',
)
'DEFAULT_PARSERS': (
'djangorestframework.parsers.JSONParser',
'djangorestframework.parsers.YAMLParser',
)
}
"""
from django.conf import settings
from djangorestframework import renderers
from djangorestframework.compat import yaml
DEFAULTS = {
'DEFAULT_RENDERERS': (
renderers.JSONRenderer,
renderers.JSONPRenderer,
renderers.DocumentingHTMLRenderer,
renderers.DocumentingXHTMLRenderer,
renderers.DocumentingPlainTextRenderer,
renderers.XMLRenderer
)
}
if yaml:
DEFAULTS['DEFAULT_RENDERERS'] += (renderers.YAMLRenderer, )
class APISettings(object):
def __getattr__(self, attr):
try:
return settings.API_SETTINGS[attr]
except (AttributeError, KeyError):
# 'API_SETTINGS' does not exist,
# or requested setting is not present in 'API_SETTINGS'.
try:
return DEFAULTS[attr]
except KeyError:
raise AttributeError("No such setting '%s'" % attr)
api_settings = APISettings()

View File

@ -146,8 +146,8 @@ class View(DjangoView):
def http_method_not_allowed(self, request, *args, **kwargs):
"""
Return an HTTP 405 error if an operation is called which does not have
a handler method.
Called if `request.method` does not corrospond to a handler method.
We raise an exception, which is handled by `.handle_exception()`.
"""
raise exceptions.MethodNotAllowed(request.method)

View File

@ -145,7 +145,7 @@ Deserialization is similar. First we parse a stream into python native datatype
serializer.is_valid()
# True
serializer.object
# <Comment object at 0x10633b2d0>
# <Comment: Comment object>
Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer.

View File

@ -36,14 +36,19 @@ The wrappers also provide behaviour such as returning `405 Method Not Allowed` r
Okay, let's go ahead and start using these new components to write a few views.
from djangorestframework.decorators import api_view
from djangorestframework.status import *
We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
@api_view(allow=['GET', 'POST'])
from blog.models import Comment
from blog.serializers import CommentSerializer
from djangorestframework import status
from djangorestframework.decorators import api_view
from djangorestframework.response import Response
@api_view(['GET', 'POST'])
def comment_root(request):
"""
List all comments, or create a new comment.
"""
"""
if request.method == 'GET':
comments = Comment.objects.all()
serializer = CommentSerializer(instance=comments)
@ -54,14 +59,14 @@ Okay, let's go ahead and start using these new components to write a few views.
if serializer.is_valid():
comment = serializer.object
comment.save()
return Response(serializer.data, status=HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.error_data, status=HTTP_400_BAD_REQUEST)
return Response(serializer.error_data, status=status.HTTP_400_BAD_REQUEST)
Our instance view is an improvement over the previous example. It's slightly more concise, and the code now feels very similar to if we were working with the Forms API.
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
@api_view(allow=['GET', 'PUT', 'DELETE'])
@api_view(['GET', 'PUT', 'DELETE'])
def comment_instance(request, pk):
"""
Retrieve, update or delete a comment instance.
@ -69,7 +74,7 @@ Our instance view is an improvement over the previous example. It's slightly mo
try:
comment = Comment.objects.get(pk=pk)
except Comment.DoesNotExist:
return Response(status=HTTP_404_NOT_FOUND)
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = CommentSerializer(instance=comment)
@ -82,19 +87,19 @@ Our instance view is an improvement over the previous example. It's slightly mo
comment.save()
return Response(serializer.data)
else:
return Response(serializer.error_data, status=HTTP_400_BAD_REQUEST)
return Response(serializer.error_data, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
comment.delete()
return Response(status=HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_204_NO_CONTENT)
This should all feel very familiar - it looks a lot like working with forms in regular Django views.
This should all feel very familiar - there's not a lot different to working with regular Django views.
Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
## Adding optional format suffixes to our URLs
To take advantage of that, let's add support for format suffixes to our API endpoints, so that we can use URLs that explicitly refer to a given format. That means our API will be able to handle URLs such as [http://example.com/api/items/4.json][1].
To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url].
Start by adding a `format` keyword argument to both of the views, like so.
@ -131,7 +136,7 @@ Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/][3
In [tutorial part 3][4], we'll start using class based views, and see how generic views reduce the amount of code we need to write.
[1]: http://example.com/api/items/4.json
[json-url]: http://example.com/api/items/4.json
[2]: 1-serialization.md
[3]: http://127.0.0.1:8000/
[4]: 3-class-based-views.md