Getting the API into shape

This commit is contained in:
Tom Christie 2011-05-10 10:49:28 +01:00
parent d373b3a067
commit 8f58ee489d
10 changed files with 327 additions and 228 deletions

View File

@ -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

View File

@ -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)

View File

@ -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()))

View File

@ -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)

View File

@ -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 )

View File

@ -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)

View File

@ -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')

View File

@ -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):

View File

@ -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)

View File

@ -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