mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-11 04:07:39 +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