Cleanups - line lengths, whitespace etc.

This commit is contained in:
Tom Christie 2011-12-14 14:48:22 +00:00
parent db6df5ce61
commit f84fd47825
5 changed files with 256 additions and 224 deletions

View File

@ -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':

View File

@ -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)

View File

@ -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<format>.+)$', 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('<?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))
self.assertTrue(string in xml, '%r not in %r' % (string, xml))

View File

@ -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)
return XMLRenderer().dict2xml(input)

View File

@ -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'