mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-31 07:57:55 +03:00 
			
		
		
		
	Getting the API into shape
This commit is contained in:
		
							parent
							
								
									d373b3a067
								
							
						
					
					
						commit
						8f58ee489d
					
				|  | @ -1,43 +1,58 @@ | ||||||
| """The :mod:`authentication` modules provides for pluggable authentication behaviour. |  | ||||||
| 
 |  | ||||||
| Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.BaseView` or Django :class:`View` class. |  | ||||||
| 
 |  | ||||||
| The set of authentication which are use is then specified by setting the :attr:`authentication` attribute on the class, and listing a set of authentication classes. |  | ||||||
| """ | """ | ||||||
|  | The ``authentication`` module provides a set of pluggable authentication classes. | ||||||
|  | 
 | ||||||
|  | Authentication behavior is provided by adding the ``AuthMixin`` class to a ``View`` . | ||||||
|  | 
 | ||||||
|  | The set of authentication methods which are used is then specified by setting | ||||||
|  | ``authentication`` attribute on the ``View`` class, and listing a set of authentication classes. | ||||||
|  | """ | ||||||
|  | 
 | ||||||
| from django.contrib.auth import authenticate | from django.contrib.auth import authenticate | ||||||
| from django.middleware.csrf import CsrfViewMiddleware | from django.middleware.csrf import CsrfViewMiddleware | ||||||
| from djangorestframework.utils import as_tuple | from djangorestframework.utils import as_tuple | ||||||
| import base64 | import base64 | ||||||
| 
 | 
 | ||||||
|  | __all__ = ( | ||||||
|  |     'BaseAuthenticaton', | ||||||
|  |     'BasicAuthenticaton', | ||||||
|  |     'UserLoggedInAuthenticaton' | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| class BaseAuthenticator(object): | 
 | ||||||
|     """All authentication should extend BaseAuthenticator.""" | class BaseAuthenticaton(object): | ||||||
|  |     """ | ||||||
|  |     All authentication classes should extend BaseAuthentication. | ||||||
|  |     """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self, view): |     def __init__(self, view): | ||||||
|         """Initialise the authentication with the mixin instance as state, |         """ | ||||||
|         in case the authentication needs to access any metadata on the mixin object.""" |         Authentication classes are always passed the current view on creation. | ||||||
|  |         """ | ||||||
|         self.view = view |         self.view = view | ||||||
| 
 | 
 | ||||||
|     def authenticate(self, request): |     def authenticate(self, request): | ||||||
|         """Authenticate the request and return the authentication context or None. |         """ | ||||||
|  |         Authenticate the request and return a ``User`` instance or None. (*) | ||||||
| 
 | 
 | ||||||
|         An authentication context might be something as simple as a User object, or it might |         This function must be overridden to be implemented. | ||||||
|         be some more complicated token, for example authentication tokens which are signed |  | ||||||
|         against a particular set of permissions for a given user, over a given timeframe. |  | ||||||
| 
 |  | ||||||
|         The default permission checking on View will use the allowed_methods attribute |  | ||||||
|         for permissions if the authentication context is not None, and use anon_allowed_methods otherwise. |  | ||||||
| 
 |  | ||||||
|         The authentication context is available to the method calls eg View.get(request) |  | ||||||
|         by accessing self.auth in order to allow them to apply any more fine grained permission |  | ||||||
|         checking at the point the response is being generated. |  | ||||||
|          |          | ||||||
|         This function must be overridden to be implemented.""" |         (*) The authentication context _will_ typically be a ``User`` object, | ||||||
|  |         but it need not be.  It can be any user-like object so long as the | ||||||
|  |         permissions classes on the view can handle the object and use | ||||||
|  |         it to determine if the request has the required permissions or not.  | ||||||
|  | 
 | ||||||
|  |         This can be an important distinction if you're implementing some token | ||||||
|  |         based authentication mechanism, where the authentication context | ||||||
|  |         may be more involved than simply mapping to a ``User``. | ||||||
|  |         """ | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BasicAuthenticator(BaseAuthenticator): | class BasicAuthenticaton(BaseAuthenticaton): | ||||||
|     """Use HTTP Basic authentication""" |     """ | ||||||
|  |     Use HTTP Basic authentication. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|     def authenticate(self, request): |     def authenticate(self, request): | ||||||
|         from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError |         from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError | ||||||
|          |          | ||||||
|  | @ -60,9 +75,13 @@ class BasicAuthenticator(BaseAuthenticator): | ||||||
|         return None |         return None | ||||||
|                  |                  | ||||||
| 
 | 
 | ||||||
| class UserLoggedInAuthenticator(BaseAuthenticator): | class UserLoggedInAuthenticaton(BaseAuthenticaton): | ||||||
|     """Use Django's built-in request session for authentication.""" |     """ | ||||||
|  |     Use Django's session framework for authentication. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|     def authenticate(self, request): |     def authenticate(self, request): | ||||||
|  |         # TODO: Switch this back to request.POST, and let MultiPartParser deal with the consequences. | ||||||
|         if getattr(request, 'user', None) and request.user.is_active: |         if getattr(request, 'user', None) and request.user.is_active: | ||||||
|             # If this is a POST request we enforce CSRF validation. |             # If this is a POST request we enforce CSRF validation. | ||||||
|             if request.method.upper() == 'POST': |             if request.method.upper() == 'POST': | ||||||
|  | @ -77,8 +96,4 @@ class UserLoggedInAuthenticator(BaseAuthenticator): | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| #class DigestAuthentication(BaseAuthentication): | # TODO: TokenAuthentication, DigestAuthentication, OAuthAuthentication | ||||||
| #    pass |  | ||||||
| # |  | ||||||
| #class OAuthAuthentication(BaseAuthentication): |  | ||||||
| #    pass |  | ||||||
|  |  | ||||||
|  | @ -1,31 +1,38 @@ | ||||||
| """""" | """""" | ||||||
| from djangorestframework.utils.mediatypes import MediaType |  | ||||||
| from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX |  | ||||||
| from djangorestframework.response import ErrorResponse |  | ||||||
| from djangorestframework.parsers import FormParser, MultipartParser |  | ||||||
| from djangorestframework import status |  | ||||||
| 
 | 
 | ||||||
|  | from django.contrib.auth.models import AnonymousUser | ||||||
|  | from django.db.models.query import QuerySet | ||||||
|  | from django.db.models.fields.related import RelatedField | ||||||
| from django.http import HttpResponse | from django.http import HttpResponse | ||||||
| from django.http.multipartparser import LimitBytes  # TODO: Use LimitedStream in compat | from django.http.multipartparser import LimitBytes  # TODO: Use LimitedStream in compat | ||||||
| 
 | 
 | ||||||
| from StringIO import StringIO | from djangorestframework import status | ||||||
|  | from djangorestframework.parsers import FormParser, MultiPartParser | ||||||
|  | from djangorestframework.response import Response, ErrorResponse | ||||||
|  | from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX | ||||||
|  | from djangorestframework.utils.mediatypes import is_form_media_type | ||||||
|  | 
 | ||||||
| from decimal import Decimal | from decimal import Decimal | ||||||
| import re | import re | ||||||
|  | from StringIO import StringIO | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| __all__ = ['RequestMixin', | __all__ = ('RequestMixin', | ||||||
|            'ResponseMixin', |            'ResponseMixin', | ||||||
|            'AuthMixin', |            'AuthMixin', | ||||||
|            'ReadModelMixin', |            'ReadModelMixin', | ||||||
|            'CreateModelMixin', |            'CreateModelMixin', | ||||||
|            'UpdateModelMixin', |            'UpdateModelMixin', | ||||||
|            'DeleteModelMixin', |            'DeleteModelMixin', | ||||||
|            'ListModelMixin'] |            'ListModelMixin') | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| ########## Request Mixin ########## | ########## Request Mixin ########## | ||||||
| 
 | 
 | ||||||
| class RequestMixin(object): | class RequestMixin(object): | ||||||
|     """Mixin class to provide request parsing behaviour.""" |     """ | ||||||
|  |     Mixin class to provide request parsing behaviour. | ||||||
|  |     """ | ||||||
| 
 | 
 | ||||||
|     USE_FORM_OVERLOADING = True |     USE_FORM_OVERLOADING = True | ||||||
|     METHOD_PARAM = "_method" |     METHOD_PARAM = "_method" | ||||||
|  | @ -53,41 +60,20 @@ class RequestMixin(object): | ||||||
| 
 | 
 | ||||||
|     def _get_content_type(self): |     def _get_content_type(self): | ||||||
|         """ |         """ | ||||||
|         Returns a MediaType object, representing the request's content type header. |         Returns the content type header. | ||||||
|         """ |         """ | ||||||
|         if not hasattr(self, '_content_type'): |         if not hasattr(self, '_content_type'): | ||||||
|             content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', '')) |             self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', '')) | ||||||
|             if content_type: |  | ||||||
|                 self._content_type = MediaType(content_type) |  | ||||||
|             else: |  | ||||||
|                 self._content_type = None |  | ||||||
|         return self._content_type |         return self._content_type | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def _set_content_type(self, content_type): |     def _set_content_type(self, content_type): | ||||||
|         """ |         """ | ||||||
|         Set the content type.  Should be a MediaType object. |         Set the content type header. | ||||||
|         """ |         """ | ||||||
|         self._content_type = content_type |         self._content_type = content_type | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def _get_accept(self): |  | ||||||
|         """ |  | ||||||
|         Returns a list of MediaType objects, representing the request's accept header. |  | ||||||
|         """ |  | ||||||
|         if not hasattr(self, '_accept'): |  | ||||||
|             accept = self.request.META.get('HTTP_ACCEPT', '*/*') |  | ||||||
|             self._accept = [MediaType(elem) for elem in accept.split(',')] |  | ||||||
|         return self._accept |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     def _set_accept(self): |  | ||||||
|         """ |  | ||||||
|         Set the acceptable media types.  Should be a list of MediaType objects. |  | ||||||
|         """ |  | ||||||
|         self._accept = accept |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     def _get_stream(self): |     def _get_stream(self): | ||||||
|         """ |         """ | ||||||
|         Returns an object that may be used to stream the request content. |         Returns an object that may be used to stream the request content. | ||||||
|  | @ -115,7 +101,7 @@ class RequestMixin(object): | ||||||
|                 #      treated as a limited byte stream. |                 #      treated as a limited byte stream. | ||||||
|                 #   2. It *can* be treated as a limited byte stream, in which case there's a |                 #   2. It *can* be treated as a limited byte stream, in which case there's a | ||||||
|                 #      minor bug in the test client, and potentially some redundant |                 #      minor bug in the test client, and potentially some redundant | ||||||
|                 #      code in MultipartParser. |                 #      code in MultiPartParser. | ||||||
|                 # |                 # | ||||||
|                 #   It's an issue because it affects if you can pass a request off to code that |                 #   It's an issue because it affects if you can pass a request off to code that | ||||||
|                 #   does something like: |                 #   does something like: | ||||||
|  | @ -166,12 +152,12 @@ class RequestMixin(object): | ||||||
|         If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply |         If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply | ||||||
|         delegating them to the original request. |         delegating them to the original request. | ||||||
|         """ |         """ | ||||||
|         if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not self.content_type.is_form(): |         if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not is_form_media_type(self.content_type): | ||||||
|             return |             return | ||||||
| 
 | 
 | ||||||
|         # Temporarily switch to using the form parsers, then parse the content |         # Temporarily switch to using the form parsers, then parse the content | ||||||
|         parsers = self.parsers |         parsers = self.parsers | ||||||
|         self.parsers = (FormParser, MultipartParser) |         self.parsers = (FormParser, MultiPartParser) | ||||||
|         content = self.RAW_CONTENT |         content = self.RAW_CONTENT | ||||||
|         self.parsers = parsers |         self.parsers = parsers | ||||||
| 
 | 
 | ||||||
|  | @ -182,7 +168,7 @@ class RequestMixin(object): | ||||||
| 
 | 
 | ||||||
|         # Content overloading - rewind the stream and modify the content type |         # Content overloading - rewind the stream and modify the content type | ||||||
|         if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content: |         if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content: | ||||||
|             self._content_type = MediaType(content[self.CONTENTTYPE_PARAM]) |             self._content_type = content[self.CONTENTTYPE_PARAM] | ||||||
|             self._stream = StringIO(content[self.CONTENT_PARAM]) |             self._stream = StringIO(content[self.CONTENT_PARAM]) | ||||||
|             del(self._raw_content) |             del(self._raw_content) | ||||||
| 
 | 
 | ||||||
|  | @ -191,26 +177,21 @@ class RequestMixin(object): | ||||||
|         """ |         """ | ||||||
|         Parse the request content. |         Parse the request content. | ||||||
| 
 | 
 | ||||||
|         May raise a 415 ErrorResponse (Unsupported Media Type), |         May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request). | ||||||
|         or a 400 ErrorResponse (Bad Request). |  | ||||||
|         """ |         """ | ||||||
|         if stream is None or content_type is None: |         if stream is None or content_type is None: | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|         parsers = as_tuple(self.parsers) |         parsers = as_tuple(self.parsers) | ||||||
| 
 | 
 | ||||||
|         parser = None |  | ||||||
|         for parser_cls in parsers: |         for parser_cls in parsers: | ||||||
|             if parser_cls.handles(content_type): |             parser = parser_cls(self) | ||||||
|                 parser = parser_cls(self) |             if parser.can_handle_request(content_type): | ||||||
|                 break |                 return parser.parse(stream) | ||||||
| 
 | 
 | ||||||
|         if parser is None: |         raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, | ||||||
|             raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, |                             {'error': 'Unsupported media type in request \'%s\'.' % | ||||||
|                                     {'error': 'Unsupported media type in request \'%s\'.' % |                             content_type}) | ||||||
|                                      content_type.media_type}) |  | ||||||
| 
 |  | ||||||
|         return parser.parse(stream) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def validate(self, content): |     def validate(self, content): | ||||||
|  | @ -250,7 +231,6 @@ class RequestMixin(object): | ||||||
| 
 | 
 | ||||||
|     method = property(_get_method, _set_method) |     method = property(_get_method, _set_method) | ||||||
|     content_type = property(_get_content_type, _set_content_type) |     content_type = property(_get_content_type, _set_content_type) | ||||||
|     accept = property(_get_accept, _set_accept) |  | ||||||
|     stream = property(_get_stream, _set_stream) |     stream = property(_get_stream, _set_stream) | ||||||
|     RAW_CONTENT = property(_get_raw_content) |     RAW_CONTENT = property(_get_raw_content) | ||||||
|     CONTENT = property(_get_content) |     CONTENT = property(_get_content) | ||||||
|  | @ -259,11 +239,13 @@ class RequestMixin(object): | ||||||
| ########## ResponseMixin ########## | ########## ResponseMixin ########## | ||||||
| 
 | 
 | ||||||
| class ResponseMixin(object): | class ResponseMixin(object): | ||||||
|     """Adds behaviour for pluggable Renderers to a :class:`.BaseView` or Django :class:`View`. class. |     """ | ||||||
|  |     Adds behavior for pluggable Renderers to a :class:`.BaseView` or Django :class:`View`. class. | ||||||
|      |      | ||||||
|     Default behaviour is to use standard HTTP Accept header content negotiation. |     Default behaviour is to use standard HTTP Accept header content negotiation. | ||||||
|     Also supports overidding the content type by specifying an _accept= parameter in the URL. |     Also supports overidding the content type by specifying an _accept= parameter in the URL. | ||||||
|     Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead.""" |     Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead. | ||||||
|  |     """ | ||||||
| 
 | 
 | ||||||
|     ACCEPT_QUERY_PARAM = '_accept'        # Allow override of Accept header in URL query params |     ACCEPT_QUERY_PARAM = '_accept'        # Allow override of Accept header in URL query params | ||||||
|     REWRITE_IE_ACCEPT_HEADER = True |     REWRITE_IE_ACCEPT_HEADER = True | ||||||
|  | @ -272,7 +254,9 @@ class ResponseMixin(object): | ||||||
| 
 | 
 | ||||||
|          |          | ||||||
|     def render(self, response): |     def render(self, response): | ||||||
|         """Takes a :class:`Response` object and returns a Django :class:`HttpResponse`.""" |         """ | ||||||
|  |         Takes a ``Response`` object and returns an ``HttpResponse``. | ||||||
|  |         """ | ||||||
|         self.response = response |         self.response = response | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|  | @ -374,7 +358,7 @@ class ResponseMixin(object): | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def default_renderer(self): |     def default_renderer(self): | ||||||
|         """Return the resource's most prefered renderer. |         """Return the resource's most preferred renderer. | ||||||
|         (This renderer is used if the client does not send and Accept: header, or sends Accept: */*)""" |         (This renderer is used if the client does not send and Accept: header, or sends Accept: */*)""" | ||||||
|         return self.renderers[0] |         return self.renderers[0] | ||||||
| 
 | 
 | ||||||
|  | @ -382,40 +366,49 @@ class ResponseMixin(object): | ||||||
| ########## Auth Mixin ########## | ########## Auth Mixin ########## | ||||||
| 
 | 
 | ||||||
| class AuthMixin(object): | class AuthMixin(object): | ||||||
|     """Mixin class to provide authentication and permission checking.""" |     """ | ||||||
|  |     Simple mixin class to provide authentication and permission checking, | ||||||
|  |     by adding a set of authentication and permission classes on a ``View``. | ||||||
|  |      | ||||||
|  |     TODO: wrap this behavior around dispatch() | ||||||
|  |     """ | ||||||
|     authentication = () |     authentication = () | ||||||
|     permissions = () |     permissions = () | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def auth(self): |     def user(self): | ||||||
|         if not hasattr(self, '_auth'): |         if not hasattr(self, '_user'): | ||||||
|             self._auth = self._authenticate() |             self._user = self._authenticate() | ||||||
|         return self._auth |         return self._user | ||||||
| 
 |      | ||||||
|     def _authenticate(self): |     def _authenticate(self): | ||||||
|  |         """ | ||||||
|  |         Attempt to authenticate the request using each authentication class in turn. | ||||||
|  |         Returns a ``User`` object, which may be ``AnonymousUser``. | ||||||
|  |         """ | ||||||
|         for authentication_cls in self.authentication: |         for authentication_cls in self.authentication: | ||||||
|             authentication = authentication_cls(self) |             authentication = authentication_cls(self) | ||||||
|             auth = authentication.authenticate(self.request) |             user = authentication.authenticate(self.request) | ||||||
|             if auth: |             if user: | ||||||
|                 return auth |                 return user | ||||||
|         return None |         return AnonymousUser() | ||||||
| 
 |  | ||||||
|     def check_permissions(self): |  | ||||||
|         if not self.permissions: |  | ||||||
|             return |  | ||||||
| 
 | 
 | ||||||
|  |     def _check_permissions(self): | ||||||
|  |         """ | ||||||
|  |         Check user permissions and either raise an ``ErrorResponse`` or return. | ||||||
|  |         """ | ||||||
|  |         user = self.user | ||||||
|         for permission_cls in self.permissions: |         for permission_cls in self.permissions: | ||||||
|             permission = permission_cls(self) |             permission = permission_cls(self) | ||||||
|             if not permission.has_permission(self.auth): |             permission.check_permission(user)                 | ||||||
|                 raise ErrorResponse(status.HTTP_403_FORBIDDEN, |  | ||||||
|                                    {'detail': 'You do not have permission to access this resource. ' + |  | ||||||
|                                     'You may need to login or otherwise authenticate the request.'})                 |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ########## Model Mixins ########## | ########## Model Mixins ########## | ||||||
| 
 | 
 | ||||||
| class ReadModelMixin(object): | class ReadModelMixin(object): | ||||||
|     """Behaviour to read a model instance on GET requests""" |     """ | ||||||
|  |     Behavior to read a model instance on GET requests | ||||||
|  |     """ | ||||||
|     def get(self, request, *args, **kwargs): |     def get(self, request, *args, **kwargs): | ||||||
|         model = self.resource.model |         model = self.resource.model | ||||||
|         try: |         try: | ||||||
|  | @ -432,7 +425,9 @@ class ReadModelMixin(object): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CreateModelMixin(object): | class CreateModelMixin(object): | ||||||
|     """Behaviour to create a model instance on POST requests""" |     """ | ||||||
|  |     Behavior to create a model instance on POST requests | ||||||
|  |     """ | ||||||
|     def post(self, request, *args, **kwargs):         |     def post(self, request, *args, **kwargs):         | ||||||
|         model = self.resource.model |         model = self.resource.model | ||||||
|         # translated 'related_field' kwargs into 'related_field_id' |         # translated 'related_field' kwargs into 'related_field_id' | ||||||
|  | @ -454,7 +449,9 @@ class CreateModelMixin(object): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class UpdateModelMixin(object): | class UpdateModelMixin(object): | ||||||
|     """Behaviour to update a model instance on PUT requests""" |     """ | ||||||
|  |     Behavior to update a model instance on PUT requests | ||||||
|  |     """ | ||||||
|     def put(self, request, *args, **kwargs): |     def put(self, request, *args, **kwargs): | ||||||
|         model = self.resource.model |         model = self.resource.model | ||||||
|         # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url  |         # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url  | ||||||
|  | @ -477,7 +474,9 @@ class UpdateModelMixin(object): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DeleteModelMixin(object): | class DeleteModelMixin(object): | ||||||
|     """Behaviour to delete a model instance on DELETE requests""" |     """ | ||||||
|  |     Behavior to delete a model instance on DELETE requests | ||||||
|  |     """ | ||||||
|     def delete(self, request, *args, **kwargs): |     def delete(self, request, *args, **kwargs): | ||||||
|         model = self.resource.model |         model = self.resource.model | ||||||
|         try: |         try: | ||||||
|  | @ -495,11 +494,13 @@ class DeleteModelMixin(object): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ListModelMixin(object): | class ListModelMixin(object): | ||||||
|     """Behaviour to list a set of model instances on GET requests""" |     """ | ||||||
|  |     Behavior to list a set of model instances on GET requests | ||||||
|  |     """ | ||||||
|     queryset = None |     queryset = None | ||||||
| 
 | 
 | ||||||
|     def get(self, request, *args, **kwargs): |     def get(self, request, *args, **kwargs): | ||||||
|         queryset = self.queryset if self.queryset else self.model.objects.all() |         queryset = self.queryset if self.queryset else self.resource.model.objects.all() | ||||||
|         return queryset.filter(**kwargs) |         return queryset.filter(**kwargs) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| """Django supports parsing the content of an HTTP request, but only for form POST requests. | """ | ||||||
| That behaviour is sufficient for dealing with standard HTML forms, but it doesn't map well | Django supports parsing the content of an HTTP request, but only for form POST requests. | ||||||
|  | That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well | ||||||
| to general HTTP requests. | to general HTTP requests. | ||||||
| 
 | 
 | ||||||
| We need a method to be able to: | We need a method to be able to: | ||||||
|  | @ -8,54 +9,72 @@ We need a method to be able to: | ||||||
| 2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded | 2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded | ||||||
|    and multipart/form-data.  (eg also handle multipart/json) |    and multipart/form-data.  (eg also handle multipart/json) | ||||||
| """ | """ | ||||||
| from django.http.multipartparser import MultiPartParser as DjangoMPParser |  | ||||||
| from django.utils import simplejson as json |  | ||||||
| 
 | 
 | ||||||
| from djangorestframework.response import ErrorResponse | from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser | ||||||
|  | from django.utils import simplejson as json | ||||||
| from djangorestframework import status | from djangorestframework import status | ||||||
| from djangorestframework.utils import as_tuple |  | ||||||
| from djangorestframework.utils.mediatypes import MediaType |  | ||||||
| from djangorestframework.compat import parse_qs | from djangorestframework.compat import parse_qs | ||||||
|  | from djangorestframework.response import ErrorResponse | ||||||
|  | from djangorestframework.utils import as_tuple | ||||||
|  | from djangorestframework.utils.mediatypes import media_type_matches | ||||||
|  | 
 | ||||||
|  | __all__ = ( | ||||||
|  |     'BaseParser', | ||||||
|  |     'JSONParser', | ||||||
|  |     'PlainTextParser', | ||||||
|  |     'FormParser', | ||||||
|  |     'MultiPartParser' | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BaseParser(object): | class BaseParser(object): | ||||||
|     """All parsers should extend BaseParser, specifying a media_type attribute, |     """ | ||||||
|     and overriding the parse() method.""" |     All parsers should extend BaseParser, specifying a media_type attribute, | ||||||
|  |     and overriding the parse() method. | ||||||
|  |     """ | ||||||
|     media_type = None |     media_type = None | ||||||
| 
 | 
 | ||||||
|     def __init__(self, view): |     def __init__(self, view): | ||||||
|         """ |         """ | ||||||
|         Initialise the parser with the View instance as state, |         Initialize the parser with the ``View`` instance as state, | ||||||
|         in case the parser needs to access any metadata on the View object. |         in case the parser needs to access any metadata on the ``View`` object. | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|         self.view = view |         self.view = view | ||||||
|      |      | ||||||
|     @classmethod |     def can_handle_request(self, media_type): | ||||||
|     def handles(self, media_type): |  | ||||||
|         """ |         """ | ||||||
|         Returns `True` if this parser is able to deal with the given MediaType. |         Returns `True` if this parser is able to deal with the given media type. | ||||||
|  |          | ||||||
|  |         The default implementation for this function is to check the ``media_type`` | ||||||
|  |         argument against the ``media_type`` attribute set on the class to see if | ||||||
|  |         they match. | ||||||
|  |          | ||||||
|  |         This may be overridden to provide for other behavior, but typically you'll | ||||||
|  |         instead want to just set the ``media_type`` attribute on the class. | ||||||
|         """ |         """ | ||||||
|         return media_type.match(self.media_type) |         return media_type_matches(media_type, self.media_type) | ||||||
| 
 | 
 | ||||||
|     def parse(self, stream): |     def parse(self, stream): | ||||||
|         """Given a stream to read from, return the deserialized output. |         """ | ||||||
|         The return value may be of any type, but for many parsers it might typically be a dict-like object.""" |         Given a stream to read from, return the deserialized output. | ||||||
|  |         The return value may be of any type, but for many parsers it might typically be a dict-like object. | ||||||
|  |         """ | ||||||
|         raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.") |         raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class JSONParser(BaseParser): | class JSONParser(BaseParser): | ||||||
|     media_type = MediaType('application/json') |     media_type = 'application/json' | ||||||
| 
 | 
 | ||||||
|     def parse(self, stream): |     def parse(self, stream): | ||||||
|         try: |         try: | ||||||
|             return json.load(stream) |             return json.load(stream) | ||||||
|         except ValueError, exc: |         except ValueError, exc: | ||||||
|             raise ErrorResponse(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)}) |             raise ErrorResponse(status.HTTP_400_BAD_REQUEST, | ||||||
|  |                                 {'detail': 'JSON parse error - %s' % unicode(exc)}) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DataFlatener(object): | class DataFlatener(object): | ||||||
|     """Utility object for flatening dictionaries of lists. Useful for "urlencoded" decoded data.""" |     """Utility object for flattening dictionaries of lists. Useful for "urlencoded" decoded data.""" | ||||||
| 
 | 
 | ||||||
|     def flatten_data(self, data): |     def flatten_data(self, data): | ||||||
|         """Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary |         """Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary | ||||||
|  | @ -83,9 +102,9 @@ class PlainTextParser(BaseParser): | ||||||
|     """ |     """ | ||||||
|     Plain text parser. |     Plain text parser. | ||||||
|      |      | ||||||
|     Simply returns the content of the stream |     Simply returns the content of the stream. | ||||||
|     """ |     """ | ||||||
|     media_type = MediaType('text/plain') |     media_type = 'text/plain' | ||||||
| 
 | 
 | ||||||
|     def parse(self, stream): |     def parse(self, stream): | ||||||
|         return stream.read() |         return stream.read() | ||||||
|  | @ -98,7 +117,7 @@ class FormParser(BaseParser, DataFlatener): | ||||||
|     In order to handle select multiple (and having possibly more than a single value for each parameter), |     In order to handle select multiple (and having possibly more than a single value for each parameter), | ||||||
|     you can customize the output by subclassing the method 'is_a_list'.""" |     you can customize the output by subclassing the method 'is_a_list'.""" | ||||||
| 
 | 
 | ||||||
|     media_type = MediaType('application/x-www-form-urlencoded') |     media_type = 'application/x-www-form-urlencoded' | ||||||
| 
 | 
 | ||||||
|     """The value of the parameter when the select multiple is empty. |     """The value of the parameter when the select multiple is empty. | ||||||
|     Browsers are usually stripping the select multiple that have no option selected from the parameters sent. |     Browsers are usually stripping the select multiple that have no option selected from the parameters sent. | ||||||
|  | @ -138,14 +157,14 @@ class MultipartData(dict): | ||||||
|         dict.__init__(self, data) |         dict.__init__(self, data) | ||||||
|         self.FILES = files |         self.FILES = files | ||||||
| 
 | 
 | ||||||
| class MultipartParser(BaseParser, DataFlatener): | class MultiPartParser(BaseParser, DataFlatener): | ||||||
|     media_type = MediaType('multipart/form-data') |     media_type = 'multipart/form-data' | ||||||
|     RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) |     RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) | ||||||
| 
 | 
 | ||||||
|     def parse(self, stream): |     def parse(self, stream): | ||||||
|         upload_handlers = self.view.request._get_upload_handlers() |         upload_handlers = self.view.request._get_upload_handlers() | ||||||
|         django_mpp = DjangoMPParser(self.view.request.META, stream, upload_handlers) |         django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers) | ||||||
|         data, files = django_mpp.parse() |         data, files = django_parser.parse() | ||||||
| 
 | 
 | ||||||
|         # Flatening data, files and combining them |         # Flatening data, files and combining them | ||||||
|         data = self.flatten_data(dict(data.iterlists())) |         data = self.flatten_data(dict(data.iterlists())) | ||||||
|  |  | ||||||
|  | @ -1,66 +1,103 @@ | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from djangorestframework import status | from djangorestframework import status | ||||||
|  | from djangorestframework.response import ErrorResponse | ||||||
| import time | import time | ||||||
| 
 | 
 | ||||||
|  | __all__ = ( | ||||||
|  |     'BasePermission', | ||||||
|  |     'FullAnonAccess', | ||||||
|  |     'IsAuthenticated', | ||||||
|  |     'IsAdminUser', | ||||||
|  |     'IsUserOrIsAnonReadOnly', | ||||||
|  |     'PerUserThrottling' | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _403_FORBIDDEN_RESPONSE = ErrorResponse( | ||||||
|  |     status.HTTP_403_FORBIDDEN, | ||||||
|  |     {'detail': 'You do not have permission to access this resource. ' + | ||||||
|  |                'You may need to login or otherwise authenticate the request.'}) | ||||||
|  | 
 | ||||||
|  | _503_THROTTLED_RESPONSE = ErrorResponse( | ||||||
|  |     status.HTTP_503_SERVICE_UNAVAILABLE, | ||||||
|  |     {'detail': 'request was throttled'}) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class BasePermission(object): | class BasePermission(object): | ||||||
|     """A base class from which all permission classes should inherit.""" |     """ | ||||||
|  |     A base class from which all permission classes should inherit. | ||||||
|  |     """ | ||||||
|     def __init__(self, view): |     def __init__(self, view): | ||||||
|  |         """ | ||||||
|  |         Permission classes are always passed the current view on creation. | ||||||
|  |         """ | ||||||
|         self.view = view |         self.view = view | ||||||
|      |      | ||||||
|     def has_permission(self, auth): |     def check_permission(self, auth): | ||||||
|         return True |         """ | ||||||
|  |         Should simply return, or raise an ErrorResponse. | ||||||
|  |         """ | ||||||
|  |         pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FullAnonAccess(BasePermission): | class FullAnonAccess(BasePermission): | ||||||
|     """""" |     """ | ||||||
|     def has_permission(self, auth): |     Allows full access. | ||||||
|         return True |     """ | ||||||
|  | 
 | ||||||
|  |     def check_permission(self, user): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class IsAuthenticated(BasePermission): | class IsAuthenticated(BasePermission): | ||||||
|     """""" |     """ | ||||||
|     def has_permission(self, auth): |     Allows access only to authenticated users. | ||||||
|         return auth is not None and auth.is_authenticated() |     """ | ||||||
| 
 | 
 | ||||||
| #class IsUser(BasePermission): |     def check_permission(self, user): | ||||||
| #    """The request has authenticated as a user.""" |         if not user.is_authenticated(): | ||||||
| #    def has_permission(self, auth): |             raise _403_FORBIDDEN_RESPONSE  | ||||||
| #        pass |  | ||||||
| # |  | ||||||
| #class IsAdminUser(): |  | ||||||
| #    """The request has authenticated as an admin user.""" |  | ||||||
| #    def has_permission(self, auth): |  | ||||||
| #        pass |  | ||||||
| # |  | ||||||
| #class IsUserOrIsAnonReadOnly(BasePermission): |  | ||||||
| #    """The request has authenticated as a user, or is a read-only request.""" |  | ||||||
| #    def has_permission(self, auth): |  | ||||||
| #        pass |  | ||||||
| # |  | ||||||
| #class OAuthTokenInScope(BasePermission): |  | ||||||
| #    def has_permission(self, auth): |  | ||||||
| #        pass |  | ||||||
| # |  | ||||||
| #class UserHasModelPermissions(BasePermission): |  | ||||||
| #    def has_permission(self, auth): |  | ||||||
| #        pass |  | ||||||
|      |  | ||||||
| 
 | 
 | ||||||
| class Throttling(BasePermission): | class IsAdminUser(): | ||||||
|     """Rate throttling of requests on a per-user basis. |     """ | ||||||
|  |     Allows access only to admin users. | ||||||
|  |     """ | ||||||
| 
 | 
 | ||||||
|     The rate is set by a 'throttle' attribute on the view class. |     def check_permission(self, user): | ||||||
|  |         if not user.is_admin(): | ||||||
|  |             raise _403_FORBIDDEN_RESPONSE | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IsUserOrIsAnonReadOnly(BasePermission): | ||||||
|  |     """ | ||||||
|  |     The request is authenticated as a user, or is a read-only request. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def check_permission(self, user):  | ||||||
|  |         if (not user.is_authenticated() and | ||||||
|  |             self.view.method != 'GET' and | ||||||
|  |             self.view.method != 'HEAD'): | ||||||
|  |             raise _403_FORBIDDEN_RESPONSE | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PerUserThrottling(BasePermission): | ||||||
|  |     """ | ||||||
|  |     Rate throttling of requests on a per-user basis. | ||||||
|  | 
 | ||||||
|  |     The rate is set by a 'throttle' attribute on the ``View`` class. | ||||||
|     The attribute is a two tuple of the form (number of requests, duration in seconds). |     The attribute is a two tuple of the form (number of requests, duration in seconds). | ||||||
| 
 | 
 | ||||||
|     The user's id will be used as a unique identifier if the user is authenticated. |     The user id will be used as a unique identifier if the user is authenticated. | ||||||
|     For anonymous requests, the IP address of the client will be used. |     For anonymous requests, the IP address of the client will be used. | ||||||
| 
 | 
 | ||||||
|     Previous request information used for throttling is stored in the cache. |     Previous request information used for throttling is stored in the cache. | ||||||
|     """ |     """ | ||||||
|     def has_permission(self, auth): | 
 | ||||||
|  |     def check_permission(self, user): | ||||||
|         (num_requests, duration) = getattr(self.view, 'throttle', (0, 0)) |         (num_requests, duration) = getattr(self.view, 'throttle', (0, 0)) | ||||||
| 
 | 
 | ||||||
|         if auth.is_authenticated(): |         if user.is_authenticated(): | ||||||
|             ident = str(auth) |             ident = str(auth) | ||||||
|         else: |         else: | ||||||
|             ident = self.view.request.META.get('REMOTE_ADDR', None) |             ident = self.view.request.META.get('REMOTE_ADDR', None) | ||||||
|  | @ -74,7 +111,7 @@ class Throttling(BasePermission): | ||||||
|             history.pop() |             history.pop() | ||||||
| 
 | 
 | ||||||
|         if len(history) >= num_requests: |         if len(history) >= num_requests: | ||||||
|             raise ErrorResponse(status.HTTP_503_SERVICE_UNAVAILABLE, {'detail': 'request was throttled'}) |             raise _503_THROTTLED_RESPONSE | ||||||
| 
 | 
 | ||||||
|         history.insert(0, now) |         history.insert(0, now) | ||||||
|         cache.set(key, history, duration)         |         cache.set(key, history, duration) | ||||||
|  |  | ||||||
|  | @ -29,8 +29,8 @@ class BaseRenderer(object): | ||||||
|     override the render() function.""" |     override the render() function.""" | ||||||
|     media_type = None |     media_type = None | ||||||
| 
 | 
 | ||||||
|     def __init__(self, resource): |     def __init__(self, view): | ||||||
|         self.resource = resource |         self.view = view | ||||||
| 
 | 
 | ||||||
|     def render(self, output=None, verbose=False): |     def render(self, output=None, verbose=False): | ||||||
|         """By default render simply returns the ouput as-is. |         """By default render simply returns the ouput as-is. | ||||||
|  | @ -42,8 +42,11 @@ class BaseRenderer(object): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TemplateRenderer(BaseRenderer): | class TemplateRenderer(BaseRenderer): | ||||||
|     """Provided for convienience. |     """A Base class provided for convenience. | ||||||
|     Render the output by simply rendering it with the given template.""" | 
 | ||||||
|  |     Render the output simply by using the given template. | ||||||
|  |     To create a template renderer, subclass this, and set | ||||||
|  |     the ``media_type`` and ``template`` attributes""" | ||||||
|     media_type = None |     media_type = None | ||||||
|     template = None |     template = None | ||||||
| 
 | 
 | ||||||
|  | @ -139,7 +142,7 @@ class DocumentingTemplateRenderer(BaseRenderer): | ||||||
|                                                                       widget=forms.Textarea) |                                                                       widget=forms.Textarea) | ||||||
| 
 | 
 | ||||||
|         # If either of these reserved parameters are turned off then content tunneling is not possible |         # If either of these reserved parameters are turned off then content tunneling is not possible | ||||||
|         if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None: |         if self.view.CONTENTTYPE_PARAM is None or self.view.CONTENT_PARAM is None: | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|         # Okey doke, let's do it |         # Okey doke, let's do it | ||||||
|  | @ -147,18 +150,18 @@ class DocumentingTemplateRenderer(BaseRenderer): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def render(self, output=None): |     def render(self, output=None): | ||||||
|         content = self._get_content(self.resource, self.resource.request, output) |         content = self._get_content(self.view, self.view.request, output) | ||||||
|         form_instance = self._get_form_instance(self.resource) |         form_instance = self._get_form_instance(self.view) | ||||||
| 
 | 
 | ||||||
|         if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL): |         if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL): | ||||||
|             login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path)) |             login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.view.request.path)) | ||||||
|             logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path)) |             logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.view.request.path)) | ||||||
|         else: |         else: | ||||||
|             login_url = None |             login_url = None | ||||||
|             logout_url = None |             logout_url = None | ||||||
| 
 | 
 | ||||||
|         name = get_name(self.resource) |         name = get_name(self.view) | ||||||
|         description = get_description(self.resource) |         description = get_description(self.view) | ||||||
| 
 | 
 | ||||||
|         markeddown = None |         markeddown = None | ||||||
|         if apply_markdown: |         if apply_markdown: | ||||||
|  | @ -167,14 +170,14 @@ class DocumentingTemplateRenderer(BaseRenderer): | ||||||
|             except AttributeError:  # TODO: possibly split the get_description / get_name into a mixin class |             except AttributeError:  # TODO: possibly split the get_description / get_name into a mixin class | ||||||
|                 markeddown = None |                 markeddown = None | ||||||
| 
 | 
 | ||||||
|         breadcrumb_list = get_breadcrumbs(self.resource.request.path) |         breadcrumb_list = get_breadcrumbs(self.view.request.path) | ||||||
| 
 | 
 | ||||||
|         template = loader.get_template(self.template) |         template = loader.get_template(self.template) | ||||||
|         context = RequestContext(self.resource.request, { |         context = RequestContext(self.view.request, { | ||||||
|             'content': content, |             'content': content, | ||||||
|             'resource': self.resource, |             'resource': self.view, | ||||||
|             'request': self.resource.request, |             'request': self.view.request, | ||||||
|             'response': self.resource.response, |             'response': self.view.response, | ||||||
|             'description': description, |             'description': description, | ||||||
|             'name': name, |             'name': name, | ||||||
|             'markeddown': markeddown, |             'markeddown': markeddown, | ||||||
|  | @ -233,11 +236,12 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): | ||||||
|     Useful for browsing an API with command line tools.""" |     Useful for browsing an API with command line tools.""" | ||||||
|     media_type = 'text/plain' |     media_type = 'text/plain' | ||||||
|     template = 'renderer.txt' |     template = 'renderer.txt' | ||||||
|      | 
 | ||||||
|  | 
 | ||||||
| DEFAULT_RENDERERS = ( JSONRenderer, | DEFAULT_RENDERERS = ( JSONRenderer, | ||||||
|                      DocumentingHTMLRenderer, |                       DocumentingHTMLRenderer, | ||||||
|                      DocumentingXHTMLRenderer, |                       DocumentingXHTMLRenderer, | ||||||
|                      DocumentingPlainTextRenderer, |                       DocumentingPlainTextRenderer, | ||||||
|                      XMLRenderer ) |                       XMLRenderer ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ Tests for content parsing, and form-overloaded content parsing. | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from djangorestframework.compat import RequestFactory | from djangorestframework.compat import RequestFactory | ||||||
| from djangorestframework.mixins import RequestMixin | from djangorestframework.mixins import RequestMixin | ||||||
| from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser | from djangorestframework.parsers import FormParser, MultiPartParser, PlainTextParser | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestContentParsing(TestCase): | class TestContentParsing(TestCase): | ||||||
|  | @ -19,7 +19,7 @@ class TestContentParsing(TestCase): | ||||||
|     def ensure_determines_form_content_POST(self, view): |     def ensure_determines_form_content_POST(self, view): | ||||||
|         """Ensure view.RAW_CONTENT returns content for POST request with form content.""" |         """Ensure view.RAW_CONTENT returns content for POST request with form content.""" | ||||||
|         form_data = {'qwerty': 'uiop'} |         form_data = {'qwerty': 'uiop'} | ||||||
|         view.parsers = (FormParser, MultipartParser) |         view.parsers = (FormParser, MultiPartParser) | ||||||
|         view.request = self.req.post('/', data=form_data) |         view.request = self.req.post('/', data=form_data) | ||||||
|         self.assertEqual(view.RAW_CONTENT, form_data) |         self.assertEqual(view.RAW_CONTENT, form_data) | ||||||
| 
 | 
 | ||||||
|  | @ -34,7 +34,7 @@ class TestContentParsing(TestCase): | ||||||
|     def ensure_determines_form_content_PUT(self, view): |     def ensure_determines_form_content_PUT(self, view): | ||||||
|         """Ensure view.RAW_CONTENT returns content for PUT request with form content.""" |         """Ensure view.RAW_CONTENT returns content for PUT request with form content.""" | ||||||
|         form_data = {'qwerty': 'uiop'} |         form_data = {'qwerty': 'uiop'} | ||||||
|         view.parsers = (FormParser, MultipartParser) |         view.parsers = (FormParser, MultiPartParser) | ||||||
|         view.request = self.req.put('/', data=form_data) |         view.request = self.req.put('/', data=form_data) | ||||||
|         self.assertEqual(view.RAW_CONTENT, form_data) |         self.assertEqual(view.RAW_CONTENT, form_data) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -39,7 +39,7 @@ This new parser only flattens the lists of parameters that contain a single valu | ||||||
|     >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} |     >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} | ||||||
|     True |     True | ||||||
| 
 | 
 | ||||||
| .. note:: The same functionality is available for :class:`parsers.MultipartParser`. | .. note:: The same functionality is available for :class:`parsers.MultiPartParser`. | ||||||
| 
 | 
 | ||||||
| Submitting an empty list | Submitting an empty list | ||||||
| -------------------------- | -------------------------- | ||||||
|  | @ -80,9 +80,8 @@ import httplib, mimetypes | ||||||
| from tempfile import TemporaryFile | from tempfile import TemporaryFile | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from djangorestframework.compat import RequestFactory | from djangorestframework.compat import RequestFactory | ||||||
| from djangorestframework.parsers import MultipartParser | from djangorestframework.parsers import MultiPartParser | ||||||
| from djangorestframework.views import BaseView | from djangorestframework.views import BaseView | ||||||
| from djangorestframework.utils.mediatypes import MediaType |  | ||||||
| from StringIO import StringIO | from StringIO import StringIO | ||||||
| 
 | 
 | ||||||
| def encode_multipart_formdata(fields, files): | def encode_multipart_formdata(fields, files): | ||||||
|  | @ -113,18 +112,18 @@ def encode_multipart_formdata(fields, files): | ||||||
| def get_content_type(filename): | def get_content_type(filename): | ||||||
|     return mimetypes.guess_type(filename)[0] or 'application/octet-stream' |     return mimetypes.guess_type(filename)[0] or 'application/octet-stream' | ||||||
| 
 | 
 | ||||||
| class TestMultipartParser(TestCase): | class TestMultiPartParser(TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.req = RequestFactory() |         self.req = RequestFactory() | ||||||
|         self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], |         self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], | ||||||
|         [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) |         [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) | ||||||
| 
 | 
 | ||||||
|     def test_multipartparser(self): |     def test_multipartparser(self): | ||||||
|         """Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters.""" |         """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters.""" | ||||||
|         post_req = RequestFactory().post('/', self.body, content_type=self.content_type) |         post_req = RequestFactory().post('/', self.body, content_type=self.content_type) | ||||||
|         view = BaseView() |         view = BaseView() | ||||||
|         view.request = post_req |         view.request = post_req | ||||||
|         parsed = MultipartParser(view).parse(StringIO(self.body)) |         parsed = MultiPartParser(view).parse(StringIO(self.body)) | ||||||
|         self.assertEqual(parsed['key1'], 'val1') |         self.assertEqual(parsed['key1'], 'val1') | ||||||
|         self.assertEqual(parsed.FILES['file1'].read(), 'blablabla') |         self.assertEqual(parsed.FILES['file1'].read(), 'blablabla') | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,11 +4,11 @@ from django.utils import simplejson as json | ||||||
| 
 | 
 | ||||||
| from djangorestframework.compat import RequestFactory | from djangorestframework.compat import RequestFactory | ||||||
| from djangorestframework.views import BaseView | from djangorestframework.views import BaseView | ||||||
| from djangorestframework.permissions import Throttling | from djangorestframework.permissions import PerUserThrottling | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MockView(BaseView): | class MockView(BaseView): | ||||||
|     permissions = ( Throttling, ) |     permissions = ( PerUserThrottling, ) | ||||||
|     throttle = (3, 1) # 3 requests per second |     throttle = (3, 1) # 3 requests per second | ||||||
| 
 | 
 | ||||||
|     def get(self, request): |     def get(self, request): | ||||||
|  |  | ||||||
|  | @ -7,11 +7,39 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 | ||||||
| from django.http.multipartparser import parse_header | from django.http.multipartparser import parse_header | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MediaType(object): | def media_type_matches(lhs, rhs): | ||||||
|  |     """ | ||||||
|  |     Returns ``True`` if the media type in the first argument <= the | ||||||
|  |     media type in the second argument.  The media types are strings | ||||||
|  |     as described by the HTTP spec. | ||||||
|  | 
 | ||||||
|  |     Valid media type strings include: | ||||||
|  | 
 | ||||||
|  |     'application/json indent=4' | ||||||
|  |     'application/json' | ||||||
|  |     'text/*' | ||||||
|  |     '*/*' | ||||||
|  |     """ | ||||||
|  |     lhs = _MediaType(lhs) | ||||||
|  |     rhs = _MediaType(rhs) | ||||||
|  |     return lhs.match(rhs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def is_form_media_type(media_type): | ||||||
|  |     """ | ||||||
|  |     Return True if the media type is a valid form media type as defined by the HTML4 spec. | ||||||
|  |     (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here) | ||||||
|  |     """ | ||||||
|  |     media_type = _MediaType(media_type) | ||||||
|  |     return media_type.full_type == 'application/x-www-form-urlencoded' or \ | ||||||
|  |            media_type.full_type == 'multipart/form-data' | ||||||
|  |    | ||||||
|  |                 | ||||||
|  | class _MediaType(object): | ||||||
|     def __init__(self, media_type_str): |     def __init__(self, media_type_str): | ||||||
|         self.orig = media_type_str |         self.orig = media_type_str | ||||||
|         self.media_type, self.params = parse_header(media_type_str) |         self.full_type, self.params = parse_header(media_type_str) | ||||||
|         self.main_type, sep, self.sub_type = self.media_type.partition('/') |         self.main_type, sep, self.sub_type = self.full_type.partition('/') | ||||||
| 
 | 
 | ||||||
|     def match(self, other): |     def match(self, other): | ||||||
|         """Return true if this MediaType satisfies the constraint of the given MediaType.""" |         """Return true if this MediaType satisfies the constraint of the given MediaType.""" | ||||||
|  | @ -55,14 +83,6 @@ class MediaType(object): | ||||||
|         # NB. quality values should only have up to 3 decimal points |         # NB. quality values should only have up to 3 decimal points | ||||||
|         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9 |         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9 | ||||||
|         return self.quality * 10000 + self.precedence |         return self.quality * 10000 + self.precedence | ||||||
| 
 |  | ||||||
|     def is_form(self): |  | ||||||
|         """ |  | ||||||
|         Return True if the MediaType is a valid form media type as defined by the HTML4 spec. |  | ||||||
|         (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here) |  | ||||||
|         """ |  | ||||||
|         return self.media_type == 'application/x-www-form-urlencoded' or \ |  | ||||||
|                self.media_type == 'multipart/form-data' |  | ||||||
|      |      | ||||||
|     def as_tuple(self): |     def as_tuple(self): | ||||||
|         return (self.main_type, self.sub_type, self.params) |         return (self.main_type, self.sub_type, self.params) | ||||||
|  |  | ||||||
|  | @ -7,11 +7,11 @@ from djangorestframework.mixins import * | ||||||
| from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status | from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| __all__ = ['BaseView', | __all__ = ('BaseView', | ||||||
|            'ModelView', |            'ModelView', | ||||||
|            'InstanceModelView', |            'InstanceModelView', | ||||||
|            'ListOrModelView', |            'ListOrModelView', | ||||||
|            'ListOrCreateModelView'] |            'ListOrCreateModelView') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -32,14 +32,14 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | ||||||
|     # List of parsers the resource can parse the request with. |     # List of parsers the resource can parse the request with. | ||||||
|     parsers = ( parsers.JSONParser, |     parsers = ( parsers.JSONParser, | ||||||
|                 parsers.FormParser, |                 parsers.FormParser, | ||||||
|                 parsers.MultipartParser ) |                 parsers.MultiPartParser ) | ||||||
| 
 | 
 | ||||||
|     # List of validators to validate, cleanup and normalize the request content     |     # List of validators to validate, cleanup and normalize the request content     | ||||||
|     validators = ( validators.FormValidator, ) |     validators = ( validators.FormValidator, ) | ||||||
| 
 | 
 | ||||||
|     # List of all authenticating methods to attempt. |     # List of all authenticating methods to attempt. | ||||||
|     authentication = ( authentication.UserLoggedInAuthenticator, |     authentication = ( authentication.UserLoggedInAuthenticaton, | ||||||
|                        authentication.BasicAuthenticator ) |                        authentication.BasicAuthenticaton ) | ||||||
|      |      | ||||||
|     # List of all permissions that must be checked. |     # List of all permissions that must be checked. | ||||||
|     permissions = ( permissions.FullAnonAccess, ) |     permissions = ( permissions.FullAnonAccess, ) | ||||||
|  | @ -92,7 +92,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | ||||||
|             self.perform_form_overloading() |             self.perform_form_overloading() | ||||||
| 
 | 
 | ||||||
|             # Authenticate and check request is has the relevant permissions |             # Authenticate and check request is has the relevant permissions | ||||||
|             self.check_permissions() |             self._check_permissions() | ||||||
| 
 | 
 | ||||||
|             # Get the appropriate handler method |             # Get the appropriate handler method | ||||||
|             if self.method.lower() in self.http_method_names: |             if self.method.lower() in self.http_method_names: | ||||||
|  | @ -112,9 +112,12 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | ||||||
| 
 | 
 | ||||||
|             # Pre-serialize filtering (eg filter complex objects into natively serializable types) |             # Pre-serialize filtering (eg filter complex objects into natively serializable types) | ||||||
|             response.cleaned_content = self.resource.object_to_serializable(response.raw_content) |             response.cleaned_content = self.resource.object_to_serializable(response.raw_content) | ||||||
| 
 |      | ||||||
|         except ErrorResponse, exc: |         except ErrorResponse, exc: | ||||||
|             response = exc.response |             response = exc.response | ||||||
|  |         except: | ||||||
|  |             import traceback | ||||||
|  |             traceback.print_exc() | ||||||
| 
 | 
 | ||||||
|         # Always add these headers. |         # Always add these headers. | ||||||
|         # |         # | ||||||
|  | @ -124,6 +127,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | ||||||
|         response.headers['Vary'] = 'Authenticate, Accept' |         response.headers['Vary'] = 'Authenticate, Accept' | ||||||
| 
 | 
 | ||||||
|         return self.render(response) |         return self.render(response) | ||||||
|  |      | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ModelView(BaseView): | class ModelView(BaseView): | ||||||
|  | @ -134,11 +138,11 @@ class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, Mode | ||||||
|     """A view which provides default operations for read/update/delete against a model instance.""" |     """A view which provides default operations for read/update/delete against a model instance.""" | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| class ListModelResource(ListModelMixin, ModelView): | class ListModelView(ListModelMixin, ModelView): | ||||||
|     """A view which provides default operations for list, against a model in the database.""" |     """A view which provides default operations for list, against a model in the database.""" | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| class ListOrCreateModelResource(ListModelMixin, CreateModelMixin, ModelView): | class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): | ||||||
|     """A view which provides default operations for list and create, against a model in the database.""" |     """A view which provides default operations for list and create, against a model in the database.""" | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user