mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-11-04 01:47:59 +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.
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
        
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
            parser = parser_cls(self)
 | 
			
		||||
            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)
 | 
			
		||||
        raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
 | 
			
		||||
                            {'error': 'Unsupported media type in request \'%s\'.' %
 | 
			
		||||
                            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 Throttling(BasePermission):
 | 
			
		||||
    """Rate throttling of requests on a per-user basis.
 | 
			
		||||
class IsAdminUser():
 | 
			
		||||
    """
 | 
			
		||||
    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 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)        
 | 
			
		||||
        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,
 | 
			
		||||
| 
						 | 
				
			
			@ -233,11 +236,12 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
 | 
			
		|||
    Useful for browsing an API with command line tools."""
 | 
			
		||||
    media_type = 'text/plain'
 | 
			
		||||
    template = 'renderer.txt'
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEFAULT_RENDERERS = ( JSONRenderer,
 | 
			
		||||
                     DocumentingHTMLRenderer,
 | 
			
		||||
                     DocumentingXHTMLRenderer,
 | 
			
		||||
                     DocumentingPlainTextRenderer,
 | 
			
		||||
                     XMLRenderer )
 | 
			
		||||
                      DocumentingHTMLRenderer,
 | 
			
		||||
                      DocumentingXHTMLRenderer,
 | 
			
		||||
                      DocumentingPlainTextRenderer,
 | 
			
		||||
                      XMLRenderer )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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."""
 | 
			
		||||
| 
						 | 
				
			
			@ -55,14 +83,6 @@ class MediaType(object):
 | 
			
		|||
        # NB. quality values should only have up to 3 decimal points
 | 
			
		||||
        # 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:
 | 
			
		||||
| 
						 | 
				
			
			@ -112,9 +112,12 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
 | 
			
		|||
 | 
			
		||||
            # Pre-serialize filtering (eg filter complex objects into natively serializable types)
 | 
			
		||||
            response.cleaned_content = self.resource.object_to_serializable(response.raw_content)
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
        except ErrorResponse, exc:
 | 
			
		||||
            response = exc.response
 | 
			
		||||
        except:
 | 
			
		||||
            import traceback
 | 
			
		||||
            traceback.print_exc()
 | 
			
		||||
 | 
			
		||||
        # Always add these headers.
 | 
			
		||||
        #
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +127,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
 | 
			
		|||
        response.headers['Vary'] = 'Authenticate, Accept'
 | 
			
		||||
 | 
			
		||||
        return self.render(response)
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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."""
 | 
			
		||||
    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