aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--djangorestframework/authenticators.py37
-rw-r--r--djangorestframework/compat.py10
-rw-r--r--djangorestframework/emitters.py53
-rw-r--r--djangorestframework/mediatypes.py2
-rw-r--r--djangorestframework/mixins.py439
-rw-r--r--djangorestframework/modelresource.py60
-rw-r--r--djangorestframework/parsers.py22
-rw-r--r--djangorestframework/permissions.py74
-rw-r--r--djangorestframework/request.py156
-rw-r--r--djangorestframework/resource.py137
-rw-r--r--djangorestframework/response.py31
-rw-r--r--djangorestframework/tests/accept.py7
-rw-r--r--djangorestframework/tests/authentication.py16
-rw-r--r--djangorestframework/tests/content.py199
-rw-r--r--djangorestframework/tests/emitters.py5
-rw-r--r--djangorestframework/tests/files.py9
-rw-r--r--djangorestframework/tests/methods.py80
-rw-r--r--djangorestframework/tests/reverse.py14
-rw-r--r--djangorestframework/tests/throttling.py38
-rw-r--r--djangorestframework/tests/validators.py167
-rw-r--r--djangorestframework/utils.py1
-rw-r--r--djangorestframework/validators.py103
-rw-r--r--examples/blogpost/tests.py7
-rw-r--r--examples/blogpost/views.py4
-rw-r--r--examples/mixin/urls.py5
-rw-r--r--examples/modelresourceexample/views.py2
-rw-r--r--examples/pygments_api/tests.py9
-rw-r--r--examples/pygments_api/views.py24
-rw-r--r--examples/resourceexample/views.py10
-rw-r--r--examples/sandbox/views.py3
30 files changed, 1032 insertions, 692 deletions
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
index 82d19779..29fbb818 100644
--- a/djangorestframework/authenticators.py
+++ b/djangorestframework/authenticators.py
@@ -10,33 +10,13 @@ from djangorestframework.utils import as_tuple
import base64
-class AuthenticatorMixin(object):
- """Adds pluggable authentication behaviour."""
-
- """The set of authenticators to use."""
- authenticators = None
-
- def authenticate(self, request):
- """Attempt to authenticate the request, returning an authentication context or None.
- An authentication context may be any object, although in many cases it will simply be a :class:`User` instance."""
-
- # Attempt authentication against each authenticator in turn,
- # and return None if no authenticators succeed in authenticating the request.
- for authenticator in as_tuple(self.authenticators):
- auth_context = authenticator(self).authenticate(request)
- if auth_context:
- return auth_context
-
- return None
-
-
class BaseAuthenticator(object):
"""All authenticators should extend BaseAuthenticator."""
- def __init__(self, mixin):
+ def __init__(self, view):
"""Initialise the authenticator with the mixin instance as state,
in case the authenticator needs to access any metadata on the mixin object."""
- self.mixin = mixin
+ self.view = view
def authenticate(self, request):
"""Authenticate the request and return the authentication context or None.
@@ -48,8 +28,9 @@ class BaseAuthenticator(object):
The default permission checking on Resource 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 passed to the method calls eg Resource.get(request, auth) in order to
- allow them to apply any more fine grained permission checking at the point the response is being generated.
+ The authentication context is available to the method calls eg Resource.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."""
return None
@@ -94,4 +75,10 @@ class UserLoggedInAuthenticator(BaseAuthenticator):
return None
return request.user
return None
-
+
+
+#class DigestAuthentication(BaseAuthentication):
+# pass
+#
+#class OAuthAuthentication(BaseAuthentication):
+# pass
diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py
index 3e82bd98..22b57186 100644
--- a/djangorestframework/compat.py
+++ b/djangorestframework/compat.py
@@ -125,4 +125,12 @@ except:
# 'request': self.request
# }
#)
- return http.HttpResponseNotAllowed(allowed_methods) \ No newline at end of file
+ return http.HttpResponseNotAllowed(allowed_methods)
+
+# parse_qs
+try:
+ # python >= ?
+ from urlparse import parse_qs
+except ImportError:
+ # python <= ?
+ from cgi import parse_qs \ No newline at end of file
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index 60a4b5dc..0adddca9 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -5,12 +5,14 @@ and providing forms and links depending on the allowed methods, emitters and par
"""
from django import forms
from django.conf import settings
-from django.http import HttpResponse
from django.template import RequestContext, loader
from django.utils import simplejson as json
+<<<<<<< local
+from django import forms
+=======
+>>>>>>> other
-from djangorestframework.response import NoContent, ResponseException
-from djangorestframework.validators import FormValidatorMixin
+from djangorestframework.response import ErrorResponse
from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.markdownwrapper import apply_markdown
from djangorestframework.breadcrumbs import get_breadcrumbs
@@ -20,6 +22,8 @@ from djangorestframework import status
from urllib import quote_plus
import string
import re
+<<<<<<< local
+=======
from decimal import Decimal
@@ -144,11 +148,12 @@ class EmitterMixin(object):
"""Return the resource's most prefered emitter.
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
return self.emitters[0]
+>>>>>>> other
# TODO: Rename verbose to something more appropriate
-# TODO: NoContent could be handled more cleanly. It'd be nice if it was handled by default,
+# TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
# and only have an emitter output anything if it explicitly provides support for that.
class BaseEmitter(object):
@@ -159,10 +164,10 @@ class BaseEmitter(object):
def __init__(self, resource):
self.resource = resource
- def emit(self, output=NoContent, verbose=False):
+ def emit(self, output=None, verbose=False):
"""By default emit simply returns the ouput as-is.
Override this method to provide for other behaviour."""
- if output is NoContent:
+ if output is None:
return ''
return output
@@ -174,8 +179,8 @@ class TemplateEmitter(BaseEmitter):
media_type = None
template = None
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
+ def emit(self, output=None, verbose=False):
+ if output is None:
return ''
context = RequestContext(self.request, output)
@@ -213,15 +218,11 @@ class DocumentingTemplateEmitter(BaseEmitter):
#form_instance = resource.form_instance
# TODO! Reinstate this
- form_instance = None
+ form_instance = getattr(resource, 'bound_form_instance', None)
- if isinstance(resource, FormValidatorMixin):
- # If we already have a bound form instance (IE provided by the input parser, then use that)
- if resource.bound_form_instance is not None:
- form_instance = resource.bound_form_instance
-
+ if not form_instance and hasattr(resource, 'get_bound_form'):
# Otherwise if we have a response that is valid against the form then use that
- if not form_instance and resource.response.has_content_body:
+ if resource.response.has_content_body:
try:
form_instance = resource.get_bound_form(resource.response.cleaned_content)
if form_instance and not form_instance.is_valid():
@@ -229,12 +230,12 @@ class DocumentingTemplateEmitter(BaseEmitter):
except:
form_instance = None
- # If we still don't have a form instance then try to get an unbound form
- if not form_instance:
- try:
- form_instance = resource.get_bound_form()
- except:
- pass
+ # If we still don't have a form instance then try to get an unbound form
+ if not form_instance:
+ try:
+ form_instance = resource.get_bound_form()
+ except:
+ pass
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
if not form_instance:
@@ -277,7 +278,7 @@ class DocumentingTemplateEmitter(BaseEmitter):
return GenericContentForm(resource)
- def emit(self, output=NoContent):
+ def emit(self, output=None):
content = self._get_content(self.resource, self.resource.request, output)
form_instance = self._get_form_instance(self.resource)
@@ -325,8 +326,8 @@ class JSONEmitter(BaseEmitter):
"""Emitter which serializes to JSON"""
media_type = 'application/json'
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
+ def emit(self, output=None, verbose=False):
+ if output is None:
return ''
if verbose:
return json.dumps(output, indent=4, sort_keys=True)
@@ -337,8 +338,8 @@ class XMLEmitter(BaseEmitter):
"""Emitter which serializes to XML."""
media_type = 'application/xml'
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
+ def emit(self, output=None, verbose=False):
+ if output is None:
return ''
return dict2xml(output)
diff --git a/djangorestframework/mediatypes.py b/djangorestframework/mediatypes.py
index d1641a8f..92d9264c 100644
--- a/djangorestframework/mediatypes.py
+++ b/djangorestframework/mediatypes.py
@@ -63,7 +63,7 @@ class MediaType(object):
"""
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/mixins.py b/djangorestframework/mixins.py
new file mode 100644
index 00000000..53262366
--- /dev/null
+++ b/djangorestframework/mixins.py
@@ -0,0 +1,439 @@
+from djangorestframework.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.http import HttpResponse
+from django.http.multipartparser import LimitBytes # TODO: Use LimitedStream in compat
+from StringIO import StringIO
+from decimal import Decimal
+import re
+
+
+
+########## Request Mixin ##########
+
+class RequestMixin(object):
+ """Mixin class to provide request parsing behaviour."""
+
+ USE_FORM_OVERLOADING = True
+ METHOD_PARAM = "_method"
+ CONTENTTYPE_PARAM = "_content_type"
+ CONTENT_PARAM = "_content"
+
+ parsers = ()
+ validators = ()
+
+ def _get_method(self):
+ """
+ Returns the HTTP method for the current view.
+ """
+ if not hasattr(self, '_method'):
+ self._method = self.request.method
+ return self._method
+
+
+ def _set_method(self, method):
+ """
+ Set the method for the current view.
+ """
+ self._method = method
+
+
+ def _get_content_type(self):
+ """
+ Returns a MediaType object, representing the request's 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
+ return self._content_type
+
+
+ def _set_content_type(self, content_type):
+ """
+ Set the content type. Should be a MediaType object.
+ """
+ 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.
+ """
+ if not hasattr(self, '_stream'):
+ request = self.request
+
+ try:
+ content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH')))
+ except (ValueError, TypeError):
+ content_length = 0
+
+ # Currently only supports parsing request body as a stream with 1.3
+ if content_length == 0:
+ return None
+ elif hasattr(request, 'read'):
+ # It's not at all clear if this needs to be byte limited or not.
+ # Maybe I'm just being dumb but it looks to me like there's some issues
+ # with that in Django.
+ #
+ # Either:
+ # 1. It *can't* be treated as a limited byte stream, and you _do_ need to
+ # respect CONTENT_LENGTH, in which case that ought to be documented,
+ # and there probably ought to be a feature request for it to be
+ # 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.
+ #
+ # It's an issue because it affects if you can pass a request off to code that
+ # does something like:
+ #
+ # while stream.read(BUFFER_SIZE):
+ # [do stuff]
+ #
+ #try:
+ # content_length = int(request.META.get('CONTENT_LENGTH',0))
+ #except (ValueError, TypeError):
+ # content_length = 0
+ # self._stream = LimitedStream(request, content_length)
+ self._stream = request
+ else:
+ self._stream = StringIO(request.raw_post_data)
+ return self._stream
+
+
+ def _set_stream(self, stream):
+ """
+ Set the stream representing the request body.
+ """
+ self._stream = stream
+
+
+ def _get_raw_content(self):
+ """
+ Returns the parsed content of the request
+ """
+ if not hasattr(self, '_raw_content'):
+ self._raw_content = self.parse(self.stream, self.content_type)
+ return self._raw_content
+
+
+ def _get_content(self):
+ """
+ Returns the parsed and validated content of the request
+ """
+ if not hasattr(self, '_content'):
+ self._content = self.validate(self.RAW_CONTENT)
+
+ return self._content
+
+
+ def perform_form_overloading(self):
+ """
+ Check the request to see if it is using form POST '_method'/'_content'/'_content_type' overrides.
+ 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():
+ return
+
+ # Temporarily switch to using the form parsers, then parse the content
+ parsers = self.parsers
+ self.parsers = (FormParser, MultipartParser)
+ content = self.RAW_CONTENT
+ self.parsers = parsers
+
+ # Method overloading - change the method and remove the param from the content
+ if self.METHOD_PARAM in content:
+ self.method = content[self.METHOD_PARAM].upper()
+ del self._raw_content[self.METHOD_PARAM]
+
+ # 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._stream = StringIO(content[self.CONTENT_PARAM])
+ del(self._raw_content)
+
+
+ def parse(self, stream, content_type):
+ """
+ Parse the request content.
+
+ May raise a 415 ErrorResponse (Unsupported Media Type),
+ or a 400 ErrorResponse (Bad Request).
+ """
+ if stream is None or content_type is None:
+ return None
+
+ parsers = as_tuple(self.parsers)
+
+ parser = None
+ for parser_cls in parsers:
+ if parser_cls.handles(content_type):
+ parser = parser_cls(self)
+ break
+
+ if parser 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)
+
+
+ def validate(self, content):
+ """
+ Validate, cleanup, and type-ify the request content.
+ """
+ for validator_cls in self.validators:
+ validator = validator_cls(self)
+ content = validator.validate(content)
+ return content
+
+
+ def get_bound_form(self, content=None):
+ """
+ Return a bound form instance for the given content,
+ if there is an appropriate form validator attached to the view.
+ """
+ for validator_cls in self.validators:
+ if hasattr(validator_cls, 'get_bound_form'):
+ validator = validator_cls(self)
+ return validator.get_bound_form(content)
+ return None
+
+
+ @property
+ def parsed_media_types(self):
+ """Return an list of all the media types that this view can parse."""
+ return [parser.media_type for parser in self.parsers]
+
+
+ @property
+ def default_parser(self):
+ """Return the view's most preffered emitter.
+ (This has no behavioural effect, but is may be used by documenting emitters)"""
+ return self.parsers[0]
+
+
+ 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)
+
+
+########## ResponseMixin ##########
+
+class ResponseMixin(object):
+ """Adds behaviour for pluggable Emitters to a :class:`.Resource` 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."""
+
+ ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
+ REWRITE_IE_ACCEPT_HEADER = True
+
+ #request = None
+ #response = None
+ emitters = ()
+
+ #def render_to_response(self, obj):
+ # if isinstance(obj, Response):
+ # response = obj
+ # elif response_obj is not None:
+ # response = Response(status.HTTP_200_OK, obj)
+ # else:
+ # response = Response(status.HTTP_204_NO_CONTENT)
+
+ # response.cleaned_content = self._filter(response.raw_content)
+
+ # self._render(response)
+
+
+ #def filter(self, content):
+ # """
+ # Filter the response content.
+ # """
+ # for filterer_cls in self.filterers:
+ # filterer = filterer_cls(self)
+ # content = filterer.filter(content)
+ # return content
+
+
+ def emit(self, response):
+ """Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
+ self.response = response
+
+ try:
+ emitter = self._determine_emitter(self.request)
+ except ErrorResponse, exc:
+ emitter = self.default_emitter
+ response = exc.response
+
+ # Serialize the response content
+ if response.has_content_body:
+ content = emitter(self).emit(output=response.cleaned_content)
+ else:
+ content = emitter(self).emit()
+
+ # Munge DELETE Response code to allow us to return content
+ # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
+ if response.status == 204:
+ response.status = 200
+
+ # Build the HTTP Response
+ # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
+ resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
+ for (key, val) in response.headers.items():
+ resp[key] = val
+
+ return resp
+
+
+ def _determine_emitter(self, request):
+ """Return the appropriate emitter for the output, given the client's 'Accept' header,
+ and the content types that this Resource knows how to serve.
+
+ See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
+
+ if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
+ # Use _accept parameter override
+ accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
+ elif (self.REWRITE_IE_ACCEPT_HEADER and
+ request.META.has_key('HTTP_USER_AGENT') and
+ MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
+ accept_list = ['text/html', '*/*']
+ elif request.META.has_key('HTTP_ACCEPT'):
+ # Use standard HTTP Accept negotiation
+ accept_list = request.META["HTTP_ACCEPT"].split(',')
+ else:
+ # No accept header specified
+ return self.default_emitter
+
+ # Parse the accept header into a dict of {qvalue: set of media types}
+ # We ignore mietype parameters
+ accept_dict = {}
+ for token in accept_list:
+ components = token.split(';')
+ mimetype = components[0].strip()
+ qvalue = Decimal('1.0')
+
+ if len(components) > 1:
+ # Parse items that have a qvalue eg text/html;q=0.9
+ try:
+ (q, num) = components[-1].split('=')
+ if q == 'q':
+ qvalue = Decimal(num)
+ except:
+ # Skip malformed entries
+ continue
+
+ if accept_dict.has_key(qvalue):
+ accept_dict[qvalue].add(mimetype)
+ else:
+ accept_dict[qvalue] = set((mimetype,))
+
+ # Convert to a list of sets ordered by qvalue (highest first)
+ accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
+
+ for accept_set in accept_sets:
+ # Return any exact match
+ for emitter in self.emitters:
+ if emitter.media_type in accept_set:
+ return emitter
+
+ # Return any subtype match
+ for emitter in self.emitters:
+ if emitter.media_type.split('/')[0] + '/*' in accept_set:
+ return emitter
+
+ # Return default
+ if '*/*' in accept_set:
+ return self.default_emitter
+
+
+ raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
+ {'detail': 'Could not satisfy the client\'s Accept header',
+ 'available_types': self.emitted_media_types})
+
+ @property
+ def emitted_media_types(self):
+ """Return an list of all the media types that this resource can emit."""
+ return [emitter.media_type for emitter in self.emitters]
+
+ @property
+ def default_emitter(self):
+ """Return the resource's most prefered emitter.
+ (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
+ return self.emitters[0]
+
+
+########## Auth Mixin ##########
+
+class AuthMixin(object):
+ """Mixin class to provide authentication and permission checking."""
+ authenticators = ()
+ permissions = ()
+
+ @property
+ def auth(self):
+ if not hasattr(self, '_auth'):
+ self._auth = self._authenticate()
+ return self._auth
+
+ def _authenticate(self):
+ for authenticator_cls in self.authenticators:
+ authenticator = authenticator_cls(self)
+ auth = authenticator.authenticate(self.request)
+ if auth:
+ return auth
+ return None
+
+ # TODO?
+ #@property
+ #def user(self):
+ # if not has_attr(self, '_user'):
+ # auth = self.auth
+ # if isinstance(auth, User...):
+ # self._user = auth
+ # else:
+ # self._user = getattr(auth, 'user', None)
+ # return self._user
+
+ def check_permissions(self):
+ if not self.permissions:
+ return
+
+ 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.'})
+
+
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index 55a15d6a..1afd7fa0 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -3,17 +3,16 @@ from django.db.models import Model
from django.db.models.query import QuerySet
from django.db.models.fields.related import RelatedField
-from djangorestframework.response import Response, ResponseException
+from djangorestframework.response import Response, ErrorResponse
from djangorestframework.resource import Resource
-from djangorestframework.validators import ModelFormValidatorMixin
-from djangorestframework import status
+from djangorestframework import status, validators
import decimal
import inspect
import re
-class ModelResource(Resource, ModelFormValidatorMixin):
+class ModelResource(Resource):
"""A specialized type of Resource, for resources that map directly to a Django Model.
Useful things this provides:
@@ -21,6 +20,9 @@ class ModelResource(Resource, ModelFormValidatorMixin):
1. Nice serialization of returned Models and QuerySets.
2. A default set of create/read/update/delete operations."""
+ # List of validators to validate, cleanup and type-ify the request content
+ validators = (validators.ModelFormValidator,)
+
# The model attribute refers to the Django Model which this Resource maps to.
# (The Model's class, rather than an instance of the Model)
model = None
@@ -339,7 +341,20 @@ class ModelResource(Resource, ModelFormValidatorMixin):
return _any(data, self.fields)
- def post(self, request, auth, content, *args, **kwargs):
+ def get(self, request, *args, **kwargs):
+ try:
+ if args:
+ # If we have any none kwargs then assume the last represents the primrary key
+ instance = self.model.objects.get(pk=args[-1], **kwargs)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+ except self.model.DoesNotExist:
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND)
+
+ return instance
+
+ def post(self, request, *args, **kwargs):
# TODO: test creation on a non-existing resource url
# translated related_field into related_field_id
@@ -348,7 +363,7 @@ class ModelResource(Resource, ModelFormValidatorMixin):
kwargs[related_name + '_id'] = kwargs[related_name]
del kwargs[related_name]
- all_kw_args = dict(content.items() + kwargs.items())
+ all_kw_args = dict(self.CONTENT.items() + kwargs.items())
if args:
instance = self.model(pk=args[-1], **all_kw_args)
else:
@@ -359,20 +374,7 @@ class ModelResource(Resource, ModelFormValidatorMixin):
headers['Location'] = instance.get_absolute_url()
return Response(status.HTTP_201_CREATED, instance, headers)
- def get(self, request, auth, *args, **kwargs):
- try:
- if args:
- # If we have any none kwargs then assume the last represents the primrary key
- instance = self.model.objects.get(pk=args[-1], **kwargs)
- else:
- # Otherwise assume the kwargs uniquely identify the model
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
- raise ResponseException(status.HTTP_404_NOT_FOUND)
-
- return instance
-
- def put(self, request, auth, content, *args, **kwargs):
+ def put(self, request, *args, **kwargs):
# 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
try:
if args:
@@ -382,16 +384,16 @@ class ModelResource(Resource, ModelFormValidatorMixin):
# Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs)
- for (key, val) in content.items():
+ for (key, val) in self.CONTENT.items():
setattr(instance, key, val)
except self.model.DoesNotExist:
- instance = self.model(**content)
+ instance = self.model(**self.CONTENT)
instance.save()
instance.save()
return instance
- def delete(self, request, auth, *args, **kwargs):
+ def delete(self, request, *args, **kwargs):
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
@@ -400,7 +402,7 @@ class ModelResource(Resource, ModelFormValidatorMixin):
# Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist:
- raise ResponseException(status.HTTP_404_NOT_FOUND, None, {})
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
instance.delete()
return
@@ -408,13 +410,13 @@ class ModelResource(Resource, ModelFormValidatorMixin):
class RootModelResource(ModelResource):
"""A Resource which provides default operations for list and create."""
- allowed_methods = ('GET', 'POST')
queryset = None
- def get(self, request, auth, *args, **kwargs):
+ def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filter(**kwargs)
+ put = delete = None
class QueryModelResource(ModelResource):
"""Resource with default operations for list.
@@ -422,10 +424,8 @@ class QueryModelResource(ModelResource):
allowed_methods = ('GET',)
queryset = None
- def get_form(self, data=None):
- return None
-
- def get(self, request, auth, *args, **kwargs):
+ def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filer(**kwargs)
+ post = put = delete = None \ No newline at end of file
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 35003a0f..03f8bf8f 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -11,11 +11,18 @@ We need a method to be able to:
from django.http.multipartparser import MultiPartParser as DjangoMPParser
from django.utils import simplejson as json
+<<<<<<< local
+from djangorestframework.response import ErrorResponse
+=======
from djangorestframework.response import ResponseException
+>>>>>>> other
from djangorestframework import status
from djangorestframework.utils import as_tuple
from djangorestframework.mediatypes import MediaType
+from djangorestframework.compat import parse_qs
+<<<<<<< local
+=======
try:
from urlparse import parse_qs
except ImportError:
@@ -56,6 +63,7 @@ class ParserMixin(object):
"""Return the ParerMixin's most prefered emitter.
(This has no behavioural effect, but is may be used by documenting emitters)"""
return self.parsers[0]
+>>>>>>> other
class BaseParser(object):
@@ -91,7 +99,7 @@ class JSONParser(BaseParser):
try:
return json.load(stream)
except ValueError, exc:
- raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
+ raise ErrorResponse(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
class DataFlatener(object):
@@ -119,6 +127,18 @@ class DataFlatener(object):
return False
+class PlainTextParser(BaseParser):
+ """
+ Plain text parser.
+
+ Simply returns the content of the stream
+ """
+ media_type = MediaType('text/plain')
+
+ def parse(self, stream):
+ return stream.read()
+
+
class FormParser(BaseParser, DataFlatener):
"""The default parser for form data.
Return a dict containing a single value for each non-reserved parameter.
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
new file mode 100644
index 00000000..98d4b0be
--- /dev/null
+++ b/djangorestframework/permissions.py
@@ -0,0 +1,74 @@
+from django.core.cache import cache
+from djangorestframework import status
+import time
+
+
+class BasePermission(object):
+ """A base class from which all permission classes should inherit."""
+ def __init__(self, view):
+ self.view = view
+
+ def has_permission(self, auth):
+ return True
+
+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
+
+
+class Throttling(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.
+ 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):
+ (num_requests, duration) = getattr(self.view, 'throttle', (0, 0))
+
+ if auth.is_authenticated():
+ ident = str(auth)
+ else:
+ ident = self.view.request.META.get('REMOTE_ADDR', None)
+
+ key = 'throttle_%s' % ident
+ history = cache.get(key, [])
+ now = time.time()
+
+ # Drop any requests from the history which have now passed the throttle duration
+ while history and history[0] < now - duration:
+ history.pop()
+
+ if len(history) >= num_requests:
+ raise ErrorResponse(status.HTTP_503_SERVICE_UNAVAILABLE, {'detail': 'request was throttled'})
+
+ history.insert(0, now)
+ cache.set(key, history, duration)
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
deleted file mode 100644
index c4381bbf..00000000
--- a/djangorestframework/request.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from djangorestframework.mediatypes import MediaType
-#from djangorestframework.requestparsing import parse, load_parser
-from django.http.multipartparser import LimitBytes
-from StringIO import StringIO
-
-class RequestMixin(object):
- """Delegate class that supplements an HttpRequest object with additional behaviour."""
-
- USE_FORM_OVERLOADING = True
- METHOD_PARAM = "_method"
- CONTENTTYPE_PARAM = "_content_type"
- CONTENT_PARAM = "_content"
-
- def _get_method(self):
- """
- Returns the HTTP method for the current view.
- """
- if not hasattr(self, '_method'):
- self._method = self.request.method
- return self._method
-
-
- def _set_method(self, method):
- """
- Set the method for the current view.
- """
- self._method = method
-
-
- def _get_content_type(self):
- """
- Returns a MediaType object, representing the request's content type header.
- """
- if not hasattr(self, '_content_type'):
- content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
- self._content_type = MediaType(content_type)
- return self._content_type
-
-
- def _set_content_type(self, content_type):
- """
- Set the content type. Should be a MediaType object.
- """
- 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.
- """
- if not hasattr(self, '_stream'):
- request = self.request
-
- # Currently only supports parsing request body as a stream with 1.3
- if hasattr(request, 'read'):
- # It's not at all clear if this needs to be byte limited or not.
- # Maybe I'm just being dumb but it looks to me like there's some issues
- # with that in Django.
- #
- # Either:
- # 1. It *can't* be treated as a limited byte stream, and you _do_ need to
- # respect CONTENT_LENGTH, in which case that ought to be documented,
- # and there probably ought to be a feature request for it to be
- # 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.
- #
- # It's an issue because it affects if you can pass a request off to code that
- # does something like:
- #
- # while stream.read(BUFFER_SIZE):
- # [do stuff]
- #
- #try:
- # content_length = int(request.META.get('CONTENT_LENGTH',0))
- #except (ValueError, TypeError):
- # content_length = 0
- # self._stream = LimitedStream(request, content_length)
- self._stream = request
- else:
- self._stream = StringIO(request.raw_post_data)
- return self._stream
-
-
- def _set_stream(self, stream):
- """
- Set the stream representing the request body.
- """
- self._stream = stream
-
-
- def _get_raw_content(self):
- """
- Returns the parsed content of the request
- """
- if not hasattr(self, '_raw_content'):
- self._raw_content = self.parse(self.stream, self.content_type)
- return self._raw_content
-
-
- def _get_content(self):
- """
- Returns the parsed and validated content of the request
- """
- if not hasattr(self, '_content'):
- self._content = self.validate(self.RAW_CONTENT)
-
- return self._content
-
-
- def perform_form_overloading(self):
- """
- Check the request to see if it is using form POST '_method'/'_content'/'_content_type' overrides.
- 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():
- return
-
- content = self.RAW_CONTENT
- if self.METHOD_PARAM in content:
- self.method = content[self.METHOD_PARAM].upper()
- del self._raw_content[self.METHOD_PARAM]
-
- if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content:
- self._content_type = MediaType(content[self.CONTENTTYPE_PARAM])
- self._stream = StringIO(content[self.CONTENT_PARAM])
- del(self._raw_content)
-
- 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)
-
-
-
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 80e5df2a..fbf51cfc 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -2,13 +2,9 @@ from django.core.urlresolvers import set_script_prefix
from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
-from djangorestframework.emitters import EmitterMixin
-from djangorestframework.parsers import ParserMixin
-from djangorestframework.authenticators import AuthenticatorMixin
-from djangorestframework.validators import FormValidatorMixin
-from djangorestframework.response import Response, ResponseException
-from djangorestframework.request import RequestMixin
-from djangorestframework import emitters, parsers, authenticators, status
+from djangorestframework.response import Response, ErrorResponse
+from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
+from djangorestframework import emitters, parsers, authenticators, permissions, validators, status
# TODO: Figure how out references and named urls need to work nicely
@@ -19,15 +15,11 @@ from djangorestframework import emitters, parsers, authenticators, status
__all__ = ['Resource']
-class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixin, View):
- """Handles incoming requests and maps them to REST operations,
- performing authentication, input deserialization, input validation, output serialization."""
+class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
+ """Handles incoming requests and maps them to REST operations.
+ Performs request deserialization, response serialization, authentication and input validation."""
- # List of RESTful operations which may be performed on this resource.
- # These are going to get dropped at some point, the allowable methods will be defined simply by
- # which methods are present on the request (in the same way as Django's generic View)
- allowed_methods = ('GET',)
- anon_allowed_methods = ()
+ http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace', 'patch']
# List of emitters the resource can serialize the response with, ordered by preference.
emitters = ( emitters.JSONEmitter,
@@ -40,10 +32,16 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
parsers = ( parsers.JSONParser,
parsers.FormParser,
parsers.MultipartParser )
-
+
+ # List of validators to validate, cleanup and normalize the request content
+ validators = ( validators.FormValidator, )
+
# List of all authenticating methods to attempt.
authenticators = ( authenticators.UserLoggedInAuthenticator,
authenticators.BasicAuthenticator )
+
+ # List of all permissions required to access the resource
+ permissions = ()
# Optional form for input validation and presentation of HTML formatted responses.
form = None
@@ -54,52 +52,14 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
name = None
description = None
- # Map standard HTTP methods to function calls
- callmap = { 'GET': 'get', 'POST': 'post',
- 'PUT': 'put', 'DELETE': 'delete' }
-
- def get(self, request, auth, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('GET')
-
-
- def post(self, request, auth, content, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('POST')
-
-
- def put(self, request, auth, content, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('PUT')
-
-
- def delete(self, request, auth, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('DELETE')
-
-
- def not_implemented(self, operation):
- """Return an HTTP 500 server error if an operation is called which has been allowed by
- allowed_methods, but which has not been implemented."""
- raise ResponseException(status.HTTP_500_INTERNAL_SERVER_ERROR,
- {'detail': '%s operation on this resource has not been implemented' % (operation, )})
-
+ @property
+ def allowed_methods(self):
+ return [method.upper() for method in self.http_method_names if getattr(self, method, None)]
- def check_method_allowed(self, method, auth):
- """Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
-
- if not method in self.callmap.keys():
- raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED,
- {'detail': 'Unknown or unsupported method \'%s\'' % method})
-
- if not method in self.allowed_methods:
- raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED,
- {'detail': 'Method \'%s\' not allowed on this resource.' % method})
-
- if auth is None and not method in self.anon_allowed_methods:
- raise ResponseException(status.HTTP_403_FORBIDDEN,
- {'detail': 'You do not have permission to access this resource. ' +
- 'You may need to login or otherwise authenticate the request.'})
+ def http_method_not_allowed(self, request, *args, **kwargs):
+ """Return an HTTP 405 error if an operation is called which does not have a handler method."""
+ raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
+ {'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
def cleanup_response(self, data):
@@ -112,54 +72,37 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
the EmitterMixin and Emitter classes."""
return data
- # Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt.
+
+ # Note: session based authentication is explicitly CSRF validated,
+ # all other authentication is CSRF exempt.
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
- """This method is the core of Resource, through which all requests are passed.
-
- Broadly this consists of the following procedure:
-
- 0. ensure the operation is permitted
- 1. deserialize request content into request data, using standard HTTP content types (PUT/POST only)
- 2. cleanup and validate request data (PUT/POST only)
- 3. call the core method to get the response data
- 4. cleanup the response data
- 5. serialize response data into response content, using standard HTTP content negotiation
- """
-
self.request = request
+ self.args = args
+ self.kwargs = kwargs
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix)
try:
- # Authenticate the request, and store any context so that the resource operations can
- # do more fine grained authentication if required.
- #
- # Typically the context will be a user, or None if this is an anonymous request,
- # but it could potentially be more complex (eg the context of a request key which
- # has been signed against a particular set of permissions)
- auth_context = self.authenticate(request)
-
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
- # self.method, self.content_type, self.CONTENT appropriately.
+ # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
self.perform_form_overloading()
- # Ensure the requested operation is permitted on this resource
- self.check_method_allowed(self.method, auth_context)
-
- # Get the appropriate create/read/update/delete function
- func = getattr(self, self.callmap.get(self.method, None))
-
- # Either generate the response data, deserializing and validating any request data
- # TODO: This is going to change to: func(request, *args, **kwargs)
- # That'll work out now that we have the lazily evaluated self.CONTENT property.
- if self.method in ('PUT', 'POST'):
- response_obj = func(request, auth_context, self.CONTENT, *args, **kwargs)
+ # Authenticate and check request is has the relevant permissions
+ self.check_permissions()
+ # Get the appropriate handler method
+ if self.method.lower() in self.http_method_names:
+ handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
+ # If a previously defined method has been disabled
+ if handler is None:
+ handler = self.http_method_not_allowed
else:
- response_obj = func(request, auth_context, *args, **kwargs)
+ handler = self.http_method_not_allowed
+
+ response_obj = handler(request, *args, **kwargs)
# Allow return value to be either Response, or an object, or None
if isinstance(response_obj, Response):
@@ -172,7 +115,7 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.cleanup_response(response.raw_content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
response = exc.response
# Always add these headers.
@@ -184,3 +127,5 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
return self.emit(response)
+
+
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index fb2e14a2..545a5834 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -1,33 +1,26 @@
from django.core.handlers.wsgi import STATUS_CODE_TEXT
-__all__ =['NoContent', 'Response', ]
-
-
-
-class NoContent(object):
- """Used to indicate no body in http response.
- (We cannot just use None, as that is a valid, serializable response object.)
-
- TODO: On relflection I'm going to get rid of this and just not support serailized 'None' responses.
- """
- pass
+__all__ =['Response', 'ErrorResponse']
+# TODO: remove raw_content/cleaned_content and just use content?
class Response(object):
- def __init__(self, status=200, content=NoContent, headers={}):
+ """An HttpResponse that may include content that hasn't yet been serialized."""
+ def __init__(self, status=200, content=None, headers={}):
self.status = status
- self.has_content_body = not content is NoContent # TODO: remove and just use content
- self.raw_content = content # content prior to filtering - TODO: remove and just use content
- self.cleaned_content = content # content after filtering TODO: remove and just use content
+ self.has_content_body = content is not None
+ self.raw_content = content # content prior to filtering
+ self.cleaned_content = content # content after filtering
self.headers = headers
@property
def status_text(self):
- """Return reason text corrosponding to our HTTP response status code.
- Provided for convienience."""
+ """Return reason text corresponding to our HTTP response status code.
+ Provided for convenience."""
return STATUS_CODE_TEXT.get(self.status, '')
-class ResponseException(BaseException):
- def __init__(self, status, content=NoContent, headers={}):
+class ErrorResponse(BaseException):
+ """An exception representing an HttpResponse that should be returned immediatley."""
+ def __init__(self, status, content=None, headers={}):
self.response = Response(status, content=content, headers=headers)
diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py
index f2a21277..b12dc757 100644
--- a/djangorestframework/tests/accept.py
+++ b/djangorestframework/tests/accept.py
@@ -18,10 +18,13 @@ class UserAgentMungingTest(TestCase):
http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
def setUp(self):
+
class MockResource(Resource):
- anon_allowed_methods = allowed_methods = ('GET',)
- def get(self, request, auth):
+ permissions = ()
+
+ def get(self, request):
return {'a':1, 'b':2, 'c':3}
+
self.req = RequestFactory()
self.MockResource = MockResource
self.view = MockResource.as_view()
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index 246ad4a0..a43a87b3 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -1,19 +1,24 @@
from django.conf.urls.defaults import patterns
+<<<<<<< local
+from django.test import TestCase
+from django.test import Client
+from django.contrib.auth.models import User
+from django.contrib.auth import login
+=======
from django.test import Client, TestCase
+>>>>>>> other
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
from djangorestframework.resource import Resource
-from django.contrib.auth.models import User
-from django.contrib.auth import login
+from djangorestframework import permissions
import base64
class MockResource(Resource):
- allowed_methods = ('POST',)
-
- def post(self, request, auth, content):
+ permissions = ( permissions.IsAuthenticated, )
+ def post(self, request):
return {'a':1, 'b':2, 'c':3}
urlpatterns = patterns('',
@@ -86,3 +91,4 @@ class SessionAuthTests(TestCase):
"""Ensure POSTing form over session authentication without logged in user fails."""
response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
+
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index c5eae2f9..6695bf68 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -1,122 +1,79 @@
-# TODO: refactor these tests
-#from django.test import TestCase
-#from djangorestframework.compat import RequestFactory
-#from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
-#
-#
-#class TestContentMixins(TestCase):
-# def setUp(self):
-# self.req = RequestFactory()
-#
-# # Interface tests
-#
-# def test_content_mixin_interface(self):
-# """Ensure the ContentMixin interface is as expected."""
-# self.assertRaises(NotImplementedError, ContentMixin().determine_content, None)
-#
-# def test_standard_content_mixin_interface(self):
-# """Ensure the OverloadedContentMixin interface is as expected."""
-# self.assertTrue(issubclass(StandardContentMixin, ContentMixin))
-# getattr(StandardContentMixin, 'determine_content')
-#
-# def test_overloaded_content_mixin_interface(self):
-# """Ensure the OverloadedContentMixin interface is as expected."""
-# self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
-# getattr(OverloadedContentMixin, 'CONTENT_PARAM')
-# getattr(OverloadedContentMixin, 'CONTENTTYPE_PARAM')
-# getattr(OverloadedContentMixin, 'determine_content')
-#
-#
-# # Common functionality to test with both StandardContentMixin and OverloadedContentMixin
-#
-# def ensure_determines_no_content_GET(self, mixin):
-# """Ensure determine_content(request) returns None for GET request with no content."""
-# request = self.req.get('/')
-# self.assertEqual(mixin.determine_content(request), None)
-#
-# def ensure_determines_form_content_POST(self, mixin):
-# """Ensure determine_content(request) returns content for POST request with content."""
-# form_data = {'qwerty': 'uiop'}
-# request = self.req.post('/', data=form_data)
-# self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
-#
-# def ensure_determines_non_form_content_POST(self, mixin):
-# """Ensure determine_content(request) returns (content type, content) for POST request with content."""
-# content = 'qwerty'
-# content_type = 'text/plain'
-# request = self.req.post('/', content, content_type=content_type)
-# self.assertEqual(mixin.determine_content(request), (content_type, content))
-#
-# def ensure_determines_form_content_PUT(self, mixin):
-# """Ensure determine_content(request) returns content for PUT request with content."""
-# form_data = {'qwerty': 'uiop'}
-# request = self.req.put('/', data=form_data)
-# self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
-#
-# def ensure_determines_non_form_content_PUT(self, mixin):
-# """Ensure determine_content(request) returns (content type, content) for PUT request with content."""
-# content = 'qwerty'
-# content_type = 'text/plain'
-# request = self.req.put('/', content, content_type=content_type)
-# self.assertEqual(mixin.determine_content(request), (content_type, content))
-#
-# # StandardContentMixin behavioural tests
-#
-# def test_standard_behaviour_determines_no_content_GET(self):
-# """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
-# self.ensure_determines_no_content_GET(StandardContentMixin())
-#
-# def test_standard_behaviour_determines_form_content_POST(self):
-# """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
-# self.ensure_determines_form_content_POST(StandardContentMixin())
-#
-# def test_standard_behaviour_determines_non_form_content_POST(self):
-# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
-# self.ensure_determines_non_form_content_POST(StandardContentMixin())
-#
-# def test_standard_behaviour_determines_form_content_PUT(self):
-# """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
-# self.ensure_determines_form_content_PUT(StandardContentMixin())
-#
-# def test_standard_behaviour_determines_non_form_content_PUT(self):
-# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
-# self.ensure_determines_non_form_content_PUT(StandardContentMixin())
-#
-# # OverloadedContentMixin behavioural tests
-#
-# def test_overloaded_behaviour_determines_no_content_GET(self):
-# """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
-# self.ensure_determines_no_content_GET(OverloadedContentMixin())
-#
-# def test_overloaded_behaviour_determines_form_content_POST(self):
-# """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
-# self.ensure_determines_form_content_POST(OverloadedContentMixin())
-#
-# def test_overloaded_behaviour_determines_non_form_content_POST(self):
-# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
-# self.ensure_determines_non_form_content_POST(OverloadedContentMixin())
-#
-# def test_overloaded_behaviour_determines_form_content_PUT(self):
-# """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
-# self.ensure_determines_form_content_PUT(OverloadedContentMixin())
-#
-# def test_overloaded_behaviour_determines_non_form_content_PUT(self):
-# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
-# self.ensure_determines_non_form_content_PUT(OverloadedContentMixin())
-#
-# def test_overloaded_behaviour_allows_content_tunnelling(self):
-# """Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
-# content = 'qwerty'
-# content_type = 'text/plain'
-# form_data = {OverloadedContentMixin.CONTENT_PARAM: content,
-# OverloadedContentMixin.CONTENTTYPE_PARAM: content_type}
-# request = self.req.post('/', form_data)
-# self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
-# self.assertEqual(request.META['CONTENT_TYPE'], content_type)
-#
-# def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
-# """Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
-# content = 'qwerty'
-# request = self.req.post('/', {OverloadedContentMixin.CONTENT_PARAM: content})
-# self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
+"""
+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
+
+class TestContentParsing(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
+
+ def ensure_determines_no_content_GET(self, view):
+ """Ensure view.RAW_CONTENT returns None for GET request with no content."""
+ view.request = self.req.get('/')
+ self.assertEqual(view.RAW_CONTENT, None)
+
+ 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.request = self.req.post('/', data=form_data)
+ self.assertEqual(view.RAW_CONTENT, form_data)
+
+ def ensure_determines_non_form_content_POST(self, view):
+ """Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ view.parsers = (PlainTextParser,)
+ view.request = self.req.post('/', content, content_type=content_type)
+ self.assertEqual(view.RAW_CONTENT, content)
+
+ 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.request = self.req.put('/', data=form_data)
+ self.assertEqual(view.RAW_CONTENT, form_data)
+
+ def ensure_determines_non_form_content_PUT(self, view):
+ """Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ view.parsers = (PlainTextParser,)
+ view.request = self.req.post('/', content, content_type=content_type)
+ self.assertEqual(view.RAW_CONTENT, content)
+
+ def test_standard_behaviour_determines_no_content_GET(self):
+ """Ensure view.RAW_CONTENT returns None for GET request with no content."""
+ self.ensure_determines_no_content_GET(RequestMixin())
+
+ def test_standard_behaviour_determines_form_content_POST(self):
+ """Ensure view.RAW_CONTENT returns content for POST request with form content."""
+ self.ensure_determines_form_content_POST(RequestMixin())
+
+ def test_standard_behaviour_determines_non_form_content_POST(self):
+ """Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
+ self.ensure_determines_non_form_content_POST(RequestMixin())
+
+ def test_standard_behaviour_determines_form_content_PUT(self):
+ """Ensure view.RAW_CONTENT returns content for PUT request with form content."""
+ self.ensure_determines_form_content_PUT(RequestMixin())
+
+ def test_standard_behaviour_determines_non_form_content_PUT(self):
+ """Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
+ self.ensure_determines_non_form_content_PUT(RequestMixin())
+
+ def test_overloaded_behaviour_allows_content_tunnelling(self):
+ """Ensure request.RAW_CONTENT returns content for overloaded POST request"""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ view = RequestMixin()
+ form_data = {view.CONTENT_PARAM: content,
+ view.CONTENTTYPE_PARAM: content_type}
+ view.request = self.req.post('/', form_data)
+ view.parsers = (PlainTextParser,)
+ view.perform_form_overloading()
+ self.assertEqual(view.RAW_CONTENT, content)
diff --git a/djangorestframework/tests/emitters.py b/djangorestframework/tests/emitters.py
index 7d024ccf..21a7eb95 100644
--- a/djangorestframework/tests/emitters.py
+++ b/djangorestframework/tests/emitters.py
@@ -2,7 +2,8 @@ from django.conf.urls.defaults import patterns, url
from django import http
from django.test import TestCase
from djangorestframework.compat import View
-from djangorestframework.emitters import EmitterMixin, BaseEmitter
+from djangorestframework.emitters import BaseEmitter
+from djangorestframework.mixins import ResponseMixin
from djangorestframework.response import Response
DUMMYSTATUS = 200
@@ -11,7 +12,7 @@ DUMMYCONTENT = 'dummycontent'
EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x
EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x
-class MockView(EmitterMixin, View):
+class MockView(ResponseMixin, View):
def get(self, request):
response = Response(DUMMYSTATUS, DUMMYCONTENT)
return self.emit(response)
diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py
index e155f181..4dc3aa40 100644
--- a/djangorestframework/tests/files.py
+++ b/djangorestframework/tests/files.py
@@ -16,13 +16,12 @@ class UploadFilesTests(TestCase):
file = forms.FileField
class MockResource(Resource):
- allowed_methods = anon_allowed_methods = ('POST',)
+ permissions = ()
form = FileForm
- def post(self, request, auth, content, *args, **kwargs):
- #self.uploaded = content.file
- return {'FILE_NAME': content['file'].name,
- 'FILE_CONTENT': content['file'].read()}
+ def post(self, request, *args, **kwargs):
+ return {'FILE_NAME': self.CONTENT['file'].name,
+ 'FILE_CONTENT': self.CONTENT['file'].read()}
file = StringIO.StringIO('stuff')
file.name = 'stuff.txt'
diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py
index f19bb3e5..0e74dc94 100644
--- a/djangorestframework/tests/methods.py
+++ b/djangorestframework/tests/methods.py
@@ -1,53 +1,27 @@
-# TODO: Refactor these tests
-#from django.test import TestCase
-#from djangorestframework.compat import RequestFactory
-#from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
-#
-#
-#class TestMethodMixins(TestCase):
-# def setUp(self):
-# self.req = RequestFactory()
-#
-# # Interface tests
-#
-# def test_method_mixin_interface(self):
-# """Ensure the base ContentMixin interface is as expected."""
-# self.assertRaises(NotImplementedError, MethodMixin().determine_method, None)
-#
-# def test_standard_method_mixin_interface(self):
-# """Ensure the StandardMethodMixin interface is as expected."""
-# self.assertTrue(issubclass(StandardMethodMixin, MethodMixin))
-# getattr(StandardMethodMixin, 'determine_method')
-#
-# def test_overloaded_method_mixin_interface(self):
-# """Ensure the OverloadedPOSTMethodMixin interface is as expected."""
-# self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
-# getattr(OverloadedPOSTMethodMixin, 'METHOD_PARAM')
-# getattr(OverloadedPOSTMethodMixin, 'determine_method')
-#
-# # Behavioural tests
-#
-# def test_standard_behaviour_determines_GET(self):
-# """GET requests identified as GET method with StandardMethodMixin"""
-# request = self.req.get('/')
-# self.assertEqual(StandardMethodMixin().determine_method(request), 'GET')
-#
-# def test_standard_behaviour_determines_POST(self):
-# """POST requests identified as POST method with StandardMethodMixin"""
-# request = self.req.post('/')
-# self.assertEqual(StandardMethodMixin().determine_method(request), 'POST')
-#
-# def test_overloaded_POST_behaviour_determines_GET(self):
-# """GET requests identified as GET method with OverloadedPOSTMethodMixin"""
-# request = self.req.get('/')
-# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'GET')
-#
-# def test_overloaded_POST_behaviour_determines_POST(self):
-# """POST requests identified as POST method with OverloadedPOSTMethodMixin"""
-# request = self.req.post('/')
-# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'POST')
-#
-# def test_overloaded_POST_behaviour_determines_overloaded_method(self):
-# """POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
-# request = self.req.post('/', {OverloadedPOSTMethodMixin.METHOD_PARAM: 'DELETE'})
-# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
+from django.test import TestCase
+from djangorestframework.compat import RequestFactory
+from djangorestframework.mixins import RequestMixin
+
+
+class TestMethodOverloading(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
+
+ def test_standard_behaviour_determines_GET(self):
+ """GET requests identified"""
+ view = RequestMixin()
+ view.request = self.req.get('/')
+ self.assertEqual(view.method, 'GET')
+
+ def test_standard_behaviour_determines_POST(self):
+ """POST requests identified"""
+ view = RequestMixin()
+ view.request = self.req.post('/')
+ self.assertEqual(view.method, 'POST')
+
+ def test_overloaded_POST_behaviour_determines_overloaded_method(self):
+ """POST requests can be overloaded to another method by setting a reserved form field"""
+ view = RequestMixin()
+ view.request = self.req.post('/', {view.METHOD_PARAM: 'DELETE'})
+ view.perform_form_overloading()
+ self.assertEqual(view.method, 'DELETE')
diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py
index 2718ebca..63e2080a 100644
--- a/djangorestframework/tests/reverse.py
+++ b/djangorestframework/tests/reverse.py
@@ -5,12 +5,16 @@ from django.utils import simplejson as json
from djangorestframework.resource import Resource
+<<<<<<< local
+
+=======
+>>>>>>> other
class MockResource(Resource):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
- anon_allowed_methods = ('GET',)
+ permissions = ()
- def get(self, request, auth):
+ def get(self, request):
return reverse('another')
urlpatterns = patterns('',
@@ -24,5 +28,9 @@ class ReverseTests(TestCase):
urls = 'djangorestframework.tests.reverse'
def test_reversed_urls_are_fully_qualified(self):
- response = self.client.get('/')
+ try:
+ response = self.client.get('/')
+ except:
+ import traceback
+ traceback.print_exc()
self.assertEqual(json.loads(response.content), 'http://testserver/another')
diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py
new file mode 100644
index 00000000..46383271
--- /dev/null
+++ b/djangorestframework/tests/throttling.py
@@ -0,0 +1,38 @@
+from django.conf.urls.defaults import patterns
+from django.test import TestCase
+from django.utils import simplejson as json
+
+from djangorestframework.compat import RequestFactory
+from djangorestframework.resource import Resource
+from djangorestframework.permissions import Throttling
+
+
+class MockResource(Resource):
+ permissions = ( Throttling, )
+ throttle = (3, 1) # 3 requests per second
+
+ def get(self, request):
+ return 'foo'
+
+urlpatterns = patterns('',
+ (r'^$', MockResource.as_view()),
+)
+
+
+#class ThrottlingTests(TestCase):
+# """Basic authentication"""
+# urls = 'djangorestframework.tests.throttling'
+#
+# def test_requests_are_throttled(self):
+# """Ensure request rate is limited"""
+# for dummy in range(3):
+# response = self.client.get('/')
+# response = self.client.get('/')
+#
+# def test_request_throttling_is_per_user(self):
+# """Ensure request rate is only limited per user, not globally"""
+# pass
+#
+# def test_request_throttling_expires(self):
+# """Ensure request rate is limited for a limited duration only"""
+# pass
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
index b5d2d566..b6563db6 100644
--- a/djangorestframework/tests/validators.py
+++ b/djangorestframework/tests/validators.py
@@ -2,8 +2,8 @@ from django import forms
from django.db import models
from django.test import TestCase
from djangorestframework.compat import RequestFactory
-from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin
-from djangorestframework.response import ResponseException
+from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
+from djangorestframework.response import ErrorResponse
class TestValidatorMixinInterfaces(TestCase):
@@ -11,59 +11,53 @@ class TestValidatorMixinInterfaces(TestCase):
def test_validator_mixin_interface(self):
"""Ensure the ValidatorMixin base class interface is as expected."""
- self.assertRaises(NotImplementedError, ValidatorMixin().validate, None)
-
- def test_form_validator_mixin_interface(self):
- """Ensure the FormValidatorMixin interface is as expected."""
- self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin))
- getattr(FormValidatorMixin, 'form')
- getattr(FormValidatorMixin, 'validate')
-
- def test_model_form_validator_mixin_interface(self):
- """Ensure the ModelFormValidatorMixin interface is as expected."""
- self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin))
- getattr(ModelFormValidatorMixin, 'model')
- getattr(ModelFormValidatorMixin, 'form')
- getattr(ModelFormValidatorMixin, 'fields')
- getattr(ModelFormValidatorMixin, 'exclude_fields')
- getattr(ModelFormValidatorMixin, 'validate')
+ self.assertRaises(NotImplementedError, BaseValidator(None).validate, None)
class TestDisabledValidations(TestCase):
- """Tests on Validator Mixins with validation disabled by setting form to None"""
+ """Tests on FormValidator with validation disabled by setting form to None"""
def test_disabled_form_validator_returns_content_unchanged(self):
- """If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
- class DisabledFormValidator(FormValidatorMixin):
+ """If the view's form attribute is None then FormValidator(view).validate(content)
+ should just return the content unmodified."""
+ class DisabledFormView(object):
form = None
+ view = DisabledFormView()
content = {'qwerty':'uiop'}
- self.assertEqual(DisabledFormValidator().validate(content), content)
+ self.assertEqual(FormValidator(view).validate(content), content)
def test_disabled_form_validator_get_bound_form_returns_none(self):
- """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
- class DisabledFormValidator(FormValidatorMixin):
+ """If the view's form attribute is None on then
+ FormValidator(view).get_bound_form(content) should just return None."""
+ class DisabledFormView(object):
form = None
- content = {'qwerty':'uiop'}
- self.assertEqual(DisabledFormValidator().get_bound_form(content), None)
+ view = DisabledFormView()
+ content = {'qwerty':'uiop'}
+ self.assertEqual(FormValidator(view).get_bound_form(content), None)
+
def test_disabled_model_form_validator_returns_content_unchanged(self):
- """If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
- class DisabledModelFormValidator(ModelFormValidatorMixin):
+ """If the view's form and model attributes are None then
+ ModelFormValidator(view).validate(content) should just return the content unmodified."""
+ class DisabledModelFormView(object):
form = None
+ model = None
+ view = DisabledModelFormView()
content = {'qwerty':'uiop'}
- self.assertEqual(DisabledModelFormValidator().validate(content), content)
+ self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
- class DisabledModelFormValidator(ModelFormValidatorMixin):
+ class DisabledModelFormView(object):
form = None
-
- content = {'qwerty':'uiop'}
- self.assertEqual(DisabledModelFormValidator().get_bound_form(content), None)
-
+ model = None
+
+ view = DisabledModelFormView()
+ content = {'qwerty':'uiop'}
+ self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
class TestNonFieldErrors(TestCase):
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
@@ -80,13 +74,14 @@ class TestNonFieldErrors(TestCase):
raise forms.ValidationError(self.ERROR_TEXT)
return self.cleaned_data #pragma: no cover
- class MockValidator(FormValidatorMixin):
+ class MockView(object):
form = MockForm
+ view = MockView()
content = {'field1': 'example1', 'field2': 'example2'}
try:
- MockValidator().validate(content)
- except ResponseException, exc:
+ FormValidator(view).validate(content)
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else:
self.fail('ResourceException was not raised') #pragma: no cover
@@ -95,19 +90,21 @@ class TestNonFieldErrors(TestCase):
class TestFormValidation(TestCase):
"""Tests which check basic form validation.
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
- (ModelFormValidatorMixin should behave as FormValidatorMixin if form is set rather than relying on the default ModelForm)"""
+ (ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
def setUp(self):
class MockForm(forms.Form):
qwerty = forms.CharField(required=True)
- class MockFormValidator(FormValidatorMixin):
+ class MockFormView(object):
form = MockForm
-
- class MockModelFormValidator(ModelFormValidatorMixin):
+ validators = (FormValidator,)
+
+ class MockModelFormView(object):
form = MockForm
-
- self.MockFormValidator = MockFormValidator
- self.MockModelFormValidator = MockModelFormValidator
+ validators = (ModelFormValidator,)
+
+ self.MockFormView = MockFormView
+ self.MockModelFormView = MockModelFormView
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
@@ -118,14 +115,14 @@ class TestFormValidation(TestCase):
def validation_failure_raises_response_exception(self, validator):
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
content = {}
- self.assertRaises(ResponseException, validator.validate, content)
+ self.assertRaises(ErrorResponse, validator.validate, content)
def validation_does_not_allow_extra_fields_by_default(self, validator):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'uiop', 'extra': 'extra'}
- self.assertRaises(ResponseException, validator.validate, content)
+ self.assertRaises(ErrorResponse, validator.validate, content)
def validation_allows_extra_fields_if_explicitly_set(self, validator):
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
@@ -142,7 +139,7 @@ class TestFormValidation(TestCase):
content = {}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
@@ -152,7 +149,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': ''}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
@@ -162,7 +159,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': 'uiop', 'extra': 'extra'}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
@@ -172,7 +169,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': '', 'extra': 'extra'}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}})
else:
@@ -181,60 +178,78 @@ class TestFormValidation(TestCase):
# Tests on FormValidtionMixin
def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
- self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
def test_form_validation_failure_raises_response_exception(self):
- self.validation_failure_raises_response_exception(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failure_raises_response_exception(validator)
def test_validation_does_not_allow_extra_fields_by_default(self):
- self.validation_does_not_allow_extra_fields_by_default(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_does_not_allow_extra_fields_by_default(validator)
def test_validation_allows_extra_fields_if_explicitly_set(self):
- self.validation_allows_extra_fields_if_explicitly_set(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
- self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
- self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
- self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
- self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
- self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
# Same tests on ModelFormValidtionMixin
def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
- self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
def test_modelform_validation_failure_raises_response_exception(self):
- self.validation_failure_raises_response_exception(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failure_raises_response_exception(validator)
def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
- self.validation_does_not_allow_extra_fields_by_default(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_does_not_allow_extra_fields_by_default(validator)
def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
- self.validation_allows_extra_fields_if_explicitly_set(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
- self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
- self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
- self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
- self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
- self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
class TestModelFormValidator(TestCase):
@@ -250,42 +265,42 @@ class TestModelFormValidator(TestCase):
def readonly(self):
return 'read only'
- class MockValidator(ModelFormValidatorMixin):
+ class MockView(object):
model = MockModel
- self.MockValidator = MockValidator
+ self.validator = ModelFormValidator(MockView)
def test_property_fields_are_allowed_on_model_forms(self):
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
- self.assertEqual(self.MockValidator().validate(content), content)
+ self.assertEqual(self.validator.validate(content), content)
def test_property_fields_are_not_required_on_model_forms(self):
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
content = {'qwerty':'example', 'uiop': 'example'}
- self.assertEqual(self.MockValidator().validate(content), content)
+ self.assertEqual(self.validator.validate(content), content)
def test_extra_fields_not_allowed_on_model_forms(self):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
- self.assertRaises(ResponseException, self.MockValidator().validate, content)
+ self.assertRaises(ErrorResponse, self.validator.validate, content)
def test_validate_requires_fields_on_model_forms(self):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
broken clients more easily (eg submitting content with a misnamed field)"""
content = {'readonly': 'read only'}
- self.assertRaises(ResponseException, self.MockValidator().validate, content)
+ self.assertRaises(ErrorResponse, self.validator.validate, content)
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
content = {'qwerty':'example', 'readonly': 'read only'}
- self.MockValidator().validate(content)
+ self.validator.validate(content)
def test_model_form_validator_uses_model_forms(self):
- self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm))
+ self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
diff --git a/djangorestframework/utils.py b/djangorestframework/utils.py
index d45e5acf..8b12294c 100644
--- a/djangorestframework/utils.py
+++ b/djangorestframework/utils.py
@@ -14,6 +14,7 @@ except ImportError:
# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
+MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
def as_tuple(obj):
"""Given obj return a tuple"""
diff --git a/djangorestframework/validators.py b/djangorestframework/validators.py
index d96e8d9e..c612de55 100644
--- a/djangorestframework/validators.py
+++ b/djangorestframework/validators.py
@@ -1,36 +1,42 @@
"""Mixin classes that provide a validate(content) function to validate and cleanup request content"""
from django import forms
from django.db import models
-from djangorestframework.response import ResponseException
+from djangorestframework.response import ErrorResponse
from djangorestframework.utils import as_tuple
-class ValidatorMixin(object):
- """Base class for all ValidatorMixin classes, which simply defines the interface they provide."""
+
+class BaseValidator(object):
+ """Base class for all Validator classes, which simply defines the interface they provide."""
+
+ def __init__(self, view):
+ self.view = view
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
- Raises a ResponseException with status code 400 (Bad Request) on failure.
-
+ Typically raises a ErrorResponse with status code 400 (Bad Request) on failure.
+
Must be overridden to be implemented."""
raise NotImplementedError()
-class FormValidatorMixin(ValidatorMixin):
- """Validator Mixin that uses forms for validation.
- Extends the ValidatorMixin interface to also provide a get_bound_form() method.
- (Which may be used by some emitters.)"""
+class FormValidator(BaseValidator):
+ """Validator class that uses forms for validation.
+ Also provides a get_bound_form() method which may be used by some renderers.
- """The form class that should be used for validation, or None to turn off form validation."""
- form = None
- bound_form_instance = None
+ The view class should provide `.form` attribute which specifies the form classmethod
+ to be used for validation.
+
+ On calling validate() this validator may set a `.bound_form_instance` attribute on the
+ view, which may be used by some renderers."""
+
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
- Raises a ResponseException with status code 400 (Bad Request) on failure.
+ Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied.
- On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
+ On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
return self._validate(content)
@@ -44,7 +50,7 @@ class FormValidatorMixin(ValidatorMixin):
if bound_form is None:
return content
- self.bound_form_instance = bound_form
+ self.view.bound_form_instance = bound_form
seen_fields_set = set(content.keys())
form_fields_set = set(bound_form.fields.keys())
@@ -78,7 +84,10 @@ class FormValidatorMixin(ValidatorMixin):
detail[u'errors'] = bound_form.non_field_errors()
# Add standard field errors
- field_errors = dict((key, map(unicode, val)) for (key, val) in bound_form.errors.iteritems() if not key.startswith('__'))
+ field_errors = dict((key, map(unicode, val))
+ for (key, val)
+ in bound_form.errors.iteritems()
+ if not key.startswith('__'))
# Add any unknown field errors
for key in unknown_fields:
@@ -88,26 +97,27 @@ class FormValidatorMixin(ValidatorMixin):
detail[u'field-errors'] = field_errors
# Return HTTP 400 response (BAD REQUEST)
- raise ResponseException(400, detail)
+ raise ErrorResponse(400, detail)
def get_bound_form(self, content=None):
"""Given some content return a Django form bound to that content.
If form validation is turned off (form class attribute is None) then returns None."""
- if not self.form:
+ form_cls = getattr(self.view, 'form', None)
+
+ if not form_cls:
return None
- if not content is None:
+ if content is not None:
if hasattr(content, 'FILES'):
- return self.form(content, content.FILES)
- return self.form(content)
- return self.form()
+ return form_cls(content, content.FILES)
+ return form_cls(content)
+ return form_cls()
-class ModelFormValidatorMixin(FormValidatorMixin):
- """Validator Mixin that uses forms for validation and falls back to a model form if no form is set.
- Extends the ValidatorMixin interface to also provide a get_bound_form() method.
- (Which may be used by some emitters.)"""
+class ModelFormValidator(FormValidator):
+ """Validator class that uses forms for validation and otherwise falls back to a model form if no form is set.
+ Also provides a get_bound_form() method which may be used by some renderers."""
"""The form class that should be used for validation, or None to use model form validation."""
form = None
@@ -129,14 +139,14 @@ class ModelFormValidatorMixin(FormValidatorMixin):
# TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
- Raises a ResponseException with status code 400 (Bad Request) on failure.
+ Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Validation is standard form or model form validation,
with an additional constraint that no extra unknown fields may be supplied,
and that all fields specified by the fields class attribute must be supplied,
even if they are not validated by the form/model form.
- On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
+ On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
return self._validate(content, allowed_extra_fields=self._property_fields_set)
@@ -148,15 +158,18 @@ class ModelFormValidatorMixin(FormValidatorMixin):
If the form class attribute has been explicitly set then use that class to create a Form,
otherwise if model is set use that class to create a ModelForm, otherwise return None."""
- if self.form:
+ form_cls = getattr(self.view, 'form', None)
+ model_cls = getattr(self.view, 'model', None)
+
+ if form_cls:
# Use explict Form
- return super(ModelFormValidatorMixin, self).get_bound_form(content)
+ return super(ModelFormValidator, self).get_bound_form(content)
- elif self.model:
+ elif model_cls:
# Fall back to ModelForm which we create on the fly
class OnTheFlyModelForm(forms.ModelForm):
class Meta:
- model = self.model
+ model = model_cls
#fields = tuple(self._model_fields_set)
# Instantiate the ModelForm as appropriate
@@ -176,24 +189,32 @@ class ModelFormValidatorMixin(FormValidatorMixin):
@property
def _model_fields_set(self):
"""Return a set containing the names of validated fields on the model."""
- model_fields = set(field.name for field in self.model._meta.fields)
+ model = getattr(self.view, 'model', None)
+ fields = getattr(self.view, 'fields', self.fields)
+ exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
- if self.fields:
- return model_fields & set(as_tuple(self.fields))
+ model_fields = set(field.name for field in model._meta.fields)
- return model_fields - set(as_tuple(self.exclude_fields))
+ if fields:
+ return model_fields & set(as_tuple(fields))
+
+ return model_fields - set(as_tuple(exclude_fields))
@property
def _property_fields_set(self):
"""Returns a set containing the names of validated properties on the model."""
- property_fields = set(attr for attr in dir(self.model) if
- isinstance(getattr(self.model, attr, None), property)
+ model = getattr(self.view, 'model', None)
+ fields = getattr(self.view, 'fields', self.fields)
+ exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
+
+ property_fields = set(attr for attr in dir(model) if
+ isinstance(getattr(model, attr, None), property)
and not attr.startswith('_'))
- if self.fields:
- return property_fields & set(as_tuple(self.fields))
+ if fields:
+ return property_fields & set(as_tuple(fields))
- return property_fields - set(as_tuple(self.exclude_fields))
+ return property_fields - set(as_tuple(exclude_fields))
diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py
index d4084e72..494478d8 100644
--- a/examples/blogpost/tests.py
+++ b/examples/blogpost/tests.py
@@ -1,8 +1,12 @@
"""Test a range of REST API usage of the example application.
"""
+from django.core.urlresolvers import reverse
from django.test import TestCase
+<<<<<<< local
+=======
from django.core.urlresolvers import reverse
+>>>>>>> other
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
@@ -166,7 +170,10 @@ class AllowedMethodsTests(TestCase):
#above testcases need to probably moved to the core
+<<<<<<< local
+=======
+>>>>>>> other
class TestRotation(TestCase):
"""For the example the maximum amount of Blogposts is capped off at views.MAX_POSTS.
diff --git a/examples/blogpost/views.py b/examples/blogpost/views.py
index 59a3fb9f..9e07aa8a 100644
--- a/examples/blogpost/views.py
+++ b/examples/blogpost/views.py
@@ -8,25 +8,21 @@ MAX_POSTS = 10
class BlogPosts(RootModelResource):
"""A resource with which lists all existing blog posts and creates new blog posts."""
- anon_allowed_methods = allowed_methods = ('GET', 'POST',)
model = models.BlogPost
fields = BLOG_POST_FIELDS
class BlogPostInstance(ModelResource):
"""A resource which represents a single blog post."""
- anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
model = models.BlogPost
fields = BLOG_POST_FIELDS
class Comments(RootModelResource):
"""A resource which lists all existing comments for a given blog post, and creates new blog comments for a given blog post."""
- anon_allowed_methods = allowed_methods = ('GET', 'POST',)
model = models.Comment
fields = COMMENT_FIELDS
class CommentInstance(ModelResource):
"""A resource which represents a single comment."""
- anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
model = models.Comment
fields = COMMENT_FIELDS
diff --git a/examples/mixin/urls.py b/examples/mixin/urls.py
index 05009284..96b630e3 100644
--- a/examples/mixin/urls.py
+++ b/examples/mixin/urls.py
@@ -1,12 +1,13 @@
from djangorestframework.compat import View # Use Django 1.3's django.views.generic.View, or fall back to a clone of that if Django < 1.3
-from djangorestframework.emitters import EmitterMixin, DEFAULT_EMITTERS
+from djangorestframework.mixins import ResponseMixin
+from djangorestframework.emitters import DEFAULT_EMITTERS
from djangorestframework.response import Response
from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
-class ExampleView(EmitterMixin, View):
+class ExampleView(ResponseMixin, View):
"""An example view using Django 1.3's class based views.
Uses djangorestframework's EmitterMixin to provide support for multiple output formats."""
emitters = DEFAULT_EMITTERS
diff --git a/examples/modelresourceexample/views.py b/examples/modelresourceexample/views.py
index e912c019..07f50b65 100644
--- a/examples/modelresourceexample/views.py
+++ b/examples/modelresourceexample/views.py
@@ -7,12 +7,10 @@ class MyModelRootResource(RootModelResource):
"""A create/list resource for MyModel.
Available for both authenticated and anonymous access for the purposes of the sandbox."""
model = MyModel
- allowed_methods = anon_allowed_methods = ('GET', 'POST')
fields = FIELDS
class MyModelResource(ModelResource):
"""A read/update/delete resource for MyModel.
Available for both authenticated and anonymous access for the purposes of the sandbox."""
model = MyModel
- allowed_methods = anon_allowed_methods = ('GET', 'PUT', 'DELETE')
fields = FIELDS
diff --git a/examples/pygments_api/tests.py b/examples/pygments_api/tests.py
index 3bdc2ec5..766defc3 100644
--- a/examples/pygments_api/tests.py
+++ b/examples/pygments_api/tests.py
@@ -1,9 +1,18 @@
from django.test import TestCase
from django.utils import simplejson as json
+<<<<<<< local
+
+=======
+>>>>>>> other
from djangorestframework.compat import RequestFactory
+
from pygments_api import views
import tempfile, shutil
+<<<<<<< local
+
+=======
+>>>>>>> other
class TestPygmentsExample(TestCase):
diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py
index 377761b1..4e6d1230 100644
--- a/examples/pygments_api/views.py
+++ b/examples/pygments_api/views.py
@@ -41,26 +41,25 @@ class PygmentsRoot(Resource):
"""This example demonstrates a simple RESTful Web API aound the awesome pygments library.
This top level resource is used to create highlighted code snippets, and to list all the existing code snippets."""
form = PygmentsForm
- allowed_methods = anon_allowed_methods = ('GET', 'POST',)
- def get(self, request, auth):
+ def get(self, request):
"""Return a list of all currently existing snippets."""
unique_ids = [os.path.split(f)[1] for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)]
return [reverse('pygments-instance', args=[unique_id]) for unique_id in unique_ids]
- def post(self, request, auth, content):
+ def post(self, request):
"""Create a new highlighed snippet and return it's location.
For the purposes of the sandbox example, also ensure we delete the oldest snippets if we have > MAX_FILES."""
unique_id = str(uuid.uuid1())
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
- lexer = get_lexer_by_name(content['lexer'])
- linenos = 'table' if content['linenos'] else False
- options = {'title': content['title']} if content['title'] else {}
- formatter = HtmlFormatter(style=content['style'], linenos=linenos, full=True, **options)
+ lexer = get_lexer_by_name(self.CONTENT['lexer'])
+ linenos = 'table' if self.CONTENT['linenos'] else False
+ options = {'title': self.CONTENT['title']} if self.CONTENT['title'] else {}
+ formatter = HtmlFormatter(style=self.CONTENT['style'], linenos=linenos, full=True, **options)
with open(pathname, 'w') as outfile:
- highlight(content['code'], lexer, formatter, outfile)
+ highlight(self.CONTENT['code'], lexer, formatter, outfile)
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
@@ -70,20 +69,19 @@ class PygmentsRoot(Resource):
class PygmentsInstance(Resource):
"""Simply return the stored highlighted HTML file with the correct mime type.
This Resource only emits HTML and uses a standard HTML emitter rather than the emitters.DocumentingHTMLEmitter class."""
- allowed_methods = anon_allowed_methods = ('GET',)
emitters = (HTMLEmitter,)
- def get(self, request, auth, unique_id):
+ def get(self, request, unique_id):
"""Return the highlighted snippet."""
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname):
- return Resource(status.HTTP_404_NOT_FOUND)
+ return Response(status.HTTP_404_NOT_FOUND)
return open(pathname, 'r').read()
- def delete(self, request, auth, unique_id):
+ def delete(self, request, unique_id):
"""Delete the highlighted snippet."""
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname):
- return Resource(status.HTTP_404_NOT_FOUND)
+ return Response(status.HTTP_404_NOT_FOUND)
return os.remove(pathname)
diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py
index 41d2e5c5..911fd467 100644
--- a/examples/resourceexample/views.py
+++ b/examples/resourceexample/views.py
@@ -8,24 +8,22 @@ from resourceexample.forms import MyForm
class ExampleResource(Resource):
"""A basic read-only resource that points to 3 other resources."""
- allowed_methods = anon_allowed_methods = ('GET',)
- def get(self, request, auth):
+ def get(self, request):
return {"Some other resources": [reverse('another-example-resource', kwargs={'num':num}) for num in range(3)]}
class AnotherExampleResource(Resource):
"""A basic GET-able/POST-able resource."""
- allowed_methods = anon_allowed_methods = ('GET', 'POST')
form = MyForm # Optional form validation on input (Applies in this case the POST method, but can also apply to PUT)
- def get(self, request, auth, num):
+ def get(self, request, num):
"""Handle GET requests"""
if int(num) > 2:
return Response(status.HTTP_404_NOT_FOUND)
return "GET request to AnotherExampleResource %s" % num
- def post(self, request, auth, content, num):
+ def post(self, request, num):
"""Handle POST requests"""
if int(num) > 2:
return Response(status.HTTP_404_NOT_FOUND)
- return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(content))
+ return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT))
diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py
index 561bdb1d..5b84e8e4 100644
--- a/examples/sandbox/views.py
+++ b/examples/sandbox/views.py
@@ -24,9 +24,8 @@ class Sandbox(Resource):
6. A blog posts and comments API.
Please feel free to browse, create, edit and delete the resources in these examples."""
- allowed_methods = anon_allowed_methods = ('GET',)
- def get(self, request, auth):
+ def get(self, request):
return [{'name': 'Simple Resource example', 'url': reverse('example-resource')},
{'name': 'Simple ModelResource example', 'url': reverse('my-model-root-resource')},
{'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')},