Commit 8f58ee48 by Tom Christie

Getting the API into shape

parent d373b3a0
"""The :mod:`authentication` modules provides for pluggable authentication behaviour. """
The ``authentication`` module provides a set of pluggable authentication classes.
Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.BaseView` or Django :class:`View` class. Authentication behavior is provided by adding the ``AuthMixin`` class to a ``View`` .
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 set of authentication methods which are used is then specified by setting
``authentication`` attribute on the ``View`` class, and listing a set of authentication classes.
""" """
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
from djangorestframework.utils import as_tuple from djangorestframework.utils import as_tuple
import base64 import base64
__all__ = (
'BaseAuthenticaton',
'BasicAuthenticaton',
'UserLoggedInAuthenticaton'
)
class BaseAuthenticator(object): class BaseAuthenticaton(object):
"""All authentication should extend BaseAuthenticator.""" """
All authentication classes should extend BaseAuthentication.
"""
def __init__(self, view): def __init__(self, view):
"""Initialise the authentication with the mixin instance as state, """
in case the authentication needs to access any metadata on the mixin object.""" Authentication classes are always passed the current view on creation.
"""
self.view = view self.view = view
def authenticate(self, request): def authenticate(self, request):
"""Authenticate the request and return the authentication context or None. """
Authenticate the request and return a ``User`` instance or None. (*)
An authentication context might be something as simple as a User object, or it might This function must be overridden to be implemented.
be some more complicated token, for example authentication tokens which are signed
against a particular set of permissions for a given user, over a given timeframe.
The default permission checking on View will use the allowed_methods attribute (*) The authentication context _will_ typically be a ``User`` object,
for permissions if the authentication context is not None, and use anon_allowed_methods otherwise. 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) This can be an important distinction if you're implementing some token
by accessing self.auth in order to allow them to apply any more fine grained permission based authentication mechanism, where the authentication context
checking at the point the response is being generated. may be more involved than simply mapping to a ``User``.
"""
This function must be overridden to be implemented."""
return None return None
class BasicAuthenticator(BaseAuthenticator): class BasicAuthenticaton(BaseAuthenticaton):
"""Use HTTP Basic authentication""" """
Use HTTP Basic authentication.
"""
def authenticate(self, request): def authenticate(self, request):
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
...@@ -60,9 +75,13 @@ class BasicAuthenticator(BaseAuthenticator): ...@@ -60,9 +75,13 @@ class BasicAuthenticator(BaseAuthenticator):
return None return None
class UserLoggedInAuthenticator(BaseAuthenticator): class UserLoggedInAuthenticaton(BaseAuthenticaton):
"""Use Django's built-in request session for authentication.""" """
Use Django's session framework for authentication.
"""
def authenticate(self, request): def authenticate(self, request):
# TODO: Switch this back to request.POST, and let MultiPartParser deal with the consequences.
if getattr(request, 'user', None) and request.user.is_active: if getattr(request, 'user', None) and request.user.is_active:
# If this is a POST request we enforce CSRF validation. # If this is a POST request we enforce CSRF validation.
if request.method.upper() == 'POST': if request.method.upper() == 'POST':
...@@ -77,8 +96,4 @@ class UserLoggedInAuthenticator(BaseAuthenticator): ...@@ -77,8 +96,4 @@ class UserLoggedInAuthenticator(BaseAuthenticator):
return None return None
#class DigestAuthentication(BaseAuthentication): # TODO: TokenAuthentication, DigestAuthentication, OAuthAuthentication
# pass
#
#class OAuthAuthentication(BaseAuthentication):
# pass
"""Django supports parsing the content of an HTTP request, but only for form POST requests. """
That behaviour is sufficient for dealing with standard HTML forms, but it doesn't map well Django supports parsing the content of an HTTP request, but only for form POST requests.
That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well
to general HTTP requests. to general HTTP requests.
We need a method to be able to: We need a method to be able to:
...@@ -8,54 +9,72 @@ We need a method to be able to: ...@@ -8,54 +9,72 @@ We need a method to be able to:
2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded 2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
and multipart/form-data. (eg also handle multipart/json) and multipart/form-data. (eg also handle multipart/json)
""" """
from django.http.multipartparser import MultiPartParser as DjangoMPParser
from django.utils import simplejson as json
from djangorestframework.response import ErrorResponse from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.utils import simplejson as json
from djangorestframework import status from djangorestframework import status
from djangorestframework.utils import as_tuple
from djangorestframework.utils.mediatypes import MediaType
from djangorestframework.compat import parse_qs from djangorestframework.compat import parse_qs
from djangorestframework.response import ErrorResponse
from djangorestframework.utils import as_tuple
from djangorestframework.utils.mediatypes import media_type_matches
__all__ = (
'BaseParser',
'JSONParser',
'PlainTextParser',
'FormParser',
'MultiPartParser'
)
class BaseParser(object): class BaseParser(object):
"""All parsers should extend BaseParser, specifying a media_type attribute, """
and overriding the parse() method.""" All parsers should extend BaseParser, specifying a media_type attribute,
and overriding the parse() method.
"""
media_type = None media_type = None
def __init__(self, view): def __init__(self, view):
""" """
Initialise the parser with the View instance as state, Initialize the parser with the ``View`` instance as state,
in case the parser needs to access any metadata on the View object. in case the parser needs to access any metadata on the ``View`` object.
""" """
self.view = view self.view = view
@classmethod def can_handle_request(self, media_type):
def handles(self, media_type):
""" """
Returns `True` if this parser is able to deal with the given MediaType. Returns `True` if this parser is able to deal with the given media type.
The default implementation for this function is to check the ``media_type``
argument against the ``media_type`` attribute set on the class to see if
they match.
This may be overridden to provide for other behavior, but typically you'll
instead want to just set the ``media_type`` attribute on the class.
""" """
return media_type.match(self.media_type) return media_type_matches(media_type, self.media_type)
def parse(self, stream): def parse(self, stream):
"""Given a stream to read from, return the deserialized output. """
The return value may be of any type, but for many parsers it might typically be a dict-like object.""" Given a stream to read from, return the deserialized output.
The return value may be of any type, but for many parsers it might typically be a dict-like object.
"""
raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.") raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.")
class JSONParser(BaseParser): class JSONParser(BaseParser):
media_type = MediaType('application/json') media_type = 'application/json'
def parse(self, stream): def parse(self, stream):
try: try:
return json.load(stream) return json.load(stream)
except ValueError, exc: except ValueError, exc:
raise ErrorResponse(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)}) raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
{'detail': 'JSON parse error - %s' % unicode(exc)})
class DataFlatener(object): class DataFlatener(object):
"""Utility object for flatening dictionaries of lists. Useful for "urlencoded" decoded data.""" """Utility object for flattening dictionaries of lists. Useful for "urlencoded" decoded data."""
def flatten_data(self, data): def flatten_data(self, data):
"""Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary """Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary
...@@ -83,9 +102,9 @@ class PlainTextParser(BaseParser): ...@@ -83,9 +102,9 @@ class PlainTextParser(BaseParser):
""" """
Plain text parser. Plain text parser.
Simply returns the content of the stream Simply returns the content of the stream.
""" """
media_type = MediaType('text/plain') media_type = 'text/plain'
def parse(self, stream): def parse(self, stream):
return stream.read() return stream.read()
...@@ -98,7 +117,7 @@ class FormParser(BaseParser, DataFlatener): ...@@ -98,7 +117,7 @@ class FormParser(BaseParser, DataFlatener):
In order to handle select multiple (and having possibly more than a single value for each parameter), In order to handle select multiple (and having possibly more than a single value for each parameter),
you can customize the output by subclassing the method 'is_a_list'.""" you can customize the output by subclassing the method 'is_a_list'."""
media_type = MediaType('application/x-www-form-urlencoded') media_type = 'application/x-www-form-urlencoded'
"""The value of the parameter when the select multiple is empty. """The value of the parameter when the select multiple is empty.
Browsers are usually stripping the select multiple that have no option selected from the parameters sent. Browsers are usually stripping the select multiple that have no option selected from the parameters sent.
...@@ -138,14 +157,14 @@ class MultipartData(dict): ...@@ -138,14 +157,14 @@ class MultipartData(dict):
dict.__init__(self, data) dict.__init__(self, data)
self.FILES = files self.FILES = files
class MultipartParser(BaseParser, DataFlatener): class MultiPartParser(BaseParser, DataFlatener):
media_type = MediaType('multipart/form-data') media_type = 'multipart/form-data'
RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)
def parse(self, stream): def parse(self, stream):
upload_handlers = self.view.request._get_upload_handlers() upload_handlers = self.view.request._get_upload_handlers()
django_mpp = DjangoMPParser(self.view.request.META, stream, upload_handlers) django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
data, files = django_mpp.parse() data, files = django_parser.parse()
# Flatening data, files and combining them # Flatening data, files and combining them
data = self.flatten_data(dict(data.iterlists())) data = self.flatten_data(dict(data.iterlists()))
......
from django.core.cache import cache from django.core.cache import cache
from djangorestframework import status from djangorestframework import status
from djangorestframework.response import ErrorResponse
import time import time
__all__ = (
'BasePermission',
'FullAnonAccess',
'IsAuthenticated',
'IsAdminUser',
'IsUserOrIsAnonReadOnly',
'PerUserThrottling'
)
_403_FORBIDDEN_RESPONSE = ErrorResponse(
status.HTTP_403_FORBIDDEN,
{'detail': 'You do not have permission to access this resource. ' +
'You may need to login or otherwise authenticate the request.'})
_503_THROTTLED_RESPONSE = ErrorResponse(
status.HTTP_503_SERVICE_UNAVAILABLE,
{'detail': 'request was throttled'})
class BasePermission(object): class BasePermission(object):
"""A base class from which all permission classes should inherit.""" """
A base class from which all permission classes should inherit.
"""
def __init__(self, view): def __init__(self, view):
"""
Permission classes are always passed the current view on creation.
"""
self.view = view self.view = view
def has_permission(self, auth): def check_permission(self, auth):
return True """
Should simply return, or raise an ErrorResponse.
"""
pass
class FullAnonAccess(BasePermission): class FullAnonAccess(BasePermission):
"""""" """
def has_permission(self, auth): Allows full access.
return True """
def check_permission(self, user):
pass
class IsAuthenticated(BasePermission): class IsAuthenticated(BasePermission):
"""""" """
def has_permission(self, auth): Allows access only to authenticated users.
return auth is not None and auth.is_authenticated() """
#class IsUser(BasePermission): def check_permission(self, user):
# """The request has authenticated as a user.""" if not user.is_authenticated():
# def has_permission(self, auth): raise _403_FORBIDDEN_RESPONSE
# pass
# class IsAdminUser():
#class IsAdminUser(): """
# """The request has authenticated as an admin user.""" Allows access only to admin users.
# def has_permission(self, auth): """
# pass
# def check_permission(self, user):
#class IsUserOrIsAnonReadOnly(BasePermission): if not user.is_admin():
# """The request has authenticated as a user, or is a read-only request.""" raise _403_FORBIDDEN_RESPONSE
# def has_permission(self, auth):
# pass
# class IsUserOrIsAnonReadOnly(BasePermission):
#class OAuthTokenInScope(BasePermission): """
# def has_permission(self, auth): The request is authenticated as a user, or is a read-only request.
# pass """
#
#class UserHasModelPermissions(BasePermission): def check_permission(self, user):
# def has_permission(self, auth): if (not user.is_authenticated() and
# pass self.view.method != 'GET' and
self.view.method != 'HEAD'):
raise _403_FORBIDDEN_RESPONSE
class Throttling(BasePermission):
"""Rate throttling of requests on a per-user basis.
class PerUserThrottling(BasePermission):
The rate is set by a 'throttle' attribute on the view class. """
Rate throttling of requests on a per-user basis.
The rate is set by a 'throttle' attribute on the ``View`` class.
The attribute is a two tuple of the form (number of requests, duration in seconds). The attribute is a two tuple of the form (number of requests, duration in seconds).
The user's id will be used as a unique identifier if the user is authenticated. The user id will be used as a unique identifier if the user is authenticated.
For anonymous requests, the IP address of the client will be used. For anonymous requests, the IP address of the client will be used.
Previous request information used for throttling is stored in the cache. Previous request information used for throttling is stored in the cache.
""" """
def has_permission(self, auth):
def check_permission(self, user):
(num_requests, duration) = getattr(self.view, 'throttle', (0, 0)) (num_requests, duration) = getattr(self.view, 'throttle', (0, 0))
if auth.is_authenticated(): if user.is_authenticated():
ident = str(auth) ident = str(auth)
else: else:
ident = self.view.request.META.get('REMOTE_ADDR', None) ident = self.view.request.META.get('REMOTE_ADDR', None)
...@@ -74,7 +111,7 @@ class Throttling(BasePermission): ...@@ -74,7 +111,7 @@ class Throttling(BasePermission):
history.pop() history.pop()
if len(history) >= num_requests: if len(history) >= num_requests:
raise ErrorResponse(status.HTTP_503_SERVICE_UNAVAILABLE, {'detail': 'request was throttled'}) raise _503_THROTTLED_RESPONSE
history.insert(0, now) history.insert(0, now)
cache.set(key, history, duration) cache.set(key, history, duration)
...@@ -29,8 +29,8 @@ class BaseRenderer(object): ...@@ -29,8 +29,8 @@ class BaseRenderer(object):
override the render() function.""" override the render() function."""
media_type = None media_type = None
def __init__(self, resource): def __init__(self, view):
self.resource = resource self.view = view
def render(self, output=None, verbose=False): def render(self, output=None, verbose=False):
"""By default render simply returns the ouput as-is. """By default render simply returns the ouput as-is.
...@@ -42,8 +42,11 @@ class BaseRenderer(object): ...@@ -42,8 +42,11 @@ class BaseRenderer(object):
class TemplateRenderer(BaseRenderer): class TemplateRenderer(BaseRenderer):
"""Provided for convienience. """A Base class provided for convenience.
Render the output by simply rendering it with the given template."""
Render the output simply by using the given template.
To create a template renderer, subclass this, and set
the ``media_type`` and ``template`` attributes"""
media_type = None media_type = None
template = None template = None
...@@ -139,7 +142,7 @@ class DocumentingTemplateRenderer(BaseRenderer): ...@@ -139,7 +142,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
widget=forms.Textarea) widget=forms.Textarea)
# If either of these reserved parameters are turned off then content tunneling is not possible # If either of these reserved parameters are turned off then content tunneling is not possible
if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None: if self.view.CONTENTTYPE_PARAM is None or self.view.CONTENT_PARAM is None:
return None return None
# Okey doke, let's do it # Okey doke, let's do it
...@@ -147,18 +150,18 @@ class DocumentingTemplateRenderer(BaseRenderer): ...@@ -147,18 +150,18 @@ class DocumentingTemplateRenderer(BaseRenderer):
def render(self, output=None): def render(self, output=None):
content = self._get_content(self.resource, self.resource.request, output) content = self._get_content(self.view, self.view.request, output)
form_instance = self._get_form_instance(self.resource) form_instance = self._get_form_instance(self.view)
if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL): if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path)) login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.view.request.path))
logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path)) logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.view.request.path))
else: else:
login_url = None login_url = None
logout_url = None logout_url = None
name = get_name(self.resource) name = get_name(self.view)
description = get_description(self.resource) description = get_description(self.view)
markeddown = None markeddown = None
if apply_markdown: if apply_markdown:
...@@ -167,14 +170,14 @@ class DocumentingTemplateRenderer(BaseRenderer): ...@@ -167,14 +170,14 @@ class DocumentingTemplateRenderer(BaseRenderer):
except AttributeError: # TODO: possibly split the get_description / get_name into a mixin class except AttributeError: # TODO: possibly split the get_description / get_name into a mixin class
markeddown = None markeddown = None
breadcrumb_list = get_breadcrumbs(self.resource.request.path) breadcrumb_list = get_breadcrumbs(self.view.request.path)
template = loader.get_template(self.template) template = loader.get_template(self.template)
context = RequestContext(self.resource.request, { context = RequestContext(self.view.request, {
'content': content, 'content': content,
'resource': self.resource, 'resource': self.view,
'request': self.resource.request, 'request': self.view.request,
'response': self.resource.response, 'response': self.view.response,
'description': description, 'description': description,
'name': name, 'name': name,
'markeddown': markeddown, 'markeddown': markeddown,
...@@ -234,6 +237,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): ...@@ -234,6 +237,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
media_type = 'text/plain' media_type = 'text/plain'
template = 'renderer.txt' template = 'renderer.txt'
DEFAULT_RENDERERS = ( JSONRenderer, DEFAULT_RENDERERS = ( JSONRenderer,
DocumentingHTMLRenderer, DocumentingHTMLRenderer,
DocumentingXHTMLRenderer, DocumentingXHTMLRenderer,
......
...@@ -4,7 +4,7 @@ Tests for content parsing, and form-overloaded content parsing. ...@@ -4,7 +4,7 @@ Tests for content parsing, and form-overloaded content parsing.
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.mixins import RequestMixin from djangorestframework.mixins import RequestMixin
from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser from djangorestframework.parsers import FormParser, MultiPartParser, PlainTextParser
class TestContentParsing(TestCase): class TestContentParsing(TestCase):
...@@ -19,7 +19,7 @@ class TestContentParsing(TestCase): ...@@ -19,7 +19,7 @@ class TestContentParsing(TestCase):
def ensure_determines_form_content_POST(self, view): def ensure_determines_form_content_POST(self, view):
"""Ensure view.RAW_CONTENT returns content for POST request with form content.""" """Ensure view.RAW_CONTENT returns content for POST request with form content."""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
view.parsers = (FormParser, MultipartParser) view.parsers = (FormParser, MultiPartParser)
view.request = self.req.post('/', data=form_data) view.request = self.req.post('/', data=form_data)
self.assertEqual(view.RAW_CONTENT, form_data) self.assertEqual(view.RAW_CONTENT, form_data)
...@@ -34,7 +34,7 @@ class TestContentParsing(TestCase): ...@@ -34,7 +34,7 @@ class TestContentParsing(TestCase):
def ensure_determines_form_content_PUT(self, view): def ensure_determines_form_content_PUT(self, view):
"""Ensure view.RAW_CONTENT returns content for PUT request with form content.""" """Ensure view.RAW_CONTENT returns content for PUT request with form content."""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
view.parsers = (FormParser, MultipartParser) view.parsers = (FormParser, MultiPartParser)
view.request = self.req.put('/', data=form_data) view.request = self.req.put('/', data=form_data)
self.assertEqual(view.RAW_CONTENT, form_data) self.assertEqual(view.RAW_CONTENT, form_data)
......
...@@ -39,7 +39,7 @@ This new parser only flattens the lists of parameters that contain a single valu ...@@ -39,7 +39,7 @@ This new parser only flattens the lists of parameters that contain a single valu
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
True True
.. note:: The same functionality is available for :class:`parsers.MultipartParser`. .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
Submitting an empty list Submitting an empty list
-------------------------- --------------------------
...@@ -80,9 +80,8 @@ import httplib, mimetypes ...@@ -80,9 +80,8 @@ import httplib, mimetypes
from tempfile import TemporaryFile from tempfile import TemporaryFile
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.parsers import MultipartParser from djangorestframework.parsers import MultiPartParser
from djangorestframework.views import BaseView from djangorestframework.views import BaseView
from djangorestframework.utils.mediatypes import MediaType
from StringIO import StringIO from StringIO import StringIO
def encode_multipart_formdata(fields, files): def encode_multipart_formdata(fields, files):
...@@ -113,18 +112,18 @@ def encode_multipart_formdata(fields, files): ...@@ -113,18 +112,18 @@ def encode_multipart_formdata(fields, files):
def get_content_type(filename): def get_content_type(filename):
return mimetypes.guess_type(filename)[0] or 'application/octet-stream' return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
class TestMultipartParser(TestCase): class TestMultiPartParser(TestCase):
def setUp(self): def setUp(self):
self.req = RequestFactory() self.req = RequestFactory()
self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')],
[('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')])
def test_multipartparser(self): def test_multipartparser(self):
"""Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters.""" """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters."""
post_req = RequestFactory().post('/', self.body, content_type=self.content_type) post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
view = BaseView() view = BaseView()
view.request = post_req view.request = post_req
parsed = MultipartParser(view).parse(StringIO(self.body)) parsed = MultiPartParser(view).parse(StringIO(self.body))
self.assertEqual(parsed['key1'], 'val1') self.assertEqual(parsed['key1'], 'val1')
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla') self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
...@@ -4,11 +4,11 @@ from django.utils import simplejson as json ...@@ -4,11 +4,11 @@ from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.views import BaseView from djangorestframework.views import BaseView
from djangorestframework.permissions import Throttling from djangorestframework.permissions import PerUserThrottling
class MockView(BaseView): class MockView(BaseView):
permissions = ( Throttling, ) permissions = ( PerUserThrottling, )
throttle = (3, 1) # 3 requests per second throttle = (3, 1) # 3 requests per second
def get(self, request): def get(self, request):
......
...@@ -7,11 +7,39 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 ...@@ -7,11 +7,39 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
class MediaType(object): def media_type_matches(lhs, rhs):
"""
Returns ``True`` if the media type in the first argument <= the
media type in the second argument. The media types are strings
as described by the HTTP spec.
Valid media type strings include:
'application/json indent=4'
'application/json'
'text/*'
'*/*'
"""
lhs = _MediaType(lhs)
rhs = _MediaType(rhs)
return lhs.match(rhs)
def is_form_media_type(media_type):
"""
Return True if the media type is a valid form media type as defined by the HTML4 spec.
(NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
"""
media_type = _MediaType(media_type)
return media_type.full_type == 'application/x-www-form-urlencoded' or \
media_type.full_type == 'multipart/form-data'
class _MediaType(object):
def __init__(self, media_type_str): def __init__(self, media_type_str):
self.orig = media_type_str self.orig = media_type_str
self.media_type, self.params = parse_header(media_type_str) self.full_type, self.params = parse_header(media_type_str)
self.main_type, sep, self.sub_type = self.media_type.partition('/') self.main_type, sep, self.sub_type = self.full_type.partition('/')
def match(self, other): def match(self, other):
"""Return true if this MediaType satisfies the constraint of the given MediaType.""" """Return true if this MediaType satisfies the constraint of the given MediaType."""
...@@ -56,14 +84,6 @@ class MediaType(object): ...@@ -56,14 +84,6 @@ class MediaType(object):
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
return self.quality * 10000 + self.precedence return self.quality * 10000 + self.precedence
def is_form(self):
"""
Return True if the MediaType is a valid form media type as defined by the HTML4 spec.
(NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
"""
return self.media_type == 'application/x-www-form-urlencoded' or \
self.media_type == 'multipart/form-data'
def as_tuple(self): def as_tuple(self):
return (self.main_type, self.sub_type, self.params) return (self.main_type, self.sub_type, self.params)
......
...@@ -7,11 +7,11 @@ from djangorestframework.mixins import * ...@@ -7,11 +7,11 @@ from djangorestframework.mixins import *
from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status
__all__ = ['BaseView', __all__ = ('BaseView',
'ModelView', 'ModelView',
'InstanceModelView', 'InstanceModelView',
'ListOrModelView', 'ListOrModelView',
'ListOrCreateModelView'] 'ListOrCreateModelView')
...@@ -32,14 +32,14 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): ...@@ -32,14 +32,14 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
# List of parsers the resource can parse the request with. # List of parsers the resource can parse the request with.
parsers = ( parsers.JSONParser, parsers = ( parsers.JSONParser,
parsers.FormParser, parsers.FormParser,
parsers.MultipartParser ) parsers.MultiPartParser )
# List of validators to validate, cleanup and normalize the request content # List of validators to validate, cleanup and normalize the request content
validators = ( validators.FormValidator, ) validators = ( validators.FormValidator, )
# List of all authenticating methods to attempt. # List of all authenticating methods to attempt.
authentication = ( authentication.UserLoggedInAuthenticator, authentication = ( authentication.UserLoggedInAuthenticaton,
authentication.BasicAuthenticator ) authentication.BasicAuthenticaton )
# List of all permissions that must be checked. # List of all permissions that must be checked.
permissions = ( permissions.FullAnonAccess, ) permissions = ( permissions.FullAnonAccess, )
...@@ -92,7 +92,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): ...@@ -92,7 +92,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
self.perform_form_overloading() self.perform_form_overloading()
# Authenticate and check request is has the relevant permissions # Authenticate and check request is has the relevant permissions
self.check_permissions() self._check_permissions()
# Get the appropriate handler method # Get the appropriate handler method
if self.method.lower() in self.http_method_names: if self.method.lower() in self.http_method_names:
...@@ -115,6 +115,9 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): ...@@ -115,6 +115,9 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
except ErrorResponse, exc: except ErrorResponse, exc:
response = exc.response response = exc.response
except:
import traceback
traceback.print_exc()
# Always add these headers. # Always add these headers.
# #
...@@ -126,6 +129,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): ...@@ -126,6 +129,7 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
return self.render(response) return self.render(response)
class ModelView(BaseView): class ModelView(BaseView):
"""A RESTful view that maps to a model in the database.""" """A RESTful view that maps to a model in the database."""
validators = (validators.ModelFormValidator,) validators = (validators.ModelFormValidator,)
...@@ -134,11 +138,11 @@ class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, Mode ...@@ -134,11 +138,11 @@ class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, Mode
"""A view which provides default operations for read/update/delete against a model instance.""" """A view which provides default operations for read/update/delete against a model instance."""
pass pass
class ListModelResource(ListModelMixin, ModelView): class ListModelView(ListModelMixin, ModelView):
"""A view which provides default operations for list, against a model in the database.""" """A view which provides default operations for list, against a model in the database."""
pass pass
class ListOrCreateModelResource(ListModelMixin, CreateModelMixin, ModelView): class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
"""A view which provides default operations for list and create, against a model in the database.""" """A view which provides default operations for list and create, against a model in the database."""
pass pass
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment