diff options
Diffstat (limited to 'djangorestframework')
27 files changed, 742 insertions, 785 deletions
diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index efe7f566..46dd608f 100644 --- a/djangorestframework/__init__.py +++ b/djangorestframework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.3.3' +__version__ = '0.4.0-dev' VERSION = __version__ # synonym diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index e326c15a..cb95fb81 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -87,8 +87,6 @@ class UserLoggedInAuthentication(BaseAuthentication): Returns a :obj:`User` if the request session currently has a logged in user. Otherwise returns :const:`None`. """ - request.DATA # Make sure our generic parsing runs first - if getattr(request, 'user', None) and request.user.is_active: # Enforce CSRF validation for session based authentication. resp = CsrfViewMiddleware().process_view(request, None, (), {}) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index b818b446..83d26f1f 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -214,18 +214,15 @@ else: REASON_NO_CSRF_COOKIE = "CSRF cookie not set." REASON_BAD_TOKEN = "CSRF token missing or incorrect." - def _get_failure_view(): """ Returns the view to be used for CSRF rejections """ return get_callable(settings.CSRF_FAILURE_VIEW) - def _get_new_csrf_key(): return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() - def get_token(request): """ Returns the the CSRF token required for a POST form. The token is an @@ -239,7 +236,6 @@ else: request.META["CSRF_COOKIE_USED"] = True return request.META.get("CSRF_COOKIE", None) - def _sanitize_token(token): # Allow only alphanum, and ensure we return a 'str' for the sake of the post # processing middleware. @@ -432,12 +428,13 @@ try: except ImportError: yaml = None + import unittest try: import unittest.skip -except ImportError: # python < 2.7 +except ImportError: # python < 2.7 from unittest import TestCase - import functools + import functools def skip(reason): # Pasted from py27/lib/unittest/case.py @@ -448,20 +445,19 @@ except ImportError: # python < 2.7 if not (isinstance(test_item, type) and issubclass(test_item, TestCase)): @functools.wraps(test_item) def skip_wrapper(*args, **kwargs): - pass + pass test_item = skip_wrapper test_item.__unittest_skip__ = True test_item.__unittest_skip_why__ = reason return test_item return decorator - + unittest.skip = skip -# reverse_lazy (Django 1.4 onwards) + +# xml.etree.parse only throws ParseError for python >= 2.7 try: - from django.core.urlresolvers import reverse_lazy -except: - from django.core.urlresolvers import reverse - from django.utils.functional import lazy - reverse_lazy = lazy(reverse, str) + from xml.etree import ParseError as ETParseError +except ImportError: # python < 2.7 + ETParseError = None diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 51c859cd..f95ec60f 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -21,14 +21,13 @@ __all__ = ( 'ResponseMixin', 'AuthMixin', 'ResourceMixin', - # Reverse URL lookup behavior - 'InstanceMixin', # Model behavior mixins 'ReadModelMixin', 'CreateModelMixin', 'UpdateModelMixin', 'DeleteModelMixin', - 'ListModelMixin' + 'ListModelMixin', + 'PaginatorMixin' ) @@ -39,39 +38,33 @@ class RequestMixin(object): `Mixin` class enabling the use of :class:`request.Request` in your views. """ - parser_classes = () - """ - The set of parsers that the view can handle. - Should be a tuple/list of classes as described in the :mod:`parsers` module. - """ - request_class = Request """ The class to use as a wrapper for the original request object. """ - def get_parsers(self): - """ - Instantiates and returns the list of parsers the request will use. - """ - return [p(self) for p in self.parser_classes] - def create_request(self, request): """ Creates and returns an instance of :class:`request.Request`. - This new instance wraps the `request` passed as a parameter, and use the - parsers set on the view. + This new instance wraps the `request` passed as a parameter, and use + the parsers set on the view. """ - parsers = self.get_parsers() - return self.request_class(request, parsers=parsers) + return self.request_class(request, parsers=self.parsers) @property def _parsed_media_types(self): """ - Returns a list of all the media types that this view can parse. + Return a 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 default parser class. """ - return [p.media_type for p in self.parser_classes] - + return self.parsers[0] + ########## ResponseMixin ########## @@ -80,58 +73,32 @@ class ResponseMixin(object): `Mixin` class enabling the use of :class:`response.Response` in your views. """ - renderer_classes = () + renderers = () """ The set of response renderers that the view can handle. Should be a tuple/list of classes as described in the :mod:`renderers` module. """ - def get_renderers(self): - """ - Instantiates and returns the list of renderers the response will use. - """ - return [r(self) for r in self.renderer_classes] - - def prepare_response(self, response): - """ - Prepares and returns `response`. - This has no effect if the response is not an instance of :class:`response.Response`. - """ - if hasattr(response, 'request') and response.request is None: - response.request = self.request - - # set all the cached headers - for name, value in self.headers.items(): - response[name] = value - - # set the views renderers on the response - response.renderers = self.get_renderers() - return response - @property - def headers(self): + def _rendered_media_types(self): """ - Dictionary of headers to set on the response. - This is useful when the response doesn't exist yet, but you - want to memorize some headers to set on it when it will exist. + Return an list of all the media types that this response can render. """ - if not hasattr(self, '_headers'): - self._headers = {} - return self._headers + return [renderer.media_type for renderer in self.renderers] @property - def _rendered_media_types(self): + def _rendered_formats(self): """ - Return an list of all the media types that this view can render. + Return a list of all the formats that this response can render. """ - return [renderer.media_type for renderer in self.get_renderers()] + return [renderer.format for renderer in self.renderers] @property - def _rendered_formats(self): + def _default_renderer(self): """ - Return a list of all the formats that this view can render. + Return the response's default renderer class. """ - return [renderer.format for renderer in self.get_renderers()] + return self.renderers[0] ########## Auth Mixin ########## @@ -254,30 +221,6 @@ class ResourceMixin(object): else: return None -########## - - -class InstanceMixin(object): - """ - `Mixin` class that is used to identify a `View` class as being the canonical identifier - for the resources it is mapped to. - """ - - @classmethod - def as_view(cls, **initkwargs): - """ - Store the callable object on the resource class that has been associated with this view. - """ - view = super(InstanceMixin, cls).as_view(**initkwargs) - resource = getattr(cls(**initkwargs), 'resource', None) - if resource: - # We do a little dance when we store the view callable... - # we need to store it wrapped in a 1-tuple, so that inspect will treat it - # as a function when we later look it up (rather than turning it into a method). - # This makes sure our URL reversing works ok. - resource.view_callable = (view,) - return view - ########## Model Mixins ########## @@ -411,7 +354,7 @@ class CreateModelMixin(ModelMixin): response = Response(instance, status=status.HTTP_201_CREATED) # Set headers - if hasattr(instance, 'get_absolute_url'): + if hasattr(self.resource, 'url'): response['Location'] = self.resource(self).url(instance) return response diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index d41e07e8..fc4450b7 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -20,6 +20,8 @@ from djangorestframework.compat import yaml from djangorestframework.response import ImmediateResponse from djangorestframework.utils.mediatypes import media_type_matches from xml.etree import ElementTree as ET +from djangorestframework.compat import ETParseError +from xml.parsers.expat import ExpatError import datetime import decimal @@ -43,13 +45,6 @@ class BaseParser(object): media_type = None - def __init__(self, view=None): - """ - Initialize the parser with the ``View`` instance as state, - in case the parser needs to access any metadata on the :obj:`View` object. - """ - self.view = view - def can_handle_request(self, content_type): """ Returns :const:`True` if this parser is able to deal with the given *content_type*. @@ -63,12 +58,12 @@ class BaseParser(object): """ return media_type_matches(self.media_type, content_type) - def parse(self, stream): + def parse(self, stream, meta, upload_handlers): """ Given a *stream* to read from, return the deserialized output. Should return a 2-tuple of (data, files). """ - raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.") + raise NotImplementedError(".parse() Must be overridden to be implemented.") class JSONParser(BaseParser): @@ -78,7 +73,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse(self, stream): + def parse(self, stream, meta, upload_handlers): """ Returns a 2-tuple of `(data, files)`. @@ -93,29 +88,26 @@ class JSONParser(BaseParser): status=status.HTTP_400_BAD_REQUEST) -if yaml: - class YAMLParser(BaseParser): - """ - Parses YAML-serialized data. - """ +class YAMLParser(BaseParser): + """ + Parses YAML-serialized data. + """ - media_type = 'application/yaml' + media_type = 'application/yaml' - def parse(self, stream): - """ - Returns a 2-tuple of `(data, files)`. + def parse(self, stream, meta, upload_handlers): + """ + Returns a 2-tuple of `(data, files)`. - `data` will be an object which is the parsed content of the response. - `files` will always be `None`. - """ - try: - return (yaml.safe_load(stream), None) - except ValueError, exc: - raise ImmediateResponse( - {'detail': 'YAML parse error - %s' % unicode(exc)}, - status=status.HTTP_400_BAD_REQUEST) -else: - YAMLParser = None + `data` will be an object which is the parsed content of the response. + `files` will always be `None`. + """ + try: + return (yaml.safe_load(stream), None) + except ValueError, exc: + raise ImmediateResponse( + {'detail': 'YAML parse error - %s' % unicode(exc)}, + status=status.HTTP_400_BAD_REQUEST) class PlainTextParser(BaseParser): @@ -125,7 +117,7 @@ class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse(self, stream): + def parse(self, stream, meta, upload_handlers): """ Returns a 2-tuple of `(data, files)`. @@ -142,7 +134,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse(self, stream): + def parse(self, stream, meta, upload_handlers): """ Returns a 2-tuple of `(data, files)`. @@ -160,21 +152,20 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse(self, stream): + def parse(self, stream, meta, upload_handlers): """ Returns a 2-tuple of `(data, files)`. `data` will be a :class:`QueryDict` containing all the form parameters. `files` will be a :class:`QueryDict` containing all the form files. """ - upload_handlers = self.view.request._get_upload_handlers() try: - django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers) + parser = DjangoMultiPartParser(meta, stream, upload_handlers) + return parser.parse() except MultiPartParserError, exc: raise ImmediateResponse( {'detail': 'multipart parse error - %s' % unicode(exc)}, status=status.HTTP_400_BAD_REQUEST) - return django_parser.parse() class XMLParser(BaseParser): @@ -184,14 +175,18 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse(self, stream): + def parse(self, stream, meta, upload_handlers): """ Returns a 2-tuple of `(data, files)`. `data` will simply be a string representing the body of the request. `files` will always be `None`. """ - tree = ET.parse(stream) + try: + tree = ET.parse(stream) + except (ExpatError, ETParseError, ValueError), exc: + content = {'detail': 'XML parse error - %s' % unicode(exc)} + raise ImmediateResponse(content, status=status.HTTP_400_BAD_REQUEST) data = self._xml_convert(tree.getroot()) return (data, None) @@ -251,5 +246,7 @@ DEFAULT_PARSERS = ( XMLParser ) -if YAMLParser: - DEFAULT_PARSERS += (YAMLParser,) +if yaml: + DEFAULT_PARSERS += (YAMLParser, ) +else: + YAMLParser = None diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index d24bcfce..8d103025 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -6,20 +6,18 @@ by serializing the output along with documentation regarding the View, output st and providing forms and links depending on the allowed methods, renderers and parsers on the View. """ from django import forms -from django.conf import settings from django.core.serializers.json import DateTimeAwareJSONEncoder from django.template import RequestContext, loader from django.utils import simplejson as json - from djangorestframework.compat import yaml -from djangorestframework.utils import dict2xml, url_resolves, allowed_methods +from djangorestframework.utils import dict2xml from djangorestframework.utils.breadcrumbs import get_breadcrumbs from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches from djangorestframework import VERSION import string -from urllib import quote_plus + __all__ = ( 'BaseRenderer', @@ -156,25 +154,22 @@ class XMLRenderer(BaseRenderer): return dict2xml(obj) -if yaml: - class YAMLRenderer(BaseRenderer): - """ - Renderer which serializes to YAML. - """ +class YAMLRenderer(BaseRenderer): + """ + Renderer which serializes to YAML. + """ - media_type = 'application/yaml' - format = 'yaml' + media_type = 'application/yaml' + format = 'yaml' - def render(self, obj=None, media_type=None): - """ - Renders *obj* into serialized YAML. - """ - if obj is None: - return '' + def render(self, obj=None, media_type=None): + """ + Renders *obj* into serialized YAML. + """ + if obj is None: + return '' - return yaml.safe_dump(obj) -else: - YAMLRenderer = None + return yaml.safe_dump(obj) class TemplateRenderer(BaseRenderer): @@ -218,8 +213,8 @@ class DocumentingTemplateRenderer(BaseRenderer): """ # Find the first valid renderer and render the content. (Don't use another documenting renderer.) - renderers = [renderer for renderer in view.renderer_classes - if not issubclass(renderer, DocumentingTemplateRenderer)] + renderers = [renderer for renderer in view.renderers + if not issubclass(renderer, DocumentingTemplateRenderer)] if not renderers: return '[No renderers were found]' @@ -278,14 +273,14 @@ class DocumentingTemplateRenderer(BaseRenderer): # NB. http://jacobian.org/writing/dynamic-form-generation/ class GenericContentForm(forms.Form): - def __init__(self, request): + def __init__(self, view, request): """We don't know the names of the fields we want to set until the point the form is instantiated, as they are determined by the Resource the form is being created against. Add the fields dynamically.""" super(GenericContentForm, self).__init__() - contenttype_choices = [(media_type, media_type) for media_type in request._parsed_media_types] - initial_contenttype = request._default_parser.media_type + contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types] + initial_contenttype = view._default_parser.media_type self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', choices=contenttype_choices, @@ -298,7 +293,7 @@ class DocumentingTemplateRenderer(BaseRenderer): return None # Okey doke, let's do it - return GenericContentForm(view.request) + return GenericContentForm(view, view.request) def get_name(self): try: @@ -327,13 +322,6 @@ class DocumentingTemplateRenderer(BaseRenderer): put_form_instance = self._get_form_instance(self.view, 'put') post_form_instance = self._get_form_instance(self.view, 'post') - if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL): - login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.view.request.path)) - logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.view.request.path)) - else: - login_url = None - logout_url = None - name = self.get_name() description = self.get_description() @@ -343,21 +331,18 @@ class DocumentingTemplateRenderer(BaseRenderer): context = RequestContext(self.view.request, { 'content': content, 'view': self.view, - 'request': self.view.request, # TODO: remove + 'request': self.view.request, 'response': self.view.response, 'description': description, 'name': name, 'version': VERSION, 'breadcrumblist': breadcrumb_list, - 'allowed_methods': allowed_methods(self.view), + 'allowed_methods': self.view.allowed_methods, 'available_formats': self.view._rendered_formats, 'put_form': put_form_instance, 'post_form': post_form_instance, - 'login_url': login_url, - 'logout_url': logout_url, 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, - 'METHOD_PARAM': getattr(self.view.request, '_METHOD_PARAM', None), - 'ADMIN_MEDIA_PREFIX': getattr(settings, 'ADMIN_MEDIA_PREFIX', None), + 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), }) ret = template.render(context) @@ -415,5 +400,7 @@ DEFAULT_RENDERERS = ( XMLRenderer ) -if YAMLRenderer: - DEFAULT_RENDERERS += (YAMLRenderer,) +if yaml: + DEFAULT_RENDERERS += (YAMLRenderer, ) +else: + YAMLRenderer = None diff --git a/djangorestframework/request.py b/djangorestframework/request.py index e8f2b8c3..a6c23fb8 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -4,15 +4,14 @@ object received in all the views. The wrapped request then offers a richer API, in particular : - - content automatically parsed according to `Content-Type` header, and available as :meth:`.DATA<Request.DATA>` + - content automatically parsed according to `Content-Type` header, + and available as :meth:`.DATA<Request.DATA>` - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ -from djangorestframework.response import ImmediateResponse from djangorestframework import status from djangorestframework.utils.mediatypes import is_form_media_type -from djangorestframework.utils import as_tuple from StringIO import StringIO @@ -20,6 +19,14 @@ from StringIO import StringIO __all__ = ('Request',) +class Empty: + pass + + +def _hasattr(obj, name): + return not getattr(obj, name) is Empty + + class Request(object): """ Wrapper allowing to enhance a standard `HttpRequest` instance. @@ -35,19 +42,29 @@ class Request(object): _CONTENT_PARAM = '_content' def __init__(self, request=None, parsers=None): - self.request = request - if parsers is not None: - self.parsers = parsers + self._request = request + self.parsers = parsers or () + self._data = Empty + self._files = Empty + self._method = Empty + self._content_type = Empty + self._stream = Empty + + def get_parsers(self): + """ + Instantiates and returns the list of parsers the request will use. + """ + return [parser() for parser in self.parsers] @property def method(self): """ Returns the HTTP method. - This allows the `method` to be overridden by using a hidden `form` field - on a form POST request. + This allows the `method` to be overridden by using a hidden `form` + field on a form POST request. """ - if not hasattr(self, '_method'): + if not _hasattr(self, '_method'): self._load_method_and_content_type() return self._method @@ -60,11 +77,20 @@ class Request(object): as it allows the content type to be overridden by using a hidden form field on a form POST request. """ - if not hasattr(self, '_content_type'): + if not _hasattr(self, '_content_type'): self._load_method_and_content_type() return self._content_type @property + def stream(self): + """ + Returns an object that may be used to stream the request content. + """ + if not _hasattr(self, '_stream'): + self._load_stream() + return self._stream + + @property def DATA(self): """ Parses the request body and returns the data. @@ -72,7 +98,7 @@ class Request(object): Similar to ``request.POST``, except that it handles arbitrary parsers, and also works on methods other than POST (eg PUT). """ - if not hasattr(self, '_data'): + if not _hasattr(self, '_data'): self._load_data_and_files() return self._data @@ -83,7 +109,7 @@ class Request(object): Similar to ``request.FILES``, except that it handles arbitrary parsers, and also works on methods other than POST (eg PUT). """ - if not hasattr(self, '_files'): + if not _hasattr(self, '_files'): self._load_data_and_files() return self._files @@ -91,11 +117,11 @@ class Request(object): """ Parses the request content into self.DATA and self.FILES. """ - if not hasattr(self, '_content_type'): + if not _hasattr(self, '_content_type'): self._load_method_and_content_type() - if not hasattr(self, '_data'): - (self._data, self._files) = self._parse(self._get_stream(), self._content_type) + if not _hasattr(self, '_data'): + (self._data, self._files) = self._parse() def _load_method_and_content_type(self): """ @@ -104,100 +130,83 @@ class Request(object): self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', '')) self._perform_form_overloading() # if the HTTP method was not overloaded, we take the raw HTTP method - if not hasattr(self, '_method'): - self._method = self.request.method - - def _get_stream(self): - """ - Returns an object that may be used to stream the request content. - """ + if not _hasattr(self, '_method'): + self._method = self._request.method + def _load_stream(self): try: - content_length = int(self.META.get('CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH'))) + content_length = int(self.META.get('CONTENT_LENGTH', + self.META.get('HTTP_CONTENT_LENGTH'))) except (ValueError, TypeError): content_length = 0 - # TODO: Add 1.3's LimitedStream to compat and use that. - # NOTE: Currently only supports parsing request body as a stream with 1.3 if content_length == 0: - return None - elif hasattr(self, 'read'): - return self - return StringIO(self.raw_post_data) + self._stream = None + elif hasattr(self._request, 'read'): + self._stream = self._request + else: + self._stream = StringIO(self.raw_post_data) def _perform_form_overloading(self): """ - If this is a form POST request, then we need to check if the method and content/content_type have been - overridden by setting them in hidden form fields or not. + If this is a form POST request, then we need to check if the method and + content/content_type have been overridden by setting them in hidden + form fields or not. """ # We only need to use form overloading on form POST requests. - if (not self._USE_FORM_OVERLOADING or self.request.method != 'POST' - or not is_form_media_type(self._content_type)): + if (not self._USE_FORM_OVERLOADING + or self._request.method != 'POST' + or not is_form_media_type(self._content_type)): return # At this point we're committed to parsing the request as form data. - self._data = data = self.POST.copy() - self._files = self.FILES + self._data = self._request.POST + self._files = self._request.FILES # Method overloading - change the method and remove the param from the content. - if self._METHOD_PARAM in data: - # NOTE: unlike `get`, `pop` on a `QueryDict` seems to return a list of values. + if self._METHOD_PARAM in self._data: + # NOTE: `pop` on a `QueryDict` returns a list of values. self._method = self._data.pop(self._METHOD_PARAM)[0].upper() # Content overloading - modify the content type, and re-parse. - if self._CONTENT_PARAM in data and self._CONTENTTYPE_PARAM in data: + if (self._CONTENT_PARAM in self._data and + self._CONTENTTYPE_PARAM in self._data): self._content_type = self._data.pop(self._CONTENTTYPE_PARAM)[0] - stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0]) - (self._data, self._files) = self._parse(stream, self._content_type) + self._stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0]) + (self._data, self._files) = self._parse() - def _parse(self, stream, content_type): + def _parse(self): """ Parse the request content. - May raise a 415 ImmediateResponse (Unsupported Media Type), or a 400 ImmediateResponse (Bad Request). + May raise a 415 ImmediateResponse (Unsupported Media Type), or a + 400 ImmediateResponse (Bad Request). """ - if stream is None or content_type is None: + if self.stream is None or self.content_type is None: return (None, None) - for parser in as_tuple(self.parsers): - if parser.can_handle_request(content_type): - return parser.parse(stream) + for parser in self.get_parsers(): + if parser.can_handle_request(self.content_type): + return parser.parse(self.stream, self.META, self.upload_handlers) - raise ImmediateResponse({ - 'error': 'Unsupported media type in request \'%s\'.' % content_type}, - status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) + self._raise_415_response(self._content_type) - @property - def _parsed_media_types(self): + def _raise_415_response(self, content_type): """ - Return a list of all the media types that this view can parse. + Raise a 415 response if we cannot parse the given content type. """ - return [parser.media_type for parser in self.parsers] + from djangorestframework.response import ImmediateResponse - @property - def _default_parser(self): - """ - Return the view's default parser class. - """ - return self.parsers[0] - - def _get_parsers(self): - if hasattr(self, '_parsers'): - return self._parsers - return () - - def _set_parsers(self, value): - self._parsers = value - - parsers = property(_get_parsers, _set_parsers) + raise ImmediateResponse( + { + 'error': 'Unsupported media type in request \'%s\'.' + % content_type + }, + status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) def __getattr__(self, name): """ - When an attribute is not present on the calling instance, try to get it - from the original request. + Proxy other attributes to the underlying HttpRequest object. """ - if hasattr(self.request, name): - return getattr(self.request, name) - else: - return super(Request, self).__getattribute__(name) + return getattr(self._request, name) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index 8ee49f82..3f2e5a09 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -1,16 +1,13 @@ from django import forms -from django.core.urlresolvers import get_urlconf, get_resolver, NoReverseMatch -from django.db import models - from djangorestframework.response import ImmediateResponse -from djangorestframework.reverse import reverse -from djangorestframework.serializer import Serializer, _SkipField -from djangorestframework.utils import as_tuple, reverse +from djangorestframework.serializer import Serializer +from djangorestframework.utils import as_tuple class BaseResource(Serializer): """ - Base class for all Resource classes, which simply defines the interface they provide. + Base class for all Resource classes, which simply defines the interface + they provide. """ fields = None include = None @@ -19,11 +16,13 @@ class BaseResource(Serializer): def __init__(self, view=None, depth=None, stack=[], **kwargs): super(BaseResource, self).__init__(depth, stack, **kwargs) self.view = view + self.request = getattr(view, 'request', None) def validate_request(self, data, files=None): """ Given the request content return the cleaned, validated content. - Typically raises a :exc:`response.ImmediateResponse` with status code 400 (Bad Request) on failure. + Typically raises a :exc:`response.ImmediateResponse` with status code + 400 (Bad Request) on failure. """ return data @@ -37,7 +36,8 @@ class BaseResource(Serializer): class Resource(BaseResource): """ A Resource determines how a python object maps to some serializable data. - Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets. + Objects that a resource can act on include plain Python object instances, + Django Models, and Django QuerySets. """ # The model attribute refers to the Django Model which this Resource maps to. @@ -220,9 +220,6 @@ class ModelResource(FormResource): Also provides a :meth:`get_bound_form` method which may be used by some renderers. """ - # Auto-register new ModelResource classes into _model_to_resource - #__metaclass__ = _RegisterModelResource - form = None """ The form class that should be used for request validation. @@ -256,7 +253,7 @@ class ModelResource(FormResource): The list of fields to exclude. This is only used if :attr:`fields` is not set. """ - include = ('url',) + include = () """ The list of extra fields to include. This is only used if :attr:`fields` is not set. """ @@ -319,47 +316,6 @@ class ModelResource(FormResource): return form() - def url(self, instance): - """ - Attempts to reverse resolve the url of the given model *instance* for this resource. - - Requires a ``View`` with :class:`mixins.InstanceMixin` to have been created for this resource. - - This method can be overridden if you need to set the resource url reversing explicitly. - """ - - if not hasattr(self, 'view_callable'): - raise _SkipField - - # dis does teh magicks... - urlconf = get_urlconf() - resolver = get_resolver(urlconf) - - possibilities = resolver.reverse_dict.getlist(self.view_callable[0]) - for tuple_item in possibilities: - possibility = tuple_item[0] - # pattern = tuple_item[1] - # Note: defaults = tuple_item[2] for django >= 1.3 - for result, params in possibility: - - #instance_attrs = dict([ (param, getattr(instance, param)) for param in params if hasattr(instance, param) ]) - - instance_attrs = {} - for param in params: - if not hasattr(instance, param): - continue - attr = getattr(instance, param) - if isinstance(attr, models.Model): - instance_attrs[param] = attr.pk - else: - instance_attrs[param] = attr - - try: - return reverse(self.view_callable[0], self.view.request, kwargs=instance_attrs) - except NoReverseMatch: - pass - raise _SkipField - @property def _model_fields_set(self): """ diff --git a/djangorestframework/response.py b/djangorestframework/response.py index 1c260ecb..bedeb6c5 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -27,6 +27,10 @@ from djangorestframework import status __all__ = ('Response', 'ImmediateResponse') +class NotAcceptable(Exception): + pass + + class Response(SimpleTemplateResponse): """ An HttpResponse that may include content that hasn't yet been serialized. @@ -40,25 +44,30 @@ class Response(SimpleTemplateResponse): _ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params _IGNORE_IE_ACCEPT_HEADER = True - def __init__(self, content=None, status=None, request=None, renderers=None, headers=None): + def __init__(self, content=None, status=None, headers=None, view=None, request=None, renderers=None): # First argument taken by `SimpleTemplateResponse.__init__` is template_name, # which we don't need super(Response, self).__init__(None, status=status) - # We need to store our content in raw content to avoid overriding HttpResponse's - # `content` property self.raw_content = content self.has_content_body = content is not None - self.request = request self.headers = headers and headers[:] or [] - if renderers is not None: - self.renderers = renderers + self.view = view + self.request = request + self.renderers = renderers + + def get_renderers(self): + """ + Instantiates and returns the list of renderers the response will use. + """ + return [renderer(self.view) for renderer in self.renderers] @property def rendered_content(self): """ - The final rendered content. Accessing this attribute triggers the complete rendering cycle : - selecting suitable renderer, setting response's actual content type, rendering data. + The final rendered content. Accessing this attribute triggers the + complete rendering cycle: selecting suitable renderer, setting + response's actual content type, rendering data. """ renderer, media_type = self._determine_renderer() @@ -70,6 +79,13 @@ class Response(SimpleTemplateResponse): return renderer.render(self.raw_content, media_type) return renderer.render() + def render(self): + try: + return super(Response, self).render() + except NotAcceptable: + response = self._get_406_response() + return response.render() + @property def status_text(self): """ @@ -88,8 +104,6 @@ class Response(SimpleTemplateResponse): If those are useless, a default value is returned instead. """ request = self.request - if request is None: - return ['*/*'] if self._ACCEPT_QUERY_PARAM and request.GET.get(self._ACCEPT_QUERY_PARAM, None): # Use _accept parameter override @@ -108,70 +122,52 @@ class Response(SimpleTemplateResponse): def _determine_renderer(self): """ - Determines the appropriate renderer for the output, given the list of accepted media types, - and the :attr:`renderers` set on this class. + Determines the appropriate renderer for the output, given the list of + accepted media types, and the :attr:`renderers` set on this class. Returns a 2-tuple of `(renderer, media_type)` - See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + See: RFC 2616, Section 14 + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html """ + + renderers = self.get_renderers() + accepts = self._determine_accept_list() + + # Not acceptable response - Ignore accept header. + if self.status_code == 406: + return (renderers[0], renderers[0].media_type) + # Check the acceptable media types against each renderer, # attempting more specific media types first # NB. The inner loop here isn't as bad as it first looks :) # Worst case is we're looping over len(accept_list) * len(self.renderers) - for media_type_list in order_by_precedence(self._determine_accept_list()): - for renderer in self.renderers: + for media_type_list in order_by_precedence(accepts): + for renderer in renderers: for media_type in media_type_list: if renderer.can_handle_response(media_type): return renderer, media_type # No acceptable renderers were found - raise ImmediateResponse({'detail': 'Could not satisfy the client\'s Accept header', - 'available_types': self._rendered_media_types}, - status=status.HTTP_406_NOT_ACCEPTABLE, - renderers=self.renderers) - - def _get_renderers(self): - if hasattr(self, '_renderers'): - return self._renderers - return () - - def _set_renderers(self, value): - self._renderers = value + raise NotAcceptable - renderers = property(_get_renderers, _set_renderers) - - @property - def _rendered_media_types(self): - """ - Return an list of all the media types that this response can render. - """ - return [renderer.media_type for renderer in self.renderers] - - @property - def _rendered_formats(self): - """ - Return a list of all the formats that this response can render. - """ - return [renderer.format for renderer in self.renderers] - - @property - def _default_renderer(self): - """ - Return the response's default renderer class. - """ - return self.renderers[0] + def _get_406_response(self): + renderer = self.renderers[0] + return Response( + { + 'detail': 'Could not satisfy the client\'s Accept header', + 'available_types': [renderer.media_type + for renderer in self.renderers] + }, + status=status.HTTP_406_NOT_ACCEPTABLE, + view=self.view, request=self.request, renderers=[renderer]) class ImmediateResponse(Response, Exception): """ - A subclass of :class:`Response` used to abort the current request handling. + An exception representing an Response that should be returned immediately. + Any content should be serialized as-is, without being filtered. """ - def __str__(self): - """ - Since this class is also an exception it has to provide a sensible - representation for the cases when it is treated as an exception. - """ - return ('%s must be caught in try/except block, ' - 'and returned as a normal HttpResponse' % self.__class__.__name__) + def __init__(self, *args, **kwargs): + self.response = Response(*args, **kwargs) diff --git a/djangorestframework/reverse.py b/djangorestframework/reverse.py index ad06f966..ba663f98 100644 --- a/djangorestframework/reverse.py +++ b/djangorestframework/reverse.py @@ -2,22 +2,19 @@ Provide reverse functions that return fully qualified URLs """ from django.core.urlresolvers import reverse as django_reverse -from djangorestframework.compat import reverse_lazy as django_reverse_lazy +from django.utils.functional import lazy -def reverse(viewname, request, *args, **kwargs): +def reverse(viewname, *args, **kwargs): """ - Do the same as `django.core.urlresolvers.reverse` but using - *request* to build a fully qualified URL. + Same as `django.core.urlresolvers.reverse`, but optionally takes a request + and returns a fully qualified URL, using the request to get the base URL. """ + request = kwargs.pop('request', None) url = django_reverse(viewname, *args, **kwargs) - return request.build_absolute_uri(url) + if request: + return request.build_absolute_uri(url) + return url -def reverse_lazy(viewname, request, *args, **kwargs): - """ - Do the same as `django.core.urlresolvers.reverse_lazy` but using - *request* to build a fully qualified URL. - """ - url = django_reverse_lazy(viewname, *args, **kwargs) - return request.build_absolute_uri(url) +reverse_lazy = lazy(reverse, str) diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py index f54a554b..7cb3e27b 100644 --- a/djangorestframework/runtests/settings.py +++ b/djangorestframework/runtests/settings.py @@ -53,11 +53,6 @@ MEDIA_ROOT = '' # Examples: "http://media.lawrence.com", "http://example.com/media/" MEDIA_URL = '' -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/' - # Make this unique, and don't share it with anybody. SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy' diff --git a/djangorestframework/templates/djangorestframework/base.html b/djangorestframework/templates/djangorestframework/base.html index fa913c33..f177f883 100644 --- a/djangorestframework/templates/djangorestframework/base.html +++ b/djangorestframework/templates/djangorestframework/base.html @@ -20,8 +20,15 @@ <h1 id="site-name">{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework</a> <span class="version"> v {{ version }}</span>{% endblock %}</h1> </div> <div id="user-tools"> - {% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} <a href='{{ logout_url }}'>Log out</a>{% endif %}{% else %}Anonymous {% if login_url %}<a href='{{ login_url }}'>Log in</a>{% endif %}{% endif %} - {% block userlinks %}{% endblock %} + {% block userlinks %} + {% if user.is_active %} + Welcome, {{ user }}. + <a href='{% url djangorestframework:logout %}?next={{ request.path }}'>Log out</a> + {% else %} + Anonymous + <a href='{% url djangorestframework:login %}?next={{ request.path }}'>Log in</a> + {% endif %} + {% endblock %} </div> {% block nav-global %}{% endblock %} </div> diff --git a/djangorestframework/templates/djangorestframework/login.html b/djangorestframework/templates/djangorestframework/login.html index 07929f0c..248744df 100644 --- a/djangorestframework/templates/djangorestframework/login.html +++ b/djangorestframework/templates/djangorestframework/login.html @@ -17,7 +17,7 @@ <div id="content" class="colM"> <div id="content-main"> - <form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form"> + <form method="post" action="{% url djangorestframework:login %}" id="login-form"> {% csrf_token %} <div class="form-row"> <label for="id_username">Username:</label> {{ form.username }} diff --git a/djangorestframework/tests/__init__.py b/djangorestframework/tests/__init__.py index f664c5c1..641b0277 100644 --- a/djangorestframework/tests/__init__.py +++ b/djangorestframework/tests/__init__.py @@ -10,4 +10,3 @@ for module in modules: exec("from djangorestframework.tests.%s import __doc__ as module_doc" % module) exec("from djangorestframework.tests.%s import *" % module) __test__[module] = module_doc or "" - diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py index e7dfc303..93385493 100644 --- a/djangorestframework/tests/accept.py +++ b/djangorestframework/tests/accept.py @@ -1,3 +1,4 @@ +from django.conf.urls.defaults import patterns, url, include from django.test import TestCase from djangorestframework.compat import RequestFactory @@ -15,9 +16,19 @@ SAFARI_5_0_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/5 OPERA_11_0_MSIE_USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00' OPERA_11_0_OPERA_USER_AGENT = 'Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00' + +urlpatterns = patterns('', + url(r'^api', include('djangorestframework.urls', namespace='djangorestframework')) +) + + class UserAgentMungingTest(TestCase): - """We need to fake up the accept headers when we deal with MSIE. Blergh. - http://www.gethifi.com/blog/browser-rest-http-accept-headers""" + """ + We need to fake up the accept headers when we deal with MSIE. Blergh. + http://www.gethifi.com/blog/browser-rest-http-accept-headers + """ + + urls = 'djangorestframework.tests.accept' def setUp(self): diff --git a/djangorestframework/tests/modelviews.py b/djangorestframework/tests/modelviews.py index 031e65c5..ccd8513f 100644 --- a/djangorestframework/tests/modelviews.py +++ b/djangorestframework/tests/modelviews.py @@ -1,5 +1,4 @@ from django.conf.urls.defaults import patterns, url -from django.test import TestCase from django.forms import ModelForm from django.contrib.auth.models import Group, User from djangorestframework.resources import ModelResource @@ -7,18 +6,22 @@ from djangorestframework.views import ListOrCreateModelView, InstanceModelView from djangorestframework.tests.models import CustomUser from djangorestframework.tests.testcases import TestModelsTestCase + class GroupResource(ModelResource): model = Group + class UserForm(ModelForm): class Meta: model = User exclude = ('last_login', 'date_joined') + class UserResource(ModelResource): model = User form = UserForm + class CustomUserResource(ModelResource): model = CustomUser diff --git a/djangorestframework/tests/oauthentication.py b/djangorestframework/tests/oauthentication.py index b4bcf2fa..29f2c44e 100644 --- a/djangorestframework/tests/oauthentication.py +++ b/djangorestframework/tests/oauthentication.py @@ -27,7 +27,7 @@ else: urlpatterns = patterns('', url(r'^$', oauth_required(ClientView.as_view())), url(r'^oauth/', include('oauth_provider.urls')), - url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'), + url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')), ) diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py index 1ecc1760..0f36cece 100644 --- a/djangorestframework/tests/parsers.py +++ b/djangorestframework/tests/parsers.py @@ -132,17 +132,18 @@ # self.assertEqual(files['file1'].read(), 'blablabla') from StringIO import StringIO -from cgi import parse_qs from django import forms from django.test import TestCase from djangorestframework.parsers import FormParser from djangorestframework.parsers import XMLParser import datetime + class Form(forms.Form): field1 = forms.CharField(max_length=3) field2 = forms.CharField() + class TestFormParser(TestCase): def setUp(self): self.string = "field1=abc&field2=defghijk" @@ -152,10 +153,11 @@ class TestFormParser(TestCase): parser = FormParser(None) stream = StringIO(self.string) - (data, files) = parser.parse(stream) + (data, files) = parser.parse(stream, {}, []) self.assertEqual(Form(data).is_valid(), True) + class TestXMLParser(TestCase): def setUp(self): self._input = StringIO( @@ -163,13 +165,13 @@ class TestXMLParser(TestCase): '<root>' '<field_a>121.0</field_a>' '<field_b>dasd</field_b>' - '<field_c></field_c>' + '<field_c></field_c>' '<field_d>2011-12-25 12:45:00</field_d>' '</root>' - ) - self._data = { + ) + self._data = { 'field_a': 121, - 'field_b': 'dasd', + 'field_b': 'dasd', 'field_c': None, 'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00) } @@ -183,21 +185,21 @@ class TestXMLParser(TestCase): '</sub_data_list>' '<name>name</name>' '</root>' - ) + ) self._complex_data = { - "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00), - "name": "name", + "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00), + "name": "name", "sub_data_list": [ { - "sub_id": 1, + "sub_id": 1, "sub_name": "first" - }, + }, { - "sub_id": 2, + "sub_id": 2, "sub_name": "second" } ] - } + } def test_parse(self): parser = XMLParser(None) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index cc211dce..8eb78b74 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -1,21 +1,180 @@ import re +from django.conf.urls.defaults import patterns, url, include from django.test import TestCase -from django.conf.urls.defaults import patterns, url -from django.test import TestCase - +from djangorestframework import status +from djangorestframework.compat import View as DjangoView from djangorestframework.response import Response +from djangorestframework.mixins import ResponseMixin from djangorestframework.views import View from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer -from djangorestframework.parsers import JSONParser, YAMLParser, XMLParser +from djangorestframework.parsers import YAMLParser, XMLParser from StringIO import StringIO import datetime from decimal import Decimal +DUMMYSTATUS = status.HTTP_200_OK +DUMMYCONTENT = 'dummycontent' + +RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x +RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x + + +class RendererA(BaseRenderer): + media_type = 'mock/renderera' + format = "formata" + + def render(self, obj=None, media_type=None): + return RENDERER_A_SERIALIZER(obj) + + +class RendererB(BaseRenderer): + media_type = 'mock/rendererb' + format = "formatb" + + def render(self, obj=None, media_type=None): + return RENDERER_B_SERIALIZER(obj) + + +class MockView(ResponseMixin, DjangoView): + renderers = (RendererA, RendererB) + + def get(self, request, **kwargs): + response = Response(DUMMYSTATUS, DUMMYCONTENT) + return self.render(response) + + +class MockGETView(View): + + def get(self, request, **kwargs): + return {'foo': ['bar', 'baz']} + + +class HTMLView(View): + renderers = (DocumentingHTMLRenderer, ) + + def get(self, request, **kwargs): + return 'text' + + +class HTMLView1(View): + renderers = (DocumentingHTMLRenderer, JSONRenderer) + + def get(self, request, **kwargs): + return 'text' + +urlpatterns = patterns('', + url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])), + url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])), + url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])), + url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])), + url(r'^html$', HTMLView.as_view()), + url(r'^html1$', HTMLView1.as_view()), + url(r'^api', include('djangorestframework.urls', namespace='djangorestframework')) +) + + +class RendererIntegrationTests(TestCase): + """ + End-to-end testing of renderers using an RendererMixin on a generic view. + """ + + urls = 'djangorestframework.tests.renderers' + + def test_default_renderer_serializes_content(self): + """If the Accept header is not set the default renderer should serialize the response.""" + resp = self.client.get('/') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_head_method_serializes_no_content(self): + """No response must be included in HEAD requests.""" + resp = self.client.head('/') + self.assertEquals(resp.status_code, DUMMYSTATUS) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, '') + + def test_default_renderer_serializes_content_on_accept_any(self): + """If the Accept header is set to */* the default renderer should serialize the response.""" + resp = self.client.get('/', HTTP_ACCEPT='*/*') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for the default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_non_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for a non-default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_on_accept_query(self): + """The '_accept' query string should behave in the same way as the Accept header.""" + resp = self.client.get('/?_accept=%s' % RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_unsatisfiable_accept_header_on_request_returns_406_status(self): + """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" + resp = self.client.get('/', HTTP_ACCEPT='foo/bar') + self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE) + + def test_specified_renderer_serializes_content_on_format_query(self): + """If a 'format' query is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_on_format_kwargs(self): + """If a 'format' keyword arg is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/something.formatb') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): + """If both a 'format' query and a matching Accept header specified, + the renderer with the matching format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format, + HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_conflicting_format_query_and_accept_ignores_accept(self): + """If a 'format' query is specified that does not match the Accept + header, we should only honor the 'format' query string.""" + resp = self.client.get('/?format=%s' % RendererB.format, + HTTP_ACCEPT='dummy') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_bla(self): # What the f***? + resp = self.client.get('/?format=formatb', + HTTP_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' @@ -27,6 +186,7 @@ def strip_trailing_whitespace(content): """ return re.sub(' +\n', '\n', content) + class JSONRendererTests(TestCase): """ Tests specific to the JSON Renderer @@ -51,30 +211,16 @@ class JSONRendererTests(TestCase): content = renderer.render(obj, 'application/json; indent=2') self.assertEquals(strip_trailing_whitespace(content), _indented_repr) - def test_render_and_parse(self): - """ - Test rendering and then parsing returns the original object. - IE obj -> render -> parse -> obj. - """ - obj = {'foo': ['bar', 'baz']} - - renderer = JSONRenderer(None) - parser = JSONParser(None) - - content = renderer.render(obj, 'application/json') - (data, files) = parser.parse(StringIO(content)) - self.assertEquals(obj, data) - class MockGETView(View): - def get(self, request, **kwargs): + def get(self, request, *args, **kwargs): return Response({'foo': ['bar', 'baz']}) urlpatterns = patterns('', - url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), - url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), + url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])), + url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])), ) @@ -149,22 +295,21 @@ if YAMLRenderer: self.assertEquals(obj, data) - class XMLRendererTestCase(TestCase): """ Tests specific to the XML Renderer """ _complex_data = { - "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00), - "name": "name", + "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00), + "name": "name", "sub_data_list": [ { - "sub_id": 1, + "sub_id": 1, "sub_name": "first" - }, + }, { - "sub_id": 2, + "sub_id": 2, "sub_name": "second" } ] @@ -219,12 +364,12 @@ class XMLRendererTestCase(TestCase): renderer = XMLRenderer(None) content = renderer.render({'field': None}, 'application/xml') self.assertXMLContains(content, '<field></field>') - + def test_render_complex_data(self): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer(None) content = renderer.render(self._complex_data, 'application/xml') self.assertXMLContains(content, '<sub_name>first</sub_name>') self.assertXMLContains(content, '<sub_name>second</sub_name>') @@ -233,9 +378,9 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer(None) content = StringIO(renderer.render(self._complex_data, 'application/xml')) - + parser = XMLParser(None) complex_data_out, dummy = parser.parse(content) error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out)) @@ -245,4 +390,3 @@ class XMLRendererTestCase(TestCase): self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>')) self.assertTrue(xml.endswith('</root>')) self.assertTrue(string in xml, '%r not in %r' % (string, xml)) - diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py index c92d3f5f..7e289536 100644 --- a/djangorestframework/tests/request.py +++ b/djangorestframework/tests/request.py @@ -4,205 +4,214 @@ Tests for content parsing, and form-overloaded content parsing. from django.conf.urls.defaults import patterns from django.contrib.auth.models import User from django.test import TestCase, Client +from django.utils import simplejson as json + from djangorestframework import status from djangorestframework.authentication import UserLoggedInAuthentication -from djangorestframework.compat import RequestFactory -from djangorestframework.mixins import RequestMixin -from djangorestframework.parsers import FormParser, MultiPartParser, \ - PlainTextParser, JSONParser +from djangorestframework.utils import RequestFactory +from djangorestframework.parsers import ( + FormParser, + MultiPartParser, + PlainTextParser, + JSONParser +) from djangorestframework.request import Request from djangorestframework.response import Response -from djangorestframework.request import Request from djangorestframework.views import View -class RequestTestCase(TestCase): - - def build_request(self, method, *args, **kwargs): - factory = RequestFactory() - method = getattr(factory, method) - original_request = method(*args, **kwargs) - return Request(original_request) - +factory = RequestFactory() -class TestMethodOverloading(RequestTestCase): - def test_standard_behaviour_determines_GET(self): - """GET requests identified""" - request = self.build_request('get', '/') +class TestMethodOverloading(TestCase): + def test_GET_method(self): + """ + GET requests identified. + """ + request = factory.get('/') self.assertEqual(request.method, 'GET') - def test_standard_behaviour_determines_POST(self): - """POST requests identified""" - request = self.build_request('post', '/') + def test_POST_method(self): + """ + POST requests identified. + """ + request = factory.post('/') self.assertEqual(request.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""" - request = self.build_request('post', '/', {Request._METHOD_PARAM: 'DELETE'}) - self.assertEqual(request.method, 'DELETE') - - def test_HEAD_is_a_valid_method(self): - """HEAD requests identified""" - request = request = self.build_request('head', '/') + def test_HEAD_method(self): + """ + HEAD requests identified. + """ + request = factory.head('/') self.assertEqual(request.method, 'HEAD') + def test_overloaded_method(self): + """ + POST requests can be overloaded to another method by setting a + reserved form field + """ + request = factory.post('/', {Request._METHOD_PARAM: 'DELETE'}) + self.assertEqual(request.method, 'DELETE') -class TestContentParsing(RequestTestCase): - - def build_request(self, method, *args, **kwargs): - factory = RequestFactory() - parsers = kwargs.pop('parsers', None) - method = getattr(factory, method) - original_request = method(*args, **kwargs) - rkwargs = {} - if parsers is not None: - rkwargs['parsers'] = parsers - request = Request(original_request, **rkwargs) - # TODO: Just a hack because the parsers need a view. This will be fixed in the future - class Obj(object): pass - obj = Obj() - obj.request = request - for p in request.parsers: - p.view = obj - return request - + +class TestContentParsing(TestCase): def test_standard_behaviour_determines_no_content_GET(self): - """Ensure request.DATA returns None for GET request with no content.""" - request = self.build_request('get', '/') + """ + Ensure request.DATA returns None for GET request with no content. + """ + request = factory.get('/') self.assertEqual(request.DATA, None) def test_standard_behaviour_determines_no_content_HEAD(self): - """Ensure request.DATA returns None for HEAD request.""" - request = self.build_request('head', '/') + """ + Ensure request.DATA returns None for HEAD request. + """ + request = factory.head('/') self.assertEqual(request.DATA, None) def test_standard_behaviour_determines_form_content_POST(self): - """Ensure request.DATA returns content for POST request with form content.""" - form_data = {'qwerty': 'uiop'} - parsers = (FormParser(), MultiPartParser()) - - request = self.build_request('post', '/', data=form_data, parsers=parsers) - self.assertEqual(request.DATA.items(), form_data.items()) + """ + Ensure request.DATA returns content for POST request with form content. + """ + data = {'qwerty': 'uiop'} + parsers = (FormParser, MultiPartParser) + request = factory.post('/', data, parser=parsers) + self.assertEqual(request.DATA.items(), data.items()) def test_standard_behaviour_determines_non_form_content_POST(self): - """Ensure request.DATA returns content for POST request with non-form content.""" + """ + Ensure request.DATA returns content for POST request with + non-form content. + """ content = 'qwerty' content_type = 'text/plain' - parsers = (PlainTextParser(),) - - request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers) + parsers = (PlainTextParser,) + request = factory.post('/', content, content_type=content_type, + parsers=parsers) self.assertEqual(request.DATA, content) def test_standard_behaviour_determines_form_content_PUT(self): - """Ensure request.DATA returns content for PUT request with form content.""" - form_data = {'qwerty': 'uiop'} - parsers = (FormParser(), MultiPartParser()) - - request = self.build_request('put', '/', data=form_data, parsers=parsers) - self.assertEqual(request.DATA.items(), form_data.items()) + """ + Ensure request.DATA returns content for PUT request with form content. + """ + data = {'qwerty': 'uiop'} + parsers = (FormParser, MultiPartParser) + request = factory.put('/', data, parsers=parsers) + self.assertEqual(request.DATA.items(), data.items()) def test_standard_behaviour_determines_non_form_content_PUT(self): - """Ensure request.DATA returns content for PUT request with non-form content.""" + """ + Ensure request.DATA returns content for PUT request with + non-form content. + """ content = 'qwerty' content_type = 'text/plain' - parsers = (PlainTextParser(),) - - request = self.build_request('put', '/', content, content_type=content_type, parsers=parsers) + parsers = (PlainTextParser, ) + request = factory.put('/', content, content_type=content_type, + parsers=parsers) self.assertEqual(request.DATA, content) def test_overloaded_behaviour_allows_content_tunnelling(self): - """Ensure request.DATA returns content for overloaded POST request""" + """ + Ensure request.DATA returns content for overloaded POST request. + """ content = 'qwerty' content_type = 'text/plain' - form_data = {Request._CONTENT_PARAM: content, - Request._CONTENTTYPE_PARAM: content_type} - parsers = (PlainTextParser(),) - - request = self.build_request('post', '/', form_data, parsers=parsers) + data = { + Request._CONTENT_PARAM: content, + Request._CONTENTTYPE_PARAM: content_type + } + parsers = (PlainTextParser, ) + request = factory.post('/', data, parsers=parsers) self.assertEqual(request.DATA, content) def test_accessing_post_after_data_form(self): - """Ensures request.POST can be accessed after request.DATA in form request""" - form_data = {'qwerty': 'uiop'} - parsers = (FormParser(), MultiPartParser()) - - request = self.build_request('post', '/', data=form_data) - self.assertEqual(request.DATA.items(), form_data.items()) - self.assertEqual(request.POST.items(), form_data.items()) + """ + Ensures request.POST can be accessed after request.DATA in + form request. + """ + data = {'qwerty': 'uiop'} + request = factory.post('/', data=data) + self.assertEqual(request.DATA.items(), data.items()) + self.assertEqual(request.POST.items(), data.items()) def test_accessing_post_after_data_for_json(self): - """Ensures request.POST can be accessed after request.DATA in json request""" - from django.utils import simplejson as json - + """ + Ensures request.POST can be accessed after request.DATA in + json request. + """ data = {'qwerty': 'uiop'} content = json.dumps(data) content_type = 'application/json' - parsers = (JSONParser(),) + parsers = (JSONParser, ) - request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers) + request = factory.post('/', content, content_type=content_type, + parsers=parsers) self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.POST.items(), []) def test_accessing_post_after_data_for_overloaded_json(self): - """Ensures request.POST can be accessed after request.DATA in overloaded json request""" - from django.utils import simplejson as json - + """ + Ensures request.POST can be accessed after request.DATA in overloaded + json request. + """ data = {'qwerty': 'uiop'} content = json.dumps(data) content_type = 'application/json' - parsers = (JSONParser(),) + parsers = (JSONParser, ) form_data = {Request._CONTENT_PARAM: content, Request._CONTENTTYPE_PARAM: content_type} - request = self.build_request('post', '/', data=form_data, parsers=parsers) + request = factory.post('/', form_data, parsers=parsers) self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.POST.items(), form_data.items()) def test_accessing_data_after_post_form(self): - """Ensures request.DATA can be accessed after request.POST in form request""" - form_data = {'qwerty': 'uiop'} + """ + Ensures request.DATA can be accessed after request.POST in + form request. + """ + data = {'qwerty': 'uiop'} parsers = (FormParser, MultiPartParser) - request = self.build_request('post', '/', data=form_data, parsers=parsers) + request = factory.post('/', data, parsers=parsers) - self.assertEqual(request.POST.items(), form_data.items()) - self.assertEqual(request.DATA.items(), form_data.items()) + self.assertEqual(request.POST.items(), data.items()) + self.assertEqual(request.DATA.items(), data.items()) def test_accessing_data_after_post_for_json(self): - """Ensures request.DATA can be accessed after request.POST in json request""" - from django.utils import simplejson as json - + """ + Ensures request.DATA can be accessed after request.POST in + json request. + """ data = {'qwerty': 'uiop'} content = json.dumps(data) content_type = 'application/json' - parsers = (JSONParser(),) - - request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers) - post_items = request.POST.items() - - self.assertEqual(len(post_items), 1) - self.assertEqual(len(post_items[0]), 2) - self.assertEqual(post_items[0][0], content) + parsers = (JSONParser, ) + request = factory.post('/', content, content_type=content_type, + parsers=parsers) + self.assertEqual(request.POST.items(), []) self.assertEqual(request.DATA.items(), data.items()) def test_accessing_data_after_post_for_overloaded_json(self): - """Ensures request.DATA can be accessed after request.POST in overloaded json request""" - from django.utils import simplejson as json - + """ + Ensures request.DATA can be accessed after request.POST in overloaded + json request + """ data = {'qwerty': 'uiop'} content = json.dumps(data) content_type = 'application/json' - parsers = (JSONParser(),) + parsers = (JSONParser, ) form_data = {Request._CONTENT_PARAM: content, Request._CONTENTTYPE_PARAM: content_type} - request = self.build_request('post', '/', data=form_data, parsers=parsers) + request = factory.post('/', form_data, parsers=parsers) self.assertEqual(request.POST.items(), form_data.items()) self.assertEqual(request.DATA.items(), data.items()) class MockView(View): authentication = (UserLoggedInAuthentication,) + def post(self, request): if request.POST.get('example') is not None: return Response(status=status.HTTP_200_OK) @@ -223,17 +232,19 @@ class TestContentParsingWithAuthentication(TestCase): self.email = 'lennon@thebeatles.com' self.password = 'password' self.user = User.objects.create_user(self.username, self.email, self.password) - self.req = RequestFactory() - def test_user_logged_in_authentication_has_post_when_not_logged_in(self): - """Ensures request.POST exists after UserLoggedInAuthentication when user doesn't log in""" + def test_user_logged_in_authentication_has_POST_when_not_logged_in(self): + """ + Ensures request.POST exists after UserLoggedInAuthentication when user + doesn't log in. + """ content = {'example': 'example'} response = self.client.post('/', content) - self.assertEqual(status.HTTP_200_OK, response.status_code, "POST data is malformed") + self.assertEqual(status.HTTP_200_OK, response.status_code) response = self.csrf_client.post('/', content) - self.assertEqual(status.HTTP_200_OK, response.status_code, "POST data is malformed") + self.assertEqual(status.HTTP_200_OK, response.status_code) # def test_user_logged_in_authentication_has_post_when_logged_in(self): # """Ensures request.POST exists after UserLoggedInAuthentication when user does log in""" diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py index 95603680..4cd000bd 100644 --- a/djangorestframework/tests/response.py +++ b/djangorestframework/tests/response.py @@ -1,18 +1,19 @@ import json import unittest -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns, url, include from django.test import TestCase from djangorestframework.response import Response, ImmediateResponse -from djangorestframework.mixins import ResponseMixin from djangorestframework.views import View -from djangorestframework.compat import View as DjangoView -from djangorestframework.renderers import BaseRenderer, DEFAULT_RENDERERS from djangorestframework.compat import RequestFactory from djangorestframework import status -from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ - XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer +from djangorestframework.renderers import ( + BaseRenderer, + JSONRenderer, + DocumentingHTMLRenderer, + DEFAULT_RENDERERS +) class TestResponseDetermineRenderer(TestCase): @@ -20,7 +21,7 @@ class TestResponseDetermineRenderer(TestCase): def get_response(self, url='', accept_list=[], renderers=[]): kwargs = {} if accept_list is not None: - kwargs['HTTP_ACCEPT'] = HTTP_ACCEPT=','.join(accept_list) + kwargs['HTTP_ACCEPT'] = ','.join(accept_list) request = RequestFactory().get(url, **kwargs) return Response(request=request, renderers=renderers) @@ -43,7 +44,7 @@ class TestResponseDetermineRenderer(TestCase): """ response = self.get_response(accept_list=None) self.assertEqual(response._determine_accept_list(), ['*/*']) - + def test_determine_accept_list_overriden_header(self): """ Test Accept header overriding. @@ -81,7 +82,7 @@ class TestResponseDetermineRenderer(TestCase): renderer, media_type = response._determine_renderer() self.assertEqual(media_type, '*/*') self.assertTrue(renderer, prenderer) - + def test_determine_renderer_no_renderer(self): """ Test determine renderer when no renderer can satisfy the Accept list. @@ -94,14 +95,14 @@ class TestResponseDetermineRenderer(TestCase): class TestResponseRenderContent(TestCase): - + def get_response(self, url='', accept_list=[], content=None): request = RequestFactory().get(url, HTTP_ACCEPT=','.join(accept_list)) return Response(request=request, content=content, renderers=[r() for r in DEFAULT_RENDERERS]) def test_render(self): """ - Test rendering simple data to json. + Test rendering simple data to json. """ content = {'a': 1, 'b': [1, 2, 3]} content_type = 'application/json' @@ -134,34 +135,33 @@ class RendererB(BaseRenderer): return RENDERER_B_SERIALIZER(obj) -class MockView(ResponseMixin, DjangoView): - renderer_classes = (RendererA, RendererB) +class MockView(View): + renderers = (RendererA, RendererB) def get(self, request, **kwargs): - response = Response(DUMMYCONTENT, status=DUMMYSTATUS) - self.response = self.prepare_response(response) - return self.response + return Response(DUMMYCONTENT, status=DUMMYSTATUS) class HTMLView(View): - renderer_classes = (DocumentingHTMLRenderer, ) + renderers = (DocumentingHTMLRenderer, ) def get(self, request, **kwargs): return Response('text') class HTMLView1(View): - renderer_classes = (DocumentingHTMLRenderer, JSONRenderer) + renderers = (DocumentingHTMLRenderer, JSONRenderer) def get(self, request, **kwargs): - return Response('text') + return Response('text') urlpatterns = patterns('', - url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), - url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])), + url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework')) ) @@ -257,13 +257,6 @@ class RendererIntegrationTests(TestCase): self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEquals(resp.status_code, DUMMYSTATUS) - def test_bla(self): - resp = self.client.get('/?format=formatb', - HTTP_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8') - self.assertEquals(resp['Content-Type'], RendererB.media_type) - self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEquals(resp.status_code, DUMMYSTATUS) - class Issue122Tests(TestCase): """ @@ -275,10 +268,10 @@ class Issue122Tests(TestCase): """ Test if no infinite recursion occurs. """ - resp = self.client.get('/html') - + self.client.get('/html') + def test_html_renderer_is_first(self): """ Test if no infinite recursion occurs. """ - resp = self.client.get('/html1') + self.client.get('/html1') diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index c2388d62..8d467513 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -16,7 +16,8 @@ class MyView(View): renderers = (JSONRenderer, ) def get(self, request): - return Response(reverse('another', request)) + return Response(reverse('myview', request=request)) + urlpatterns = patterns('', url(r'^myview$', MyView.as_view(), name='myview'), diff --git a/djangorestframework/tests/views.py b/djangorestframework/tests/views.py index d4189087..00bce002 100644 --- a/djangorestframework/tests/views.py +++ b/djangorestframework/tests/views.py @@ -1,16 +1,17 @@ -from django.conf.urls.defaults import patterns, url +from django.core.urlresolvers import reverse +from django.conf.urls.defaults import patterns, url, include from django.http import HttpResponse from django.test import TestCase -from django.test import Client from django import forms from django.db import models +from django.utils import simplejson as json -from djangorestframework.views import View -from djangorestframework.parsers import JSONParser from djangorestframework.resources import ModelResource -from djangorestframework.views import ListOrCreateModelView, InstanceModelView - -from StringIO import StringIO +from djangorestframework.views import ( + View, + ListOrCreateModelView, + InstanceModelView +) class MockView(View): @@ -24,6 +25,7 @@ class MockViewFinal(View): def final(self, request, response, *args, **kwargs): return HttpResponse('{"test": "passed"}', content_type="application/json") + class ResourceMockView(View): """This is a resource-based mock view""" @@ -34,6 +36,7 @@ class ResourceMockView(View): form = MockForm + class MockResource(ModelResource): """This is a mock model-based resource""" @@ -45,16 +48,16 @@ class MockResource(ModelResource): model = MockResourceModel fields = ('foo', 'bar', 'baz') -urlpatterns = patterns('djangorestframework.utils.staticviews', - url(r'^accounts/login$', 'api_login'), - url(r'^accounts/logout$', 'api_logout'), +urlpatterns = patterns('', url(r'^mock/$', MockView.as_view()), url(r'^mock/final/$', MockViewFinal.as_view()), url(r'^resourcemock/$', ResourceMockView.as_view()), url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)), url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MockResource)), + url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')), ) + class BaseViewTests(TestCase): """Test the base view class of djangorestframework""" urls = 'djangorestframework.tests.views' @@ -62,8 +65,7 @@ class BaseViewTests(TestCase): def test_view_call_final(self): response = self.client.options('/mock/final/') self.assertEqual(response['Content-Type'].split(';')[0], "application/json") - parser = JSONParser(None) - (data, files) = parser.parse(StringIO(response.content)) + data = json.loads(response.content) self.assertEqual(data['test'], 'passed') def test_options_method_simple_view(self): @@ -77,9 +79,9 @@ class BaseViewTests(TestCase): self._verify_options_response(response, name='Resource Mock', description='This is a resource-based mock view', - fields={'foo':'BooleanField', - 'bar':'IntegerField', - 'baz':'CharField', + fields={'foo': 'BooleanField', + 'bar': 'IntegerField', + 'baz': 'CharField', }) def test_options_method_model_resource_list_view(self): @@ -87,9 +89,9 @@ class BaseViewTests(TestCase): self._verify_options_response(response, name='Mock List', description='This is a mock model-based resource', - fields={'foo':'BooleanField', - 'bar':'IntegerField', - 'baz':'CharField', + fields={'foo': 'BooleanField', + 'bar': 'IntegerField', + 'baz': 'CharField', }) def test_options_method_model_resource_detail_view(self): @@ -97,17 +99,16 @@ class BaseViewTests(TestCase): self._verify_options_response(response, name='Mock Instance', description='This is a mock model-based resource', - fields={'foo':'BooleanField', - 'bar':'IntegerField', - 'baz':'CharField', + fields={'foo': 'BooleanField', + 'bar': 'IntegerField', + 'baz': 'CharField', }) def _verify_options_response(self, response, name, description, fields=None, status=200, mime_type='application/json'): self.assertEqual(response.status_code, status) self.assertEqual(response['Content-Type'].split(';')[0], mime_type) - parser = JSONParser(None) - (data, files) = parser.parse(StringIO(response.content)) + data = json.loads(response.content) self.assertTrue('application/json' in data['renders']) self.assertEqual(name, data['name']) self.assertEqual(description, data['description']) @@ -123,15 +124,12 @@ class ExtraViewsTests(TestCase): def test_login_view(self): """Ensure the login view exists""" - response = self.client.get('/accounts/login') + response = self.client.get(reverse('djangorestframework:login')) self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') def test_logout_view(self): """Ensure the logout view exists""" - response = self.client.get('/accounts/logout') + response = self.client.get(reverse('djangorestframework:logout')) self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') - - # TODO: Add login/logout behaviour tests - diff --git a/djangorestframework/urls.py b/djangorestframework/urls.py index 5c797bcd..3fa813ea 100644 --- a/djangorestframework/urls.py +++ b/djangorestframework/urls.py @@ -1,6 +1,9 @@ -from django.conf.urls.defaults import patterns +from django.conf.urls.defaults import patterns, url -urlpatterns = patterns('djangorestframework.utils.staticviews', - (r'^accounts/login/$', 'api_login'), - (r'^accounts/logout/$', 'api_logout'), + +template_name = {'template_name': 'djangorestframework/login.html'} + +urlpatterns = patterns('django.contrib.auth.views', + url(r'^login/$', 'login', template_name, name='login'), + url(r'^logout/$', 'logout', template_name, name='logout'), ) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index afef4f19..9d250be2 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,24 +1,18 @@ -import django from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator -from django.core.urlresolvers import resolve, reverse as django_reverse -from django.conf import settings +from django.core.urlresolvers import resolve from djangorestframework.compat import StringIO +from djangorestframework.compat import RequestFactory as DjangoRequestFactory +from djangorestframework.request import Request import re import xml.etree.ElementTree as ET -#def admin_media_prefix(request): -# """Adds the ADMIN_MEDIA_PREFIX to the request context.""" -# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX} - -from mediatypes import media_type_matches, is_form_media_type -from mediatypes import add_media_type_param, get_media_type_params, order_by_precedence - 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 an object which may be a list/tuple, another object, or None, @@ -49,45 +43,6 @@ def url_resolves(url): return True -def allowed_methods(view): - """ - Return the list of uppercased allowed HTTP methods on `view`. - """ - return [method.upper() for method in view.http_method_names if hasattr(view, method)] - - -# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml -#class object_dict(dict): -# """object view of dict, you can -# >>> a = object_dict() -# >>> a.fish = 'fish' -# >>> a['fish'] -# 'fish' -# >>> a['water'] = 'water' -# >>> a.water -# 'water' -# >>> a.test = {'value': 1} -# >>> a.test2 = object_dict({'name': 'test2', 'value': 2}) -# >>> a.test, a.test2.name, a.test2.value -# (1, 'test2', 2) -# """ -# def __init__(self, initd=None): -# if initd is None: -# initd = {} -# dict.__init__(self, initd) -# -# def __getattr__(self, item): -# d = self.__getitem__(item) -# # if value is the only key in object, you can omit it -# if isinstance(d, dict) and 'value' in d and len(d) == 1: -# return d['value'] -# else: -# return d -# -# def __setattr__(self, item, value): -# self.__setitem__(item, value) - - # From xml2dict class XML2Dict(object): @@ -99,24 +54,23 @@ class XML2Dict(object): # Save attrs and text, hope there will not be a child with same name if node.text: node_tree = node.text - for (k,v) in node.attrib.items(): - k,v = self._namespace_split(k, v) + for (k, v) in node.attrib.items(): + k, v = self._namespace_split(k, v) node_tree[k] = v #Save childrens for child in node.getchildren(): tag, tree = self._namespace_split(child.tag, self._parse_node(child)) - if tag not in node_tree: # the first time, so store it in dict + if tag not in node_tree: # the first time, so store it in dict node_tree[tag] = tree continue old = node_tree[tag] if not isinstance(old, list): node_tree.pop(tag) - node_tree[tag] = [old] # multi times, so change old dict to a list - node_tree[tag].append(tree) # add the new one + node_tree[tag] = [old] # multi times, so change old dict to a list + node_tree[tag].append(tree) # add the new one return node_tree - def _namespace_split(self, tag, value): """ Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients' @@ -179,23 +133,41 @@ class XMLRenderer(): xml.endDocument() return stream.getvalue() + def dict2xml(input): return XMLRenderer().dict2xml(input) -def reverse(viewname, request, *args, **kwargs): +class RequestFactory(DjangoRequestFactory): """ - Do the same as :py:func:`django.core.urlresolvers.reverse` but using - *request* to build a fully qualified URL. + Replicate RequestFactory, but return Request, not HttpRequest. """ - return request.build_absolute_uri(django_reverse(viewname, *args, **kwargs)) - -if django.VERSION >= (1, 4): - from django.core.urlresolvers import reverse_lazy as django_reverse_lazy - - def reverse_lazy(viewname, request, *args, **kwargs): - """ - Do the same as :py:func:`django.core.urlresolvers.reverse_lazy` but using - *request* to build a fully qualified URL. - """ - return request.build_absolute_uri(django_reverse_lazy(viewname, *args, **kwargs)) + def get(self, *args, **kwargs): + parsers = kwargs.pop('parsers', None) + request = super(RequestFactory, self).get(*args, **kwargs) + return Request(request, parsers) + + def post(self, *args, **kwargs): + parsers = kwargs.pop('parsers', None) + request = super(RequestFactory, self).post(*args, **kwargs) + return Request(request, parsers) + + def put(self, *args, **kwargs): + parsers = kwargs.pop('parsers', None) + request = super(RequestFactory, self).put(*args, **kwargs) + return Request(request, parsers) + + def delete(self, *args, **kwargs): + parsers = kwargs.pop('parsers', None) + request = super(RequestFactory, self).delete(*args, **kwargs) + return Request(request, parsers) + + def head(self, *args, **kwargs): + parsers = kwargs.pop('parsers', None) + request = super(RequestFactory, self).head(*args, **kwargs) + return Request(request, parsers) + + def options(self, *args, **kwargs): + parsers = kwargs.pop('parsers', None) + request = super(RequestFactory, self).options(*args, **kwargs) + return Request(request, parsers) diff --git a/djangorestframework/utils/staticviews.py b/djangorestframework/utils/staticviews.py deleted file mode 100644 index 7cbc0b9b..00000000 --- a/djangorestframework/utils/staticviews.py +++ /dev/null @@ -1,61 +0,0 @@ -from django.contrib.auth.views import * -from django.conf import settings -from django.http import HttpResponse -from django.shortcuts import render_to_response -from django.template import RequestContext -import base64 - - -# BLERGH -# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS -# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to -# be making settings changes in order to accomodate django-rest-framework -@csrf_protect -@never_cache -def api_login(request, template_name='djangorestframework/login.html', - redirect_field_name=REDIRECT_FIELD_NAME, - authentication_form=AuthenticationForm): - """Displays the login form and handles the login action.""" - - redirect_to = request.REQUEST.get(redirect_field_name, '') - - if request.method == "POST": - form = authentication_form(data=request.POST) - if form.is_valid(): - # Light security check -- make sure redirect_to isn't garbage. - if not redirect_to or ' ' in redirect_to: - redirect_to = settings.LOGIN_REDIRECT_URL - - # Heavier security check -- redirects to http://example.com should - # not be allowed, but things like /view/?param=http://example.com - # should be allowed. This regex checks if there is a '//' *before* a - # question mark. - elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to): - redirect_to = settings.LOGIN_REDIRECT_URL - - # Okay, security checks complete. Log the user in. - auth_login(request, form.get_user()) - - if request.session.test_cookie_worked(): - request.session.delete_test_cookie() - - return HttpResponseRedirect(redirect_to) - - else: - form = authentication_form(request) - - request.session.set_test_cookie() - - #current_site = get_current_site(request) - - return render_to_response(template_name, { - 'form': form, - redirect_field_name: redirect_to, - #'site': current_site, - #'site_name': current_site.name, - 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, - }, context_instance=RequestContext(request)) - - -def api_logout(request, next_page=None, template_name='djangorestframework/login.html', redirect_field_name=REDIRECT_FIELD_NAME): - return logout(request, next_page, template_name, redirect_field_name) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 6bfc4192..46223a3f 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -6,15 +6,13 @@ By setting or modifying class attributes on your view, you change it's predefine """ import re -from django.core.urlresolvers import set_script_prefix, get_script_prefix from django.utils.html import escape from django.utils.safestring import mark_safe from django.views.decorators.csrf import csrf_exempt from djangorestframework.compat import View as DjangoView, apply_markdown -from djangorestframework.response import ImmediateResponse +from djangorestframework.response import Response, ImmediateResponse from djangorestframework.mixins import * -from djangorestframework.utils import allowed_methods from djangorestframework import resources, renderers, parsers, authentication, permissions, status @@ -81,12 +79,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): or `None` to use default behaviour. """ - renderer_classes = renderers.DEFAULT_RENDERERS + renderers = renderers.DEFAULT_RENDERERS """ List of renderer classes the resource can serialize the response with, ordered by preference. """ - parser_classes = parsers.DEFAULT_PARSERS + parsers = parsers.DEFAULT_PARSERS """ List of parser classes the resource can parse the request with. """ @@ -118,7 +116,15 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ Return the list of allowed HTTP methods, uppercased. """ - return allowed_methods(self) + return [method.upper() for method in self.http_method_names + if hasattr(self, method)] + + @property + def default_response_headers(self): + return { + 'Allow': ', '.join(self.allowed_methods), + 'Vary': 'Authenticate, Accept' + } def get_name(self): """ @@ -183,32 +189,35 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): def initial(self, request, *args, **kargs): """ - Returns an `HttpRequest`. This method is a hook for any code that needs to run - prior to anything else. - Required if you want to do things like set `request.upload_handlers` before - the authentication and dispatch handling is run. + This method is a hook for any code that needs to run prior to + anything else. + Required if you want to do things like set `request.upload_handlers` + before the authentication and dispatch handling is run. """ pass def final(self, request, response, *args, **kargs): """ - Returns an `HttpResponse`. This method is a hook for any code that needs to run - after everything else in the view. + This method is a hook for any code that needs to run after everything + else in the view. + Returns the final response object. """ - # Always add these headers. - response['Allow'] = ', '.join(allowed_methods(self)) - # sample to allow caching using Vary http header - response['Vary'] = 'Authenticate, Accept' - + response.view = self + response.request = request + response.renderers = self.renderers + for key, value in self.headers.items(): + response[key] = value return response # Note: session based authentication is explicitly CSRF validated, # all other authentication is CSRF exempt. @csrf_exempt def dispatch(self, request, *args, **kwargs): - self.request = self.create_request(request) + request = self.create_request(request) + self.request = request self.args = args self.kwargs = kwargs + self.headers = self.default_response_headers try: self.initial(request, *args, **kwargs) @@ -222,26 +231,17 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): else: handler = self.http_method_not_allowed - # TODO: should we enforce HttpResponse, like Django does ? response = handler(request, *args, **kwargs) - # Prepare response for the response cycle. - self.response = response = self.prepare_response(response) - - # Pre-serialize filtering (eg filter complex objects into natively serializable types) - # TODO: ugly hack to handle both HttpResponse and Response. - if hasattr(response, 'raw_content'): + if isinstance(response, Response): + # Pre-serialize filtering (eg filter complex objects into natively serializable types) response.raw_content = self.filter_response(response.raw_content) - else: - response.content = self.filter_response(response.content) - except ImmediateResponse, response: - # Prepare response for the response cycle. - self.response = response = self.prepare_response(response) + except ImmediateResponse, exc: + response = exc.response - # `final` is the last opportunity to temper with the response, or even - # completely replace it. - return self.final(request, response, *args, **kwargs) + self.response = self.final(request, response, *args, **kwargs) + return self.response def options(self, request, *args, **kwargs): content = { @@ -266,7 +266,7 @@ class ModelView(View): resource = resources.ModelResource -class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): +class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): """ A view which provides default operations for read/update/delete against a model instance. """ |
