diff --git a/djangorestframework/decorators.py b/djangorestframework/decorators.py new file mode 100644 index 000000000..ff1583672 --- /dev/null +++ b/djangorestframework/decorators.py @@ -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 diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index fc0260fcf..96dd81ede 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -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: diff --git a/djangorestframework/response.py b/djangorestframework/response.py index 9dde1f015..4664e0799 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -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): diff --git a/djangorestframework/settings.py b/djangorestframework/settings.py new file mode 100644 index 000000000..1acaad0c3 --- /dev/null +++ b/djangorestframework/settings.py @@ -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() diff --git a/djangorestframework/views.py b/djangorestframework/views.py index fa34dc9a4..3b76988c8 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -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) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 34bac1559..3d6615d91 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -145,7 +145,7 @@ Deserialization is similar. First we parse a stream into python native datatype serializer.is_valid() # True serializer.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. diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 2bb6c20eb..9309f6e0f 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -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 \ No newline at end of file