diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index 3ddd9e45f..197aa424c 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -11,7 +11,7 @@ import base64 __all__ = ( 'BaseAuthentication', 'BasicAuthentication', - 'UserLoggedInAuthentication' + 'SessionAuthentication' ) @@ -68,7 +68,7 @@ class BasicAuthentication(BaseAuthentication): return None -class UserLoggedInAuthentication(BaseAuthentication): +class SessionAuthentication(BaseAuthentication): """ Use Django's session framework for authentication. """ diff --git a/djangorestframework/decorators.py b/djangorestframework/decorators.py index ff1583672..314ea6350 100644 --- a/djangorestframework/decorators.py +++ b/djangorestframework/decorators.py @@ -10,14 +10,13 @@ from djangorestframework.request import Request def api_view(allowed_methods): """ - Decorator to make a view only accept particular request methods. Usage:: + Decorator for function based views. @api_view(['GET', 'POST']) def my_view(request): # request will be an instance of `Request` + # `Response` objects will have .request set automatically # APIException instances will be handled - - Note that request methods should be in uppercase. """ allowed_methods = [method.upper() for method in allowed_methods] @@ -25,17 +24,26 @@ def api_view(allowed_methods): @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) + raise exceptions.MethodNotAllowed(request.method) + response = func(request, *args, **kwargs) - response.request = request + + if isinstance(response, Response): + 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) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 2721f59ef..7269f2980 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -2,10 +2,31 @@ from djangorestframework import status from djangorestframework.response import Response +class MetadataMixin(object): + """ + Should be mixed in with any `BaseView`. + """ + def metadata(self, request, *args, **kwargs): + content = { + 'name': self.get_name(), + 'description': self.get_description(), + 'renders': self._rendered_media_types, + 'parses': self._parsed_media_types, + } + # TODO: Add 'fields', from serializer info. + # form = self.get_bound_form() + # if form is not None: + # field_name_types = {} + # for name, field in form.fields.iteritems(): + # field_name_types[name] = field.__class__.__name__ + # content['fields'] = field_name_types + raise Response(content, status=status.HTTP_200_OK) + + class CreateModelMixin(object): """ Create a model instance. - Should be mixed in with any `APIView` + Should be mixed in with any `BaseView`. """ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.DATA) @@ -47,7 +68,7 @@ class UpdateModelMixin(object): self.object = self.get_object() serializer = self.get_serializer(data=request.DATA, instance=self.object) if serializer.is_valid(): - self.object = serializer.deserialized + self.object = serializer.object self.object.save() return Response(serializer.data) return Response(serializer.error_data, status=status.HTTP_400_BAD_REQUEST) diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py index 7ad99c074..2bb90c0ab 100644 --- a/djangorestframework/tests/request.py +++ b/djangorestframework/tests/request.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User from django.test import TestCase, Client from djangorestframework import status -from djangorestframework.authentication import UserLoggedInAuthentication +from djangorestframework.authentication import SessionAuthentication from djangorestframework.utils import RequestFactory from djangorestframework.parsers import ( FormParser, @@ -208,7 +208,7 @@ class TestContentParsing(TestCase): class MockView(APIView): - authentication = (UserLoggedInAuthentication,) + authentication = (SessionAuthentication,) def post(self, request): if request.POST.get('example') is not None: @@ -233,7 +233,7 @@ class TestContentParsingWithAuthentication(TestCase): def test_user_logged_in_authentication_has_POST_when_not_logged_in(self): """ - Ensures request.POST exists after UserLoggedInAuthentication when user + Ensures request.POST exists after SessionAuthentication when user doesn't log in. """ content = {'example': 'example'} diff --git a/djangorestframework/urls.py b/djangorestframework/urls.py index 3fa813eae..e446c3964 100644 --- a/djangorestframework/urls.py +++ b/djangorestframework/urls.py @@ -1,3 +1,17 @@ +""" +Login and logout views for the browseable API. + +Add these to your root URLconf if you're using the browseable API and +your API requires authentication. + +The urls must be namespaced as 'djangorestframework', and you should make sure +your authentication settings include `SessionAuthentication`. + + urlpatterns = patterns('', + ... + url(r'^auth', include('djangorestframework.urls', namespace='djangorestframework')) + ) +""" from django.conf.urls.defaults import patterns, url diff --git a/djangorestframework/utils/mediatypes.py b/djangorestframework/utils/mediatypes.py index 48dca0f02..5eba7fb2a 100644 --- a/djangorestframework/utils/mediatypes.py +++ b/djangorestframework/utils/mediatypes.py @@ -53,7 +53,7 @@ def get_media_type_params(media_type): def order_by_precedence(media_type_lst): """ - Returns a list of lists of media type strings, ordered by precedence. + Returns a list of sets of media type strings, ordered by precedence. Precedence is determined by how specific a media type is: 3. 'type/subtype; param=val' @@ -61,11 +61,11 @@ def order_by_precedence(media_type_lst): 1. 'type/*' 0. '*/*' """ - ret = [[], [], [], []] + ret = [set(), set(), set(), set()] for media_type in media_type_lst: precedence = _MediaType(media_type).precedence - ret[3 - precedence].append(media_type) - return ret + ret[3 - precedence].add(media_type) + return [media_types for media_types in ret if media_types] class _MediaType(object): diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 1939eed22..3f0138d83 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -80,7 +80,7 @@ class APIView(_View): List of parser classes the view can parse the request with. """ - authentication = (authentication.UserLoggedInAuthentication, + authentication = (authentication.SessionAuthentication, authentication.BasicAuthentication) """ List of all authenticating methods to attempt. @@ -217,11 +217,14 @@ class APIView(_View): else in the view. Returns the final response object. """ - response.view = self - response.request = request - response.renderers = self.renderers + if isinstance(response, Response): + response.view = self + response.request = request + response.renderers = self.renderers + for key, value in self.headers.items(): response[key] = value + return response def handle_exception(self, exc): @@ -269,43 +272,43 @@ class APIView(_View): self.response = self.final(request, response, *args, **kwargs) return self.response - def options(self, request, *args, **kwargs): - content = { - 'name': self.get_name(), - 'description': self.get_description(), - 'renders': self._rendered_media_types, - 'parses': self._parsed_media_types, + +# Abstract view classes that do not provide any method handlers, +# but which provide required behaviour for concrete views to build on. + +class BaseView(APIView): + """ + Base class for all generic views. + """ + serializer_class = None + + def get_serializer(self, data=None, files=None, instance=None): + context = { + 'request': self.request, + 'format': self.kwargs.get('format', None) } - form = self.get_bound_form() - if form is not None: - field_name_types = {} - for name, field in form.fields.iteritems(): - field_name_types[name] = field.__class__.__name__ - content['fields'] = field_name_types - raise Response(content, status=status.HTTP_200_OK) - -# TODO: .get_serializer() + return self.serializer_class(data, context=context) -### Abstract view classes, that do not provide any method handlers ### - -class MultipleObjectBaseView(MultipleObjectMixin, APIView): +class MultipleObjectBaseView(MultipleObjectMixin, BaseView): """ - Base class for views onto a queryset. + Base class for generic views onto a queryset. """ pass -class SingleObjectBaseView(SingleObjectMixin, APIView): +class SingleObjectBaseView(SingleObjectMixin, BaseView): """ - Base class for views onto a model instance. + Base class for generic views onto a model instance. """ pass -### Concrete view classes, that provide existing method handlers ### +# Concrete view classes that provide method handlers +# by composing the mixin classes with a base view. class ListAPIView(mixins.ListModelMixin, + mixins.MetadataMixin, MultipleObjectBaseView): """ Concrete view for listing a queryset. @@ -313,9 +316,13 @@ class ListAPIView(mixins.ListModelMixin, def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) + def options(self, request, *args, **kwargs): + return self.metadata(request, *args, **kwargs) + class RootAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, + mixins.MetadataMixin, MultipleObjectBaseView): """ Concrete view for listing a queryset or creating a model instance. @@ -326,19 +333,27 @@ class RootAPIView(mixins.ListModelMixin, def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) + def options(self, request, *args, **kwargs): + return self.metadata(request, *args, **kwargs) + class DetailAPIView(mixins.RetrieveModelMixin, - SingleObjectBaseView): + mixins.MetadataMixin, + SingleObjectBaseView): """ Concrete view for retrieving a model instance. """ def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) + def options(self, request, *args, **kwargs): + return self.metadata(request, *args, **kwargs) + class InstanceAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, + mixins.MetadataMixin, SingleObjectBaseView): """ Concrete view for retrieving, updating or deleting a model instance. @@ -351,3 +366,6 @@ class InstanceAPIView(mixins.RetrieveModelMixin, def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + + def options(self, request, *args, **kwargs): + return self.metadata(request, *args, **kwargs)