diff options
| -rw-r--r-- | djangorestframework/authentication.py | 71 | ||||
| -rw-r--r-- | djangorestframework/mixins.py | 157 | ||||
| -rw-r--r-- | djangorestframework/parsers.py | 75 | ||||
| -rw-r--r-- | djangorestframework/permissions.py | 119 | ||||
| -rw-r--r-- | djangorestframework/renderers.py | 46 | ||||
| -rw-r--r-- | djangorestframework/tests/content.py | 6 | ||||
| -rw-r--r-- | djangorestframework/tests/parsers.py | 11 | ||||
| -rw-r--r-- | djangorestframework/tests/throttling.py | 4 | ||||
| -rw-r--r-- | djangorestframework/utils/mediatypes.py | 42 | ||||
| -rw-r--r-- | djangorestframework/views.py | 22 |
10 files changed, 326 insertions, 227 deletions
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index 9dd5c958..dea19f91 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -1,43 +1,58 @@ -"""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.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. - - 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. + """ + Authenticate the request and return a ``User`` instance or None. (*) - 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 diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 467ce0e0..297d3f8d 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -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) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 6d6bd5ce..da700367 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -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())) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index d98651e0..1b151558 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -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() - -#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 - + """ + Allows access only to authenticated users. + """ -class Throttling(BasePermission): - """Rate throttling of requests on a per-user basis. + def check_permission(self, user): + if not user.is_authenticated(): + raise _403_FORBIDDEN_RESPONSE - The rate is set by a 'throttle' attribute on the view class. +class IsAdminUser(): + """ + Allows access only to admin users. + """ + + 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) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 9e4e2053..78dc26b5 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -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 ) diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py index 6695bf68..e566ea00 100644 --- a/djangorestframework/tests/content.py +++ b/djangorestframework/tests/content.py @@ -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) diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py index 049ac741..88aad880 100644 --- a/djangorestframework/tests/parsers.py +++ b/djangorestframework/tests/parsers.py @@ -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') diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py index 94d01428..e7a054cd 100644 --- a/djangorestframework/tests/throttling.py +++ b/djangorestframework/tests/throttling.py @@ -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): diff --git a/djangorestframework/utils/mediatypes.py b/djangorestframework/utils/mediatypes.py index 92d9264c..3bf914e4 100644 --- a/djangorestframework/utils/mediatypes.py +++ b/djangorestframework/utils/mediatypes.py @@ -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) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index dd30a092..02251885 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -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 |
