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.middleware.csrf import CsrfViewMiddleware | ||||
| from djangorestframework.utils import as_tuple | ||||
| 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): | ||||
|         """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 | ||||
| 
 | ||||
|     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 | ||||
|         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. | ||||
|         This function must be overridden to be implemented. | ||||
|          | ||||
|         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 _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.  | ||||
| 
 | ||||
|         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.""" | ||||
|         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 | ||||
| 
 | ||||
| 
 | ||||
| class BasicAuthenticator(BaseAuthenticator): | ||||
|     """Use HTTP Basic authentication""" | ||||
| class BasicAuthenticaton(BaseAuthenticaton): | ||||
|     """ | ||||
|     Use HTTP Basic authentication. | ||||
|     """ | ||||
| 
 | ||||
|     def authenticate(self, request): | ||||
|         from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError | ||||
|          | ||||
|  | @ -60,9 +75,13 @@ class BasicAuthenticator(BaseAuthenticator): | |||
|         return None | ||||
|                  | ||||
| 
 | ||||
| class UserLoggedInAuthenticator(BaseAuthenticator): | ||||
|     """Use Django's built-in request session for authentication.""" | ||||
| class UserLoggedInAuthenticaton(BaseAuthenticaton): | ||||
|     """ | ||||
|     Use Django's session framework for authentication. | ||||
|     """ | ||||
| 
 | ||||
|     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 this is a POST request we enforce CSRF validation. | ||||
|             if request.method.upper() == 'POST': | ||||
|  | @ -77,8 +96,4 @@ class UserLoggedInAuthenticator(BaseAuthenticator): | |||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| #class DigestAuthentication(BaseAuthentication): | ||||
| #    pass | ||||
| # | ||||
| #class OAuthAuthentication(BaseAuthentication): | ||||
| #    pass | ||||
| # TODO: TokenAuthentication, DigestAuthentication, OAuthAuthentication | ||||
|  |  | |||
|  | @ -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.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 | ||||
| import re | ||||
| from StringIO import StringIO | ||||
| 
 | ||||
| 
 | ||||
| __all__ = ['RequestMixin', | ||||
| __all__ = ('RequestMixin', | ||||
|            'ResponseMixin', | ||||
|            'AuthMixin', | ||||
|            'ReadModelMixin', | ||||
|            'CreateModelMixin', | ||||
|            'UpdateModelMixin', | ||||
|            'DeleteModelMixin', | ||||
|            'ListModelMixin'] | ||||
|            'ListModelMixin') | ||||
| 
 | ||||
| 
 | ||||
| ########## Request Mixin ########## | ||||
| 
 | ||||
| class RequestMixin(object): | ||||
|     """Mixin class to provide request parsing behaviour.""" | ||||
|     """ | ||||
|     Mixin class to provide request parsing behaviour. | ||||
|     """ | ||||
| 
 | ||||
|     USE_FORM_OVERLOADING = True | ||||
|     METHOD_PARAM = "_method" | ||||
|  | @ -53,41 +60,20 @@ class RequestMixin(object): | |||
| 
 | ||||
|     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'): | ||||
|             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 | ||||
|             self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', '')) | ||||
|         return 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 | ||||
| 
 | ||||
| 
 | ||||
|     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): | ||||
|         """ | ||||
|         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. | ||||
|                 #   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 | ||||
|                 #      code in MultipartParser. | ||||
|                 #      code in MultiPartParser. | ||||
|                 # | ||||
|                 #   It's an issue because it affects if you can pass a request off to code that | ||||
|                 #   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 | ||||
|         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 | ||||
| 
 | ||||
|         # Temporarily switch to using the form parsers, then parse the content | ||||
|         parsers = self.parsers | ||||
|         self.parsers = (FormParser, MultipartParser) | ||||
|         self.parsers = (FormParser, MultiPartParser) | ||||
|         content = self.RAW_CONTENT | ||||
|         self.parsers = parsers | ||||
| 
 | ||||
|  | @ -182,7 +168,7 @@ class RequestMixin(object): | |||
| 
 | ||||
|         # Content overloading - rewind the stream and modify the content type | ||||
|         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]) | ||||
|             del(self._raw_content) | ||||
| 
 | ||||
|  | @ -191,26 +177,21 @@ class RequestMixin(object): | |||
|         """ | ||||
|         Parse the request content. | ||||
| 
 | ||||
|         May raise a 415 ErrorResponse (Unsupported Media Type), | ||||
|         or a 400 ErrorResponse (Bad Request). | ||||
|         May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request). | ||||
|         """ | ||||
|         if stream is None or content_type is None: | ||||
|             return None | ||||
| 
 | ||||
|         parsers = as_tuple(self.parsers) | ||||
| 
 | ||||
|         parser = None | ||||
|         for parser_cls in parsers: | ||||
|             if parser_cls.handles(content_type): | ||||
|             parser = parser_cls(self) | ||||
|                 break | ||||
|             if parser.can_handle_request(content_type): | ||||
|                 return parser.parse(stream) | ||||
| 
 | ||||
|         if parser is None: | ||||
|         raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, | ||||
|                             {'error': 'Unsupported media type in request \'%s\'.' % | ||||
|                                      content_type.media_type}) | ||||
| 
 | ||||
|         return parser.parse(stream) | ||||
|                             content_type}) | ||||
| 
 | ||||
| 
 | ||||
|     def validate(self, content): | ||||
|  | @ -250,7 +231,6 @@ class RequestMixin(object): | |||
| 
 | ||||
|     method = property(_get_method, _set_method) | ||||
|     content_type = property(_get_content_type, _set_content_type) | ||||
|     accept = property(_get_accept, _set_accept) | ||||
|     stream = property(_get_stream, _set_stream) | ||||
|     RAW_CONTENT = property(_get_raw_content) | ||||
|     CONTENT = property(_get_content) | ||||
|  | @ -259,11 +239,13 @@ class RequestMixin(object): | |||
| ########## ResponseMixin ########## | ||||
| 
 | ||||
| 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. | ||||
|     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 | ||||
|     REWRITE_IE_ACCEPT_HEADER = True | ||||
|  | @ -272,7 +254,9 @@ class ResponseMixin(object): | |||
| 
 | ||||
|          | ||||
|     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 | ||||
| 
 | ||||
|         try: | ||||
|  | @ -374,7 +358,7 @@ class ResponseMixin(object): | |||
| 
 | ||||
|     @property | ||||
|     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: */*)""" | ||||
|         return self.renderers[0] | ||||
| 
 | ||||
|  | @ -382,40 +366,49 @@ class ResponseMixin(object): | |||
| ########## Auth Mixin ########## | ||||
| 
 | ||||
| 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 = () | ||||
|     permissions = () | ||||
| 
 | ||||
|     @property | ||||
|     def auth(self): | ||||
|         if not hasattr(self, '_auth'): | ||||
|             self._auth = self._authenticate() | ||||
|         return self._auth | ||||
|     def user(self): | ||||
|         if not hasattr(self, '_user'): | ||||
|             self._user = self._authenticate() | ||||
|         return self._user | ||||
|      | ||||
|     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: | ||||
|             authentication = authentication_cls(self) | ||||
|             auth = authentication.authenticate(self.request) | ||||
|             if auth: | ||||
|                 return auth | ||||
|         return None | ||||
| 
 | ||||
|     def check_permissions(self): | ||||
|         if not self.permissions: | ||||
|             return | ||||
|             user = authentication.authenticate(self.request) | ||||
|             if user: | ||||
|                 return user | ||||
|         return AnonymousUser() | ||||
| 
 | ||||
|     def _check_permissions(self): | ||||
|         """ | ||||
|         Check user permissions and either raise an ``ErrorResponse`` or return. | ||||
|         """ | ||||
|         user = self.user | ||||
|         for permission_cls in self.permissions: | ||||
|             permission = permission_cls(self) | ||||
|             if not permission.has_permission(self.auth): | ||||
|                 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.'})                 | ||||
|             permission.check_permission(user)                 | ||||
| 
 | ||||
| 
 | ||||
| ########## Model Mixins ########## | ||||
| 
 | ||||
| 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): | ||||
|         model = self.resource.model | ||||
|         try: | ||||
|  | @ -432,7 +425,9 @@ class ReadModelMixin(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):         | ||||
|         model = self.resource.model | ||||
|         # translated 'related_field' kwargs into 'related_field_id' | ||||
|  | @ -454,7 +449,9 @@ class CreateModelMixin(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): | ||||
|         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  | ||||
|  | @ -477,7 +474,9 @@ class UpdateModelMixin(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): | ||||
|         model = self.resource.model | ||||
|         try: | ||||
|  | @ -495,11 +494,13 @@ class DeleteModelMixin(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 | ||||
| 
 | ||||
|     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) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
| 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 | ||||
|    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.utils import as_tuple | ||||
| from djangorestframework.utils.mediatypes import MediaType | ||||
| 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): | ||||
|     """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 | ||||
| 
 | ||||
|     def __init__(self, view): | ||||
|         """ | ||||
|         Initialise the parser with the View instance as state, | ||||
|         in case the parser needs to access any metadata on the View object. | ||||
|          | ||||
|         Initialize the parser with the ``View`` instance as state, | ||||
|         in case the parser needs to access any metadata on the ``View`` object. | ||||
|         """ | ||||
|         self.view = view | ||||
|      | ||||
|     @classmethod | ||||
|     def handles(self, media_type): | ||||
|     def can_handle_request(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): | ||||
|         """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.") | ||||
| 
 | ||||
| 
 | ||||
| class JSONParser(BaseParser): | ||||
|     media_type = MediaType('application/json') | ||||
|     media_type = 'application/json' | ||||
| 
 | ||||
|     def parse(self, stream): | ||||
|         try: | ||||
|             return json.load(stream) | ||||
|         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): | ||||
|     """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): | ||||
|         """Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary | ||||
|  | @ -83,9 +102,9 @@ class PlainTextParser(BaseParser): | |||
|     """ | ||||
|     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): | ||||
|         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), | ||||
|     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. | ||||
|     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) | ||||
|         self.FILES = files | ||||
| 
 | ||||
| class MultipartParser(BaseParser, DataFlatener): | ||||
|     media_type = MediaType('multipart/form-data') | ||||
| class MultiPartParser(BaseParser, DataFlatener): | ||||
|     media_type = 'multipart/form-data' | ||||
|     RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) | ||||
| 
 | ||||
|     def parse(self, stream): | ||||
|         upload_handlers = self.view.request._get_upload_handlers() | ||||
|         django_mpp = DjangoMPParser(self.view.request.META, stream, upload_handlers) | ||||
|         data, files = django_mpp.parse() | ||||
|         django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers) | ||||
|         data, files = django_parser.parse() | ||||
| 
 | ||||
|         # Flatening data, files and combining them | ||||
|         data = self.flatten_data(dict(data.iterlists())) | ||||
|  |  | |||
|  | @ -1,66 +1,103 @@ | |||
| from django.core.cache import cache | ||||
| from djangorestframework import status | ||||
| from djangorestframework.response import ErrorResponse | ||||
| 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): | ||||
|     """A base class from which all permission classes should inherit.""" | ||||
|     """ | ||||
|     A base class from which all permission classes should inherit. | ||||
|     """ | ||||
|     def __init__(self, view): | ||||
|         """ | ||||
|         Permission classes are always passed the current view on creation. | ||||
|         """ | ||||
|         self.view = view | ||||
|      | ||||
|     def has_permission(self, auth): | ||||
|         return True | ||||
|     def check_permission(self, auth): | ||||
|         """ | ||||
|         Should simply return, or raise an ErrorResponse. | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class FullAnonAccess(BasePermission): | ||||
|     """""" | ||||
|     def has_permission(self, auth): | ||||
|         return True | ||||
|     """ | ||||
|     Allows full access. | ||||
|     """ | ||||
| 
 | ||||
|     def check_permission(self, user): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class IsAuthenticated(BasePermission): | ||||
|     """""" | ||||
|     def has_permission(self, auth): | ||||
|         return auth is not None and auth.is_authenticated() | ||||
|     """ | ||||
|     Allows access only to authenticated users. | ||||
|     """ | ||||
| 
 | ||||
| #class IsUser(BasePermission): | ||||
| #    """The request has authenticated as a user.""" | ||||
| #    def has_permission(self, auth): | ||||
| #        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 | ||||
|     def check_permission(self, user): | ||||
|         if not user.is_authenticated(): | ||||
|             raise _403_FORBIDDEN_RESPONSE  | ||||
| 
 | ||||
| class IsAdminUser(): | ||||
|     """ | ||||
|     Allows access only to admin users. | ||||
|     """ | ||||
| 
 | ||||
|     def check_permission(self, user): | ||||
|         if not user.is_admin(): | ||||
|             raise _403_FORBIDDEN_RESPONSE | ||||
| 
 | ||||
| 
 | ||||
| class Throttling(BasePermission): | ||||
|     """Rate throttling of requests on a per-user basis. | ||||
| class IsUserOrIsAnonReadOnly(BasePermission): | ||||
|     """ | ||||
|     The request is authenticated as a user, or is a read-only request. | ||||
|     """ | ||||
| 
 | ||||
|     The rate is set by a 'throttle' attribute on the view class. | ||||
|     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 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. | ||||
| 
 | ||||
|     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)) | ||||
| 
 | ||||
|         if auth.is_authenticated(): | ||||
|         if user.is_authenticated(): | ||||
|             ident = str(auth) | ||||
|         else: | ||||
|             ident = self.view.request.META.get('REMOTE_ADDR', None) | ||||
|  | @ -74,7 +111,7 @@ class Throttling(BasePermission): | |||
|             history.pop() | ||||
| 
 | ||||
|         if len(history) >= num_requests: | ||||
|             raise ErrorResponse(status.HTTP_503_SERVICE_UNAVAILABLE, {'detail': 'request was throttled'}) | ||||
|             raise _503_THROTTLED_RESPONSE | ||||
| 
 | ||||
|         history.insert(0, now) | ||||
|         cache.set(key, history, duration) | ||||
|  |  | |||
|  | @ -29,8 +29,8 @@ class BaseRenderer(object): | |||
|     override the render() function.""" | ||||
|     media_type = None | ||||
| 
 | ||||
|     def __init__(self, resource): | ||||
|         self.resource = resource | ||||
|     def __init__(self, view): | ||||
|         self.view = view | ||||
| 
 | ||||
|     def render(self, output=None, verbose=False): | ||||
|         """By default render simply returns the ouput as-is. | ||||
|  | @ -42,8 +42,11 @@ class BaseRenderer(object): | |||
| 
 | ||||
| 
 | ||||
| class TemplateRenderer(BaseRenderer): | ||||
|     """Provided for convienience. | ||||
|     Render the output by simply rendering it with the given template.""" | ||||
|     """A Base class provided for convenience. | ||||
| 
 | ||||
|     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 | ||||
|     template = None | ||||
| 
 | ||||
|  | @ -139,7 +142,7 @@ class DocumentingTemplateRenderer(BaseRenderer): | |||
|                                                                       widget=forms.Textarea) | ||||
| 
 | ||||
|         # 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 | ||||
| 
 | ||||
|         # Okey doke, let's do it | ||||
|  | @ -147,18 +150,18 @@ class DocumentingTemplateRenderer(BaseRenderer): | |||
| 
 | ||||
| 
 | ||||
|     def render(self, output=None): | ||||
|         content = self._get_content(self.resource, self.resource.request, output) | ||||
|         form_instance = self._get_form_instance(self.resource) | ||||
|         content = self._get_content(self.view, self.view.request, output) | ||||
|         form_instance = self._get_form_instance(self.view) | ||||
| 
 | ||||
|         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)) | ||||
|             logout_url = "%s?next=%s" % (settings.LOGOUT_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.view.request.path)) | ||||
|         else: | ||||
|             login_url = None | ||||
|             logout_url = None | ||||
| 
 | ||||
|         name = get_name(self.resource) | ||||
|         description = get_description(self.resource) | ||||
|         name = get_name(self.view) | ||||
|         description = get_description(self.view) | ||||
| 
 | ||||
|         markeddown = None | ||||
|         if apply_markdown: | ||||
|  | @ -167,14 +170,14 @@ class DocumentingTemplateRenderer(BaseRenderer): | |||
|             except AttributeError:  # TODO: possibly split the get_description / get_name into a mixin class | ||||
|                 markeddown = None | ||||
| 
 | ||||
|         breadcrumb_list = get_breadcrumbs(self.resource.request.path) | ||||
|         breadcrumb_list = get_breadcrumbs(self.view.request.path) | ||||
| 
 | ||||
|         template = loader.get_template(self.template) | ||||
|         context = RequestContext(self.resource.request, { | ||||
|         context = RequestContext(self.view.request, { | ||||
|             'content': content, | ||||
|             'resource': self.resource, | ||||
|             'request': self.resource.request, | ||||
|             'response': self.resource.response, | ||||
|             'resource': self.view, | ||||
|             'request': self.view.request, | ||||
|             'response': self.view.response, | ||||
|             'description': description, | ||||
|             'name': name, | ||||
|             'markeddown': markeddown, | ||||
|  | @ -234,6 +237,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): | |||
|     media_type = 'text/plain' | ||||
|     template = 'renderer.txt' | ||||
| 
 | ||||
| 
 | ||||
| DEFAULT_RENDERERS = ( JSONRenderer, | ||||
|                       DocumentingHTMLRenderer, | ||||
|                       DocumentingXHTMLRenderer, | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ Tests for content parsing, and form-overloaded content parsing. | |||
| from django.test import TestCase | ||||
| from djangorestframework.compat import RequestFactory | ||||
| from djangorestframework.mixins import RequestMixin | ||||
| from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser | ||||
| from djangorestframework.parsers import FormParser, MultiPartParser, PlainTextParser | ||||
| 
 | ||||
| 
 | ||||
| class TestContentParsing(TestCase): | ||||
|  | @ -19,7 +19,7 @@ class TestContentParsing(TestCase): | |||
|     def ensure_determines_form_content_POST(self, view): | ||||
|         """Ensure view.RAW_CONTENT returns content for POST request with form content.""" | ||||
|         form_data = {'qwerty': 'uiop'} | ||||
|         view.parsers = (FormParser, MultipartParser) | ||||
|         view.parsers = (FormParser, MultiPartParser) | ||||
|         view.request = self.req.post('/', data=form_data) | ||||
|         self.assertEqual(view.RAW_CONTENT, form_data) | ||||
| 
 | ||||
|  | @ -34,7 +34,7 @@ class TestContentParsing(TestCase): | |||
|     def ensure_determines_form_content_PUT(self, view): | ||||
|         """Ensure view.RAW_CONTENT returns content for PUT request with form content.""" | ||||
|         form_data = {'qwerty': 'uiop'} | ||||
|         view.parsers = (FormParser, MultipartParser) | ||||
|         view.parsers = (FormParser, MultiPartParser) | ||||
|         view.request = self.req.put('/', data=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']} | ||||
|     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 | ||||
| -------------------------- | ||||
|  | @ -80,9 +80,8 @@ import httplib, mimetypes | |||
| from tempfile import TemporaryFile | ||||
| from django.test import TestCase | ||||
| from djangorestframework.compat import RequestFactory | ||||
| from djangorestframework.parsers import MultipartParser | ||||
| from djangorestframework.parsers import MultiPartParser | ||||
| from djangorestframework.views import BaseView | ||||
| from djangorestframework.utils.mediatypes import MediaType | ||||
| from StringIO import StringIO | ||||
| 
 | ||||
| def encode_multipart_formdata(fields, files): | ||||
|  | @ -113,18 +112,18 @@ def encode_multipart_formdata(fields, files): | |||
| def get_content_type(filename): | ||||
|     return mimetypes.guess_type(filename)[0] or 'application/octet-stream' | ||||
| 
 | ||||
| class TestMultipartParser(TestCase): | ||||
| class TestMultiPartParser(TestCase): | ||||
|     def setUp(self): | ||||
|         self.req = RequestFactory() | ||||
|         self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], | ||||
|         [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) | ||||
| 
 | ||||
|     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) | ||||
|         view = BaseView() | ||||
|         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.FILES['file1'].read(), 'blablabla') | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,11 +4,11 @@ from django.utils import simplejson as json | |||
| 
 | ||||
| from djangorestframework.compat import RequestFactory | ||||
| from djangorestframework.views import BaseView | ||||
| from djangorestframework.permissions import Throttling | ||||
| from djangorestframework.permissions import PerUserThrottling | ||||
| 
 | ||||
| 
 | ||||
| class MockView(BaseView): | ||||
|     permissions = ( Throttling, ) | ||||
|     permissions = ( PerUserThrottling, ) | ||||
|     throttle = (3, 1) # 3 requests per second | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
| 
 | ||||
| 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): | ||||
|         self.orig = media_type_str | ||||
|         self.media_type, self.params = parse_header(media_type_str) | ||||
|         self.main_type, sep, self.sub_type = self.media_type.partition('/') | ||||
|         self.full_type, self.params = parse_header(media_type_str) | ||||
|         self.main_type, sep, self.sub_type = self.full_type.partition('/') | ||||
| 
 | ||||
|     def match(self, other): | ||||
|         """Return true if this MediaType satisfies the constraint of the given MediaType.""" | ||||
|  | @ -56,14 +84,6 @@ class MediaType(object): | |||
|         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9 | ||||
|         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): | ||||
|         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 | ||||
| 
 | ||||
| 
 | ||||
| __all__ = ['BaseView', | ||||
| __all__ = ('BaseView', | ||||
|            'ModelView', | ||||
|            'InstanceModelView', | ||||
|            'ListOrModelView', | ||||
|            'ListOrCreateModelView'] | ||||
|            'ListOrCreateModelView') | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -32,14 +32,14 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | |||
|     # List of parsers the resource can parse the request with. | ||||
|     parsers = ( parsers.JSONParser, | ||||
|                 parsers.FormParser, | ||||
|                 parsers.MultipartParser ) | ||||
|                 parsers.MultiPartParser ) | ||||
| 
 | ||||
|     # List of validators to validate, cleanup and normalize the request content     | ||||
|     validators = ( validators.FormValidator, ) | ||||
| 
 | ||||
|     # List of all authenticating methods to attempt. | ||||
|     authentication = ( authentication.UserLoggedInAuthenticator, | ||||
|                        authentication.BasicAuthenticator ) | ||||
|     authentication = ( authentication.UserLoggedInAuthenticaton, | ||||
|                        authentication.BasicAuthenticaton ) | ||||
|      | ||||
|     # List of all permissions that must be checked. | ||||
|     permissions = ( permissions.FullAnonAccess, ) | ||||
|  | @ -92,7 +92,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | |||
|             self.perform_form_overloading() | ||||
| 
 | ||||
|             # Authenticate and check request is has the relevant permissions | ||||
|             self.check_permissions() | ||||
|             self._check_permissions() | ||||
| 
 | ||||
|             # Get the appropriate handler method | ||||
|             if self.method.lower() in self.http_method_names: | ||||
|  | @ -115,6 +115,9 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | |||
|      | ||||
|         except ErrorResponse, exc: | ||||
|             response = exc.response | ||||
|         except: | ||||
|             import traceback | ||||
|             traceback.print_exc() | ||||
| 
 | ||||
|         # Always add these headers. | ||||
|         # | ||||
|  | @ -126,6 +129,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | |||
|         return self.render(response) | ||||
|      | ||||
| 
 | ||||
| 
 | ||||
| class ModelView(BaseView): | ||||
|     """A RESTful view that maps to a model in the database.""" | ||||
|     validators = (validators.ModelFormValidator,) | ||||
|  | @ -134,11 +138,11 @@ class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, Mode | |||
|     """A view which provides default operations for read/update/delete against a model instance.""" | ||||
|     pass | ||||
| 
 | ||||
| class ListModelResource(ListModelMixin, ModelView): | ||||
| class ListModelView(ListModelMixin, ModelView): | ||||
|     """A view which provides default operations for list, against a model in the database.""" | ||||
|     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.""" | ||||
|     pass | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user