diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index be22103e6..d342e7a0f 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -1,15 +1,17 @@ """ -The :mod:`authentication` module provides a set of pluggable authentication classes. +The :mod:`authentication` module provides a set of pluggable authentication +classes. -Authentication behavior is provided by mixing the :class:`mixins.AuthMixin` class into a :class:`View` class. +Authentication behavior is provided by mixing the :class:`mixins.AuthMixin` +class into a :class:`View` class. -The set of authentication methods which are used is then specified by setting the -:attr:`authentication` attribute on the :class:`View` class, and listing a set of :class:`authentication` classes. +The set of authentication methods which are used is then specified by setting +the :attr:`authentication` attribute on the :class:`View` class, and listing a +set of :class:`authentication` classes. """ from django.contrib.auth import authenticate from django.middleware.csrf import CsrfViewMiddleware -from djangorestframework.utils import as_tuple import base64 __all__ = ( @@ -26,23 +28,25 @@ class BaseAuthentication(object): def __init__(self, view): """ - :class:`Authentication` classes are always passed the current view on creation. + :class:`Authentication` classes are always passed the current view on + creation. """ self.view = view def authenticate(self, request): """ - Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_ - + Authenticate the :obj:`request` and return a :obj:`User` or + :const:`None`. [*]_ + .. [*] The authentication context *will* typically be a :obj:`User`, but it need not be. It can be any user-like object so long as the - permissions classes (see the :mod:`permissions` module) on the view can - handle the object and use it to determine if the request has the required - permissions or not. - - This can be an important distinction if you're implementing some token - based authentication mechanism, where the authentication context - may be more involved than simply mapping to a :obj:`User`. + permissions classes (see the :mod:`permissions` module) on the view + can handle the object and use it to determine if the request has + the required permissions or not. + + This can be an important distinction if you're implementing some + token based authentication mechanism, where the authentication + context may be more involved than simply mapping to a :obj:`User`. """ return None @@ -51,14 +55,20 @@ class BasicAuthentication(BaseAuthentication): """ Use HTTP Basic authentication. """ + def _authenticate_user(self, username, password): + user = authenticate(username=username, password=password) + if user and user.is_active: + return user + return None def authenticate(self, request): """ - Returns a :obj:`User` if a correct username and password have been supplied - using HTTP Basic authentication. Otherwise returns :const:`None`. + Returns a :obj:`User` if a correct username and password have been + supplied using HTTP Basic authentication. + Otherwise returns :const:`None`. """ - from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError - + from django.utils import encoding + if 'HTTP_AUTHORIZATION' in request.META: auth = request.META['HTTP_AUTHORIZATION'].split() if len(auth) == 2 and auth[0].lower() == "basic": @@ -66,17 +76,19 @@ class BasicAuthentication(BaseAuthentication): auth_parts = base64.b64decode(auth[1]).partition(':') except TypeError: return None - + try: - uname, passwd = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2]) - except DjangoUnicodeDecodeError: + username = encoding.smart_unicode(auth_parts[0]) + password = encoding.smart_unicode(auth_parts[2]) + except encoding.DjangoUnicodeDecodeError: return None - - user = authenticate(username=uname, password=passwd) - if user is not None and user.is_active: + + user = self._authenticate_user(username, password) + if user: return user + return None - + class UserLoggedInAuthentication(BaseAuthentication): """ @@ -85,10 +97,11 @@ class UserLoggedInAuthentication(BaseAuthentication): def authenticate(self, request): """ - Returns a :obj:`User` if the request session currently has a logged in user. - Otherwise returns :const:`None`. + Returns a :obj:`User` if the request session currently has a logged in + user. Otherwise returns :const:`None`. """ - # TODO: Switch this back to request.POST, and let FormParser/MultiPartParser deal with the consequences. + # TODO: Switch this back to request.POST, and let + # FormParser/MultiPartParser deal with the consequences. if getattr(request, 'user', None) and request.user.is_active: # If this is a POST request we enforce CSRF validation. if request.method.upper() == 'POST': diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 350af5cb1..69e03f696 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -5,13 +5,12 @@ classes that can be added to a `View`. from django.contrib.auth.models import AnonymousUser from django.core.paginator import Paginator -from django.db.models.fields.related import ForeignKey from django.http import HttpResponse from djangorestframework import status +from djangorestframework.renderers import BaseRenderer from djangorestframework.resources import Resource, FormResource, ModelResource -from djangorestframework.response import ErrorResponse -from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX +from djangorestframework.response import Response, ErrorResponse from djangorestframework.utils import MSIE_USER_AGENT_REGEX from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence @@ -55,14 +54,14 @@ class RequestMixin(object): """ Returns the HTTP method. - This should be used instead of just reading :const:`request.method`, as it allows the `method` - to be overridden by using a hidden `form` field on a form POST request. + This should be used instead of just reading :const:`request.method`, as + it allows the `method` to be overridden by using a hidden `form` field + on a form POST request. """ if not hasattr(self, '_method'): self._load_method_and_content_type() return self._method - @property def content_type(self): """ @@ -76,7 +75,6 @@ class RequestMixin(object): self._load_method_and_content_type() return self._content_type - @property def DATA(self): """ @@ -89,7 +87,6 @@ class RequestMixin(object): self._load_data_and_files() return self._data - @property def FILES(self): """ @@ -101,7 +98,6 @@ class RequestMixin(object): self._load_data_and_files() return self._files - def _load_data_and_files(self): """ Parse the request content into self.DATA and self.FILES. @@ -110,18 +106,19 @@ class RequestMixin(object): self._load_method_and_content_type() if not hasattr(self, '_data'): - (self._data, self._files) = self._parse(self._get_stream(), self._content_type) - + (self._data, self._files) = self._parse(self._get_stream(), + self._content_type) def _load_method_and_content_type(self): """ - Set the method and content_type, and then check if they've been overridden. + Set the method and content_type, and then check if they've been + overridden. """ self._method = self.request.method - self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', '')) + self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', + self.request.META.get('CONTENT_TYPE', '')) self._perform_form_overloading() - def _get_stream(self): """ Returns an object that may be used to stream the request content. @@ -129,7 +126,8 @@ class RequestMixin(object): request = self.request try: - content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH'))) + content_length = int(request.META.get('CONTENT_LENGTH', + request.META.get('HTTP_CONTENT_LENGTH'))) except (ValueError, TypeError): content_length = 0 @@ -138,18 +136,20 @@ class RequestMixin(object): if content_length == 0: return None elif hasattr(request, 'read'): - return request + return request return StringIO(request.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._method != 'POST' or not is_form_media_type(self._content_type): + if (not self._USE_FORM_OVERLOADING + or self._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. @@ -167,26 +167,24 @@ class RequestMixin(object): stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0]) (self._data, self._files) = self._parse(stream, self._content_type) - def _parse(self, stream, content_type): """ Parse the request content. - May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request). + May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 + ErrorResponse (Bad Request). """ if stream is None or content_type is None: return (None, None) - parsers = as_tuple(self.parsers) for parser_cls in self.parsers: parser = parser_cls(self) if parser.can_handle_request(content_type): return parser.parse(stream) - raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - {'error': 'Unsupported media type in request \'%s\'.' % - content_type}) - + error = {'error': + "Unsupported media type in request '%s'." % content_type} + raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, error) @property def _parsed_media_types(self): @@ -195,7 +193,6 @@ class RequestMixin(object): """ return [parser.media_type for parser in self.parsers] - @property def _default_parser(self): """ @@ -204,29 +201,34 @@ class RequestMixin(object): return self.parsers[0] - ########## ResponseMixin ########## + class ResponseMixin(object): """ Adds behavior for pluggable `Renderers` to a :class:`views.View` class. Default behavior is to use standard HTTP Accept header content negotiation. - Also supports overriding the content type by specifying an ``_accept=`` parameter in the URL. - Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead. + + Also supports overriding the content type by specifying an ``_accept=`` + parameter in the URL. + + Ignores Accept headers from Internet Explorer user agents and uses a + sensible browser Accept header instead. """ - _ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params + # Allow override of Accept header in URL query params + _ACCEPT_QUERY_PARAM = '_accept' _IGNORE_IE_ACCEPT_HEADER = True """ The set of response renderers that the view can handle. - Should be a tuple/list of classes as described in the :mod:`renderers` module. + Should be a tuple/list of classes as described in the :mod:`renderers` + module. """ renderers = () - # TODO: wrap this behavior around dispatch(), ensuring it works # out of the box with existing Django classes that use render_to_response. def render(self, response): @@ -253,13 +255,13 @@ class ResponseMixin(object): content = renderer.render() # Build the HTTP Response - resp = HttpResponse(content, mimetype=response.media_type, status=response.status) + resp = HttpResponse(content, mimetype=response.media_type, + status=response.status) for (key, val) in response.headers.items(): resp[key] = val return resp - def _determine_renderer(self, request): """ Determines the appropriate renderer for the output, given the client's @@ -283,10 +285,10 @@ class ResponseMixin(object): # instead. accept_list = ['text/html', '*/*'] - elif 'HTTP_USER_AGENT' in request.META: + elif 'HTTP_ACCEPT' in request.META: # Use standard HTTP Accept negotiation accept_list = [token.strip() for token in - request.META["HTTP_ACCEPT"].split(',')] + request.META['HTTP_ACCEPT'].split(',')] else: # No accept header specified @@ -295,7 +297,7 @@ class ResponseMixin(object): # 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) + # Worst case is: len(accept_list) * len(self.renderers) renderers = [renderer_cls(self) for renderer_cls in self.renderers] for accepted_media_type_lst in order_by_precedence(accept_list): @@ -305,10 +307,9 @@ class ResponseMixin(object): return renderer, accepted_media_type # No acceptable renderers were found - raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE, - {'detail': 'Could not satisfy the client\'s Accept header', - 'available_types': self._rendered_media_types}) - + error = {'detail': "Could not satisfy the client's Accept header", + 'available_types': self._rendered_media_types} + raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE, error) @property def _rendered_media_types(self): @@ -336,39 +337,40 @@ class ResponseMixin(object): class AuthMixin(object): """ - Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class. + Simple :class:`mixin` class to add authentication and permission checking + to a :class:`View` class. """ """ The set of authentication types that this view can handle. - Should be a tuple/list of classes as described in the :mod:`authentication` module. + Should be a tuple/list of classes as described in the :mod:`authentication` + module. """ authentication = () """ The set of permissions that will be enforced on this view. - Should be a tuple/list of classes as described in the :mod:`permissions` module. + Should be a tuple/list of classes as described in the :mod:`permissions` + module. """ permissions = () - @property def user(self): """ - Returns the :obj:`user` for the current request, as determined by the set of - :class:`authentication` classes applied to the :class:`View`. + Returns the :obj:`user` for the current request, as determined by the + set of :class:`authentication` classes applied to the :class:`View`. """ if not hasattr(self, '_user'): self._user = self._authenticate() return self._user - def _authenticate(self): """ - Attempt to authenticate the request using each authentication class in turn. - Returns a ``User`` object, which may be ``AnonymousUser``. + Attempt to authenticate the request using each authentication class in + turn. Returns a ``User`` object, which may be ``AnonymousUser``. """ for authentication_cls in self.authentication: authentication = authentication_cls(self) @@ -377,7 +379,6 @@ class AuthMixin(object): return user return AnonymousUser() - # TODO: wrap this behavior around dispatch() def _check_permissions(self): """ @@ -397,10 +398,12 @@ class ResourceMixin(object): Should be a class as described in the :mod:`resources` module. - The :obj:`resource` is an object that maps a view onto it's representation on the server. + The :obj:`resource` is an object that maps a view onto it's representation + on the server. It provides validation on the content of incoming requests, - and filters the object representation into a serializable object for the response. + and filters the object representation into a serializable object for the + response. """ resource = None @@ -409,7 +412,8 @@ class ResourceMixin(object): """ Returns the cleaned, validated request content. - May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request). + May raise an :class:`response.ErrorResponse` with status code 400 + (Bad Request). """ if not hasattr(self, '_content'): self._content = self.validate_request(self.DATA, self.FILES) @@ -420,7 +424,8 @@ class ResourceMixin(object): """ Returns the cleaned, validated query parameters. - May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request). + May raise an :class:`response.ErrorResponse` with status code 400 + (Bad Request). """ return self.validate_request(self.request.GET) @@ -438,8 +443,10 @@ class ResourceMixin(object): def validate_request(self, data, files=None): """ - Given the request *data* and optional *files*, return the cleaned, validated content. - May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request) on failure. + Given the request *data* and optional *files*, return the cleaned, + validated content. + May raise an :class:`response.ErrorResponse` with status code 400 + (Bad Request) on failure. """ return self._resource.validate_request(data, files) @@ -456,26 +463,28 @@ class ResourceMixin(object): 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. + `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. + 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). + # 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 @@ -483,6 +492,7 @@ class InstanceMixin(object): ########## Model Mixins ########## + class ModelMixin(object): def get_model(self): """ @@ -497,7 +507,7 @@ class ModelMixin(object): """ return getattr(self, 'queryset', getattr(self.resource, 'queryset', - self._get_model().objects.all())) + self.get_model().objects.all())) def get_ordering(self): """ @@ -507,73 +517,32 @@ class ModelMixin(object): getattr(self.resource, 'ordering', None)) + # Underlying instance API... + def get_instance(self, *args, **kwargs): """ Return a model instance or None. """ model = self.get_model() queryset = self.get_queryset() - kwargs = self._filter_kwargs(kwargs) try: - # If we have any positional args then assume the last - # represents the primary key. Otherwise assume the named kwargs - # uniquely identify the instance. - if args: - return queryset.get(pk=args[-1], **kwargs) - else: - return queryset.get(**kwargs) + return queryset.get(**kwargs) except model.DoesNotExist: return None - def read(self, request, *args, **kwargs): - instance = self.get_instance(*args, **kwargs) - return instance + def create_instance(self, *args, **kwargs): + model = self.get_model() - def update(self, request, *args, **kwargs): - """ - Return a model instance. - """ - instance = self.get_instance(*args, **kwargs) - - if instance: - for (key, val) in self.CONTENT.items(): - setattr(instance, key, val) - else: - instance = self.get_model()(**self.CONTENT) - - instance.save() - return instance - - def create(self, request, *args, **kwargs): - """ - Return a model instance. - """ - model = self._get_model() - - # Copy the dict to keep self.CONTENT intact - content = dict(self.CONTENT) m2m_data = {} - - for field in model._meta.fields: - if isinstance(field, ForeignKey) and field.name in kwargs: - # translate 'related_field' kwargs into 'related_field_id' - kwargs[field.name + '_id'] = kwargs[field.name] + for field in model._meta.many_to_many: + if field.name in kwargs: + m2m_data[field.name] = ( + field.m2m_reverse_field_name(), kwargs[field.name] + ) del kwargs[field.name] - for field in model._meta.many_to_many: - if field.name in content: - m2m_data[field.name] = ( - field.m2m_reverse_field_name(), content[field.name] - ) - del content[field.name] - - all_kw_args = dict(content.items() + kwargs.items()) - - if args: - instance = model(pk=args[-1], **all_kw_args) - else: - instance = model(**all_kw_args) + instance = model(**kwargs) instance.save() for fieldname in m2m_data: @@ -591,31 +560,81 @@ class ModelMixin(object): return instance - - def destroy(self, request, *args, **kwargs): - """ - Return a model instance or None. - """ - instance = self.get_instance(*args, **kwargs) - - if instance: - instance.delete() - + def update_instance(self, instance, *args, **kwargs): + for (key, val) in kwargs.items(): + setattr(instance, key, val) + instance.save() return instance + def delete_instance(self, instance, *args, **kwargs): + instance.delete() + return instance - def list(self, request, *args, **kwargs): - """ - Return a list of instances. - """ + def list_instances(self, *args, **kwargs): queryset = self.get_queryset() ordering = self.get_ordering() if ordering: - assert(hasattr(ordering, '__iter__')) - queryset = queryset.order_by(*args) + queryset = queryset.order_by(ordering) return queryset.filter(**kwargs) + # Request/Response layer... + + def _get_url_kwargs(self, kwargs): + format_arg = BaseRenderer._FORMAT_QUERY_PARAM + if format_arg in kwargs: + kwargs = kwargs.copy() + del kwargs[format_arg] + return kwargs + + def _get_content_kwargs(self, kwargs): + return dict(self._get_url_kwargs(kwargs).items() + + self.CONTENT.items()) + + def read(self, request, *args, **kwargs): + kwargs = self._get_url_kwargs(kwargs) + instance = self.get_instance(**kwargs) + + if instance is None: + raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {}) + + return instance + + def update(self, request, *args, **kwargs): + kwargs = self._get_url_kwargs(kwargs) + instance = self.get_instance(**kwargs) + + kwargs = self._get_content_kwargs(kwargs) + if instance: + instance = self.update_instance(instance, **kwargs) + else: + instance = self.create_instance(**kwargs) + + return instance + + def create(self, request, *args, **kwargs): + kwargs = self._get_content_kwargs(kwargs) + instance = self.create_instance(**kwargs) + + headers = {} + try: + headers['Location'] = self.resource(self).url(instance) + except: # TODO: _SkipField should not really happen. + pass + + return Response(status.HTTP_201_CREATED, instance, headers) + + def destroy(self, request, *args, **kwargs): + kwargs = self._get_url_kwargs(kwargs) + instance = self.delete_instance(**kwargs) + if not instance: + raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {}) + + return instance + + def list(self, request, *args, **kwargs): + return self.list_instances(**kwargs) + ########## Pagination Mixins ########## @@ -638,7 +657,7 @@ class PaginatorMixin(object): return self.limit def url_with_page_number(self, page_number): - """ Constructs a url used for getting the next/previous urls """ + """Constructs a url used for getting the next/previous urls.""" url = "%s?page=%d" % (self.request.path, page_number) limit = self.get_limit() @@ -648,21 +667,21 @@ class PaginatorMixin(object): return url def next(self, page): - """ Returns a url to the next page of results (if any) """ + """Returns a url to the next page of results. (If any exists.)""" if not page.has_next(): return None return self.url_with_page_number(page.next_page_number()) def previous(self, page): - """ Returns a url to the previous page of results (if any) """ + """Returns a url to the previous page of results. (If any exists.)""" if not page.has_previous(): return None return self.url_with_page_number(page.previous_page_number()) def serialize_page_info(self, page): - """ This is some useful information that is added to the response """ + """This is some useful information that is added to the response.""" return { 'next': self.next(page), 'page': page.number, @@ -676,14 +695,15 @@ class PaginatorMixin(object): """ Given the response content, paginate and then serialize. - The response is modified to include to useful data relating to the number - of objects, number of pages, next/previous urls etc. etc. + The response is modified to include to useful data relating to the + number of objects, number of pages, next/previous urls etc. etc. The serialised objects are put into `results` on this new, modified response """ - # We don't want to paginate responses for anything other than GET requests + # We don't want to paginate responses for anything other than GET + # requests if self.method.upper() != 'GET': return self._resource.filter_response(obj) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index 997fd5103..023de54b9 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -1,15 +1,13 @@ from django.conf.urls.defaults import patterns, url -from django import http from django.test import TestCase from djangorestframework import status from djangorestframework.compat import View as DjangoView -from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer,\ - XMLRenderer +from djangorestframework.renderers import BaseRenderer, JSONRenderer, \ + YAMLRenderer, XMLRenderer from djangorestframework.parsers import JSONParser, YAMLParser from djangorestframework.mixins import ResponseMixin from djangorestframework.response import Response -from djangorestframework.utils.mediatypes import add_media_type_param from StringIO import StringIO import datetime @@ -21,27 +19,30 @@ 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" + format = "formata" def render(self, obj=None, media_type=None): return RENDERER_A_SERIALIZER(obj) + class RendererB(BaseRenderer): media_type = 'mock/rendererb' - format="formatb" + 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) - + urlpatterns = patterns('', url(r'^.*\.(?P.+)$', MockView.as_view(renderers=[RendererA, RendererB])), @@ -92,7 +93,7 @@ class RendererIntegrationTests(TestCase): 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) @@ -148,12 +149,7 @@ class RendererIntegrationTests(TestCase): _flat_repr = '{"foo": ["bar", "baz"]}' -_indented_repr = """{ - "foo": [ - "bar", - "baz" - ] -}""" +_indented_repr = '{\n "foo": [\n "bar", \n "baz"\n ]\n}' class JSONRendererTests(TestCase): @@ -165,45 +161,44 @@ class JSONRendererTests(TestCase): """ Test basic JSON rendering. """ - obj = {'foo':['bar','baz']} + obj = {'foo': ['bar', 'baz']} renderer = JSONRenderer(None) content = renderer.render(obj, 'application/json') self.assertEquals(content, _flat_repr) def test_with_content_type_args(self): """ - Test JSON rendering with additional content type arguments supplied. + Test JSON rendering with additional content type arguments supplied. """ - obj = {'foo':['bar','baz']} + obj = {'foo': ['bar', 'baz']} renderer = JSONRenderer(None) - content = renderer.render(obj, 'application/json; indent=2') + content = renderer.render(obj, 'application/json; indent=4') self.assertEquals(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']} + 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) - + self.assertEquals(obj, data) if YAMLRenderer: _yaml_repr = 'foo: [bar, baz]\n' - - + + class YAMLRendererTests(TestCase): """ Tests specific to the JSON Renderer """ - + def test_render(self): """ Test basic YAML rendering. @@ -212,24 +207,24 @@ if YAMLRenderer: renderer = YAMLRenderer(None) content = renderer.render(obj, 'application/yaml') self.assertEquals(content, _yaml_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 = YAMLRenderer(None) parser = YAMLParser(None) - + content = renderer.render(obj, 'application/yaml') (data, files) = parser.parse(StringIO(content)) - self.assertEquals(obj, data) + self.assertEquals(obj, data) + - class XMLRendererTestCase(TestCase): """ Tests specific to the XML Renderer @@ -289,4 +284,4 @@ class XMLRendererTestCase(TestCase): def assertXMLContains(self, xml, string): self.assertTrue(xml.startswith('\n')) self.assertTrue(xml.endswith('')) - self.assertTrue(string in xml, '%r not in %r' % (string, xml)) + self.assertTrue(string in xml, '%r not in %r' % (string, xml)) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index 7d693cc44..86fa4a295 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -32,7 +32,7 @@ def url_resolves(url): # From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml #class object_dict(dict): -# """object view of dict, you can +# """object view of dict, you can # >>> a = object_dict() # >>> a.fish = 'fish' # >>> a['fish'] @@ -85,8 +85,8 @@ class XML2Dict(object): 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 @@ -99,13 +99,13 @@ class XML2Dict(object): """ result = re.compile("\{(.*)\}(.*)").search(tag) if result: - value.namespace, tag = result.groups() + value.namespace, tag = result.groups() return (tag, value) def parse(self, file): """parse a xml file to a dict""" f = open(file, 'r') - return self.fromstring(f.read()) + return self.fromstring(f.read()) def fromstring(self, s): """parse a string""" @@ -132,16 +132,16 @@ class XMLRenderer(): xml.startElement(key, {}) self._to_xml(xml, value) xml.endElement(key) - + elif data is None: # Don't output any value - pass + pass else: xml.characters(smart_unicode(data)) def dict2xml(self, data): - stream = StringIO.StringIO() + stream = StringIO.StringIO() xml = SimplerXMLGenerator(stream, "utf-8") xml.startDocument() @@ -154,4 +154,4 @@ class XMLRenderer(): return stream.getvalue() def dict2xml(input): - return XMLRenderer().dict2xml(input) \ No newline at end of file + return XMLRenderer().dict2xml(input) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 4c472e2dc..d449636db 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -50,13 +50,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ List of all authenticating methods to attempt. """ - authentication = ( authentication.UserLoggedInAuthentication, - authentication.BasicAuthentication ) + authentication = (authentication.UserLoggedInAuthentication, + authentication.BasicAuthentication) """ List of all permissions that must be checked. """ - permissions = ( permissions.FullAnonAccess, ) + permissions = (permissions.FullAnonAccess,) @classmethod def as_view(cls, **initkwargs): @@ -86,8 +86,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): def initial(self, request, *args, **kargs): """ 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. + Required if you want to do things like set `request.upload_handlers` + before the authentication and dispatch handling is run. """ pass @@ -136,11 +136,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): response = Response(status.HTTP_204_NO_CONTENT) if request.method == 'OPTIONS': - # do not filter the response for HTTP OPTIONS, else the response fields are lost, + # do not filter the response for HTTP OPTIONS, + # else the response fields are lost, # as they do not correspond with model fields response.cleaned_content = response.raw_content else: - # Pre-serialize filtering (eg filter complex objects into natively serializable types) + # Pre-serialize filtering (eg filter complex objects into + # natively serializable types) response.cleaned_content = self.filter_response(response.raw_content) except ErrorResponse, exc: @@ -148,8 +150,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): # Always add these headers. # - # TODO - this isn't actually the correct way to set the vary header, - # also it's currently sub-optimal for HTTP caching - need to sort that out. + # TODO - this isn't really the correct way to set the Vary header, + # also it's currently sub-optimal for HTTP caching. response.headers['Allow'] = ', '.join(self.allowed_methods) response.headers['Vary'] = 'Authenticate, Accept' @@ -161,7 +163,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): return self.render(response) def options(self, request, *args, **kwargs): - response_obj = { + ret = { 'name': get_name(self), 'description': get_description(self), 'renders': self._rendered_media_types, @@ -172,8 +174,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): field_name_types = {} for name, field in form.fields.iteritems(): field_name_types[name] = field.__class__.__name__ - response_obj['fields'] = field_name_types - return response_obj + ret['fields'] = field_name_types + return ret class ModelView(ModelMixin, View): @@ -191,7 +193,9 @@ class ModelView(ModelMixin, View): class InstanceModelView(ModelView): """ - A view which provides default operations for read/update/delete against a model instance. + A view which provides default operations for read/update/delete against a + model instance. This view is also treated as the Canonical identifier + of the instances. """ _suffix = 'Instance'