Commit d373b3a0 by Tom Christie

Decouple views and resources

parent 8756664e
"""The :mod:`authentication` modules provides for pluggable authentication behaviour. """The :mod:`authentication` modules provides for pluggable authentication behaviour.
Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.Resource` or Django :class:`View` class. Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.BaseView` or Django :class:`View` class.
The set of authentication which are use is then specified by setting the :attr:`authentication` attribute on the class, and listing a set of authentication classes. The set of authentication which are use is then specified by setting the :attr:`authentication` attribute on the class, and listing a set of authentication classes.
""" """
...@@ -25,10 +25,10 @@ class BaseAuthenticator(object): ...@@ -25,10 +25,10 @@ class BaseAuthenticator(object):
be some more complicated token, for example authentication tokens which are signed be some more complicated token, for example authentication tokens which are signed
against a particular set of permissions for a given user, over a given timeframe. against a particular set of permissions for a given user, over a given timeframe.
The default permission checking on Resource will use the allowed_methods attribute The default permission checking on View will use the allowed_methods attribute
for permissions if the authentication context is not None, and use anon_allowed_methods otherwise. for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
The authentication context is available to the method calls eg Resource.get(request) The authentication context is available to the method calls eg View.get(request)
by accessing self.auth in order to allow them to apply any more fine grained permission by accessing self.auth in order to allow them to apply any more fine grained permission
checking at the point the response is being generated. checking at the point the response is being generated.
......
""""""
from djangorestframework.utils.mediatypes import MediaType from djangorestframework.utils.mediatypes import MediaType
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
from djangorestframework.response import ErrorResponse from djangorestframework.response import ErrorResponse
...@@ -12,6 +13,14 @@ from decimal import Decimal ...@@ -12,6 +13,14 @@ from decimal import Decimal
import re import re
__all__ = ['RequestMixin',
'ResponseMixin',
'AuthMixin',
'ReadModelMixin',
'CreateModelMixin',
'UpdateModelMixin',
'DeleteModelMixin',
'ListModelMixin']
########## Request Mixin ########## ########## Request Mixin ##########
...@@ -250,7 +259,7 @@ class RequestMixin(object): ...@@ -250,7 +259,7 @@ class RequestMixin(object):
########## ResponseMixin ########## ########## ResponseMixin ##########
class ResponseMixin(object): class ResponseMixin(object):
"""Adds behaviour for pluggable Renderers to a :class:`.Resource` or Django :class:`View`. class. """Adds behaviour for pluggable Renderers to a :class:`.BaseView` or Django :class:`View`. class.
Default behaviour is to use standard HTTP Accept header content negotiation. Default behaviour is to use standard HTTP Accept header content negotiation.
Also supports overidding the content type by specifying an _accept= parameter in the URL. Also supports overidding the content type by specifying an _accept= parameter in the URL.
...@@ -259,32 +268,8 @@ class ResponseMixin(object): ...@@ -259,32 +268,8 @@ class ResponseMixin(object):
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
REWRITE_IE_ACCEPT_HEADER = True REWRITE_IE_ACCEPT_HEADER = True
#request = None
#response = None
renderers = () renderers = ()
#def render_to_response(self, obj):
# if isinstance(obj, Response):
# response = obj
# elif response_obj is not None:
# response = Response(status.HTTP_200_OK, obj)
# else:
# response = Response(status.HTTP_204_NO_CONTENT)
# response.cleaned_content = self._filter(response.raw_content)
# self._render(response)
#def filter(self, content):
# """
# Filter the response content.
# """
# for filterer_cls in self.filterers:
# filterer = filterer_cls(self)
# content = filterer.filter(content)
# return content
def render(self, response): def render(self, response):
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`.""" """Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
...@@ -318,7 +303,7 @@ class ResponseMixin(object): ...@@ -318,7 +303,7 @@ class ResponseMixin(object):
def _determine_renderer(self, request): def _determine_renderer(self, request):
"""Return the appropriate renderer for the output, given the client's 'Accept' header, """Return the appropriate renderer for the output, given the client's 'Accept' header,
and the content types that this Resource knows how to serve. and the content types that this mixin knows how to serve.
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"""
...@@ -415,17 +400,6 @@ class AuthMixin(object): ...@@ -415,17 +400,6 @@ class AuthMixin(object):
return auth return auth
return None return None
# TODO?
#@property
#def user(self):
# if not has_attr(self, '_user'):
# auth = self.auth
# if isinstance(auth, User...):
# self._user = auth
# else:
# self._user = getattr(auth, 'user', None)
# return self._user
def check_permissions(self): def check_permissions(self):
if not self.permissions: if not self.permissions:
return return
...@@ -443,14 +417,15 @@ class AuthMixin(object): ...@@ -443,14 +417,15 @@ class AuthMixin(object):
class ReadModelMixin(object): class ReadModelMixin(object):
"""Behaviour to read a model instance on GET requests""" """Behaviour to read a model instance on GET requests"""
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
model = self.resource.model
try: try:
if args: if args:
# If we have any none kwargs then assume the last represents the primrary key # If we have any none kwargs then assume the last represents the primrary key
instance = self.model.objects.get(pk=args[-1], **kwargs) instance = model.objects.get(pk=args[-1], **kwargs)
else: else:
# Otherwise assume the kwargs uniquely identify the model # Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs) instance = model.objects.get(**kwargs)
except self.model.DoesNotExist: except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND) raise ErrorResponse(status.HTTP_404_NOT_FOUND)
return instance return instance
...@@ -459,17 +434,18 @@ class ReadModelMixin(object): ...@@ -459,17 +434,18 @@ class ReadModelMixin(object):
class CreateModelMixin(object): class CreateModelMixin(object):
"""Behaviour to create a model instance on POST requests""" """Behaviour to create a model instance on POST requests"""
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
model = self.resource.model
# translated 'related_field' kwargs into 'related_field_id' # translated 'related_field' kwargs into 'related_field_id'
for related_name in [field.name for field in self.model._meta.fields if isinstance(field, RelatedField)]: for related_name in [field.name for field in model._meta.fields if isinstance(field, RelatedField)]:
if kwargs.has_key(related_name): if kwargs.has_key(related_name):
kwargs[related_name + '_id'] = kwargs[related_name] kwargs[related_name + '_id'] = kwargs[related_name]
del kwargs[related_name] del kwargs[related_name]
all_kw_args = dict(self.CONTENT.items() + kwargs.items()) all_kw_args = dict(self.CONTENT.items() + kwargs.items())
if args: if args:
instance = self.model(pk=args[-1], **all_kw_args) instance = model(pk=args[-1], **all_kw_args)
else: else:
instance = self.model(**all_kw_args) instance = model(**all_kw_args)
instance.save() instance.save()
headers = {} headers = {}
if hasattr(instance, 'get_absolute_url'): if hasattr(instance, 'get_absolute_url'):
...@@ -480,19 +456,20 @@ class CreateModelMixin(object): ...@@ -480,19 +456,20 @@ class CreateModelMixin(object):
class UpdateModelMixin(object): class UpdateModelMixin(object):
"""Behaviour to update a model instance on PUT requests""" """Behaviour to update a model instance on PUT requests"""
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
model = self.resource.model
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
try: try:
if args: if args:
# If we have any none kwargs then assume the last represents the primrary key # If we have any none kwargs then assume the last represents the primrary key
instance = self.model.objects.get(pk=args[-1], **kwargs) instance = model.objects.get(pk=args[-1], **kwargs)
else: else:
# Otherwise assume the kwargs uniquely identify the model # Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs) instance = model.objects.get(**kwargs)
for (key, val) in self.CONTENT.items(): for (key, val) in self.CONTENT.items():
setattr(instance, key, val) setattr(instance, key, val)
except self.model.DoesNotExist: except model.DoesNotExist:
instance = self.model(**self.CONTENT) instance = model(**self.CONTENT)
instance.save() instance.save()
instance.save() instance.save()
...@@ -502,14 +479,15 @@ class UpdateModelMixin(object): ...@@ -502,14 +479,15 @@ class UpdateModelMixin(object):
class DeleteModelMixin(object): class DeleteModelMixin(object):
"""Behaviour to delete a model instance on DELETE requests""" """Behaviour to delete a model instance on DELETE requests"""
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
model = self.resource.model
try: try:
if args: if args:
# If we have any none kwargs then assume the last represents the primrary key # If we have any none kwargs then assume the last represents the primrary key
instance = self.model.objects.get(pk=args[-1], **kwargs) instance = model.objects.get(pk=args[-1], **kwargs)
else: else:
# Otherwise assume the kwargs uniquely identify the model # Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs) instance = model.objects.get(**kwargs)
except self.model.DoesNotExist: except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {}) raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
instance.delete() instance.delete()
......
...@@ -11,6 +11,12 @@ class BasePermission(object): ...@@ -11,6 +11,12 @@ class BasePermission(object):
def has_permission(self, auth): def has_permission(self, auth):
return True return True
class FullAnonAccess(BasePermission):
""""""
def has_permission(self, auth):
return True
class IsAuthenticated(BasePermission): class IsAuthenticated(BasePermission):
"""""" """"""
def has_permission(self, auth): def has_permission(self, auth):
......
"""Renderers are used to serialize a Resource's output into specific media types. """Renderers are used to serialize a View's output into specific media types.
django-rest-framework also provides HTML and PlainText renderers that help self-document the API, django-rest-framework also provides HTML and PlainText renderers that help self-document the API,
by serializing the output along with documentation regarding the Resource, output status and headers, by serializing the output along with documentation regarding the Resource, output status and headers,
and providing forms and links depending on the allowed methods, renderers and parsers on the Resource. and providing forms and links depending on the allowed methods, renderers and parsers on the Resource.
......
...@@ -21,6 +21,6 @@ class Response(object): ...@@ -21,6 +21,6 @@ class Response(object):
class ErrorResponse(BaseException): class ErrorResponse(BaseException):
"""An exception representing an HttpResponse that should be returned immediatley.""" """An exception representing an HttpResponse that should be returned immediately."""
def __init__(self, status, content=None, headers={}): def __init__(self, status, content=None, headers={}):
self.response = Response(status, content=content, headers=headers) self.response = Response(status, content=content, headers=headers)
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<div id="content" class="colM"> <div id="content" class="colM">
<div id="content-main"> <div id="content-main">
<form method="post" action="{% url djangorestframework.views.api_login %}" id="login-form"> <form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form">
{% csrf_token %} {% csrf_token %}
<div class="form-row"> <div class="form-row">
<label for="id_username">Username:</label> {{ form.username }} <label for="id_username">Username:</label> {{ form.username }}
......
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
# See: http://www.useragentstring.com/ # See: http://www.useragentstring.com/
...@@ -19,15 +19,15 @@ class UserAgentMungingTest(TestCase): ...@@ -19,15 +19,15 @@ class UserAgentMungingTest(TestCase):
def setUp(self): def setUp(self):
class MockResource(Resource): class MockView(BaseView):
permissions = () permissions = ()
def get(self, request): def get(self, request):
return {'a':1, 'b':2, 'c':3} return {'a':1, 'b':2, 'c':3}
self.req = RequestFactory() self.req = RequestFactory()
self.MockResource = MockResource self.MockView = MockView
self.view = MockResource.as_view() self.view = MockView.as_view()
def test_munge_msie_accept_header(self): def test_munge_msie_accept_header(self):
"""Send MSIE user agent strings and ensure that we get an HTML response, """Send MSIE user agent strings and ensure that we get an HTML response,
...@@ -42,7 +42,7 @@ class UserAgentMungingTest(TestCase): ...@@ -42,7 +42,7 @@ class UserAgentMungingTest(TestCase):
def test_dont_rewrite_msie_accept_header(self): def test_dont_rewrite_msie_accept_header(self):
"""Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure """Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
that we get a JSON response if we set a */* accept header.""" that we get a JSON response if we set a */* accept header."""
view = self.MockResource.as_view(REWRITE_IE_ACCEPT_HEADER=False) view = self.MockView.as_view(REWRITE_IE_ACCEPT_HEADER=False)
for user_agent in (MSIE_9_USER_AGENT, for user_agent in (MSIE_9_USER_AGENT,
MSIE_8_USER_AGENT, MSIE_8_USER_AGENT,
......
...@@ -6,19 +6,19 @@ from django.test import Client, TestCase ...@@ -6,19 +6,19 @@ from django.test import Client, TestCase
from django.utils import simplejson as json from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
from djangorestframework import permissions from djangorestframework import permissions
import base64 import base64
class MockResource(Resource): class MockView(BaseView):
permissions = ( permissions.IsAuthenticated, ) permissions = ( permissions.IsAuthenticated, )
def post(self, request): def post(self, request):
return {'a':1, 'b':2, 'c':3} return {'a':1, 'b':2, 'c':3}
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^$', MockResource.as_view()), (r'^$', MockView.as_view()),
) )
......
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from django.test import TestCase from django.test import TestCase
from djangorestframework.utils.breadcrumbs import get_breadcrumbs from djangorestframework.utils.breadcrumbs import get_breadcrumbs
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
class Root(Resource): class Root(BaseView):
pass pass
class ResourceRoot(Resource): class ResourceRoot(BaseView):
pass pass
class ResourceInstance(Resource): class ResourceInstance(BaseView):
pass pass
class NestedResourceRoot(Resource): class NestedResourceRoot(BaseView):
pass pass
class NestedResourceInstance(Resource): class NestedResourceInstance(BaseView):
pass pass
urlpatterns = patterns('', urlpatterns = patterns('',
......
from django.test import TestCase from django.test import TestCase
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
from djangorestframework.compat import apply_markdown from djangorestframework.compat import apply_markdown
from djangorestframework.utils.description import get_name, get_description from djangorestframework.utils.description import get_name, get_description
...@@ -32,23 +32,23 @@ MARKED_DOWN = """<h2>an example docstring</h2> ...@@ -32,23 +32,23 @@ MARKED_DOWN = """<h2>an example docstring</h2>
<h2 id="hash_style_header">hash style header</h2>""" <h2 id="hash_style_header">hash style header</h2>"""
class TestResourceNamesAndDescriptions(TestCase): class TestViewNamesAndDescriptions(TestCase):
def test_resource_name_uses_classname_by_default(self): def test_resource_name_uses_classname_by_default(self):
"""Ensure Resource names are based on the classname by default.""" """Ensure Resource names are based on the classname by default."""
class MockResource(Resource): class MockView(BaseView):
pass pass
self.assertEquals(get_name(MockResource()), 'Mock Resource') self.assertEquals(get_name(MockView()), 'Mock View')
def test_resource_name_can_be_set_explicitly(self): def test_resource_name_can_be_set_explicitly(self):
"""Ensure Resource names can be set using the 'name' class attribute.""" """Ensure Resource names can be set using the 'name' class attribute."""
example = 'Some Other Name' example = 'Some Other Name'
class MockResource(Resource): class MockView(BaseView):
name = example name = example
self.assertEquals(get_name(MockResource()), example) self.assertEquals(get_name(MockView()), example)
def test_resource_description_uses_docstring_by_default(self): def test_resource_description_uses_docstring_by_default(self):
"""Ensure Resource names are based on the docstring by default.""" """Ensure Resource names are based on the docstring by default."""
class MockResource(Resource): class MockView(BaseView):
"""an example docstring """an example docstring
==================== ====================
...@@ -64,28 +64,28 @@ class TestResourceNamesAndDescriptions(TestCase): ...@@ -64,28 +64,28 @@ class TestResourceNamesAndDescriptions(TestCase):
# hash style header #""" # hash style header #"""
self.assertEquals(get_description(MockResource()), DESCRIPTION) self.assertEquals(get_description(MockView()), DESCRIPTION)
def test_resource_description_can_be_set_explicitly(self): def test_resource_description_can_be_set_explicitly(self):
"""Ensure Resource descriptions can be set using the 'description' class attribute.""" """Ensure Resource descriptions can be set using the 'description' class attribute."""
example = 'Some other description' example = 'Some other description'
class MockResource(Resource): class MockView(BaseView):
"""docstring""" """docstring"""
description = example description = example
self.assertEquals(get_description(MockResource()), example) self.assertEquals(get_description(MockView()), example)
def test_resource_description_does_not_require_docstring(self): def test_resource_description_does_not_require_docstring(self):
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute.""" """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
example = 'Some other description' example = 'Some other description'
class MockResource(Resource): class MockView(BaseView):
description = example description = example
self.assertEquals(get_description(MockResource()), example) self.assertEquals(get_description(MockView()), example)
def test_resource_description_can_be_empty(self): def test_resource_description_can_be_empty(self):
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string""" """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
class MockResource(Resource): class MockView(BaseView):
pass pass
self.assertEquals(get_description(MockResource()), '') self.assertEquals(get_description(MockView()), '')
def test_markdown(self): def test_markdown(self):
"""Ensure markdown to HTML works as expected""" """Ensure markdown to HTML works as expected"""
......
from django.test import TestCase from django.test import TestCase
from django import forms from django import forms
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
import StringIO import StringIO
class UploadFilesTests(TestCase): class UploadFilesTests(TestCase):
...@@ -15,7 +15,7 @@ class UploadFilesTests(TestCase): ...@@ -15,7 +15,7 @@ class UploadFilesTests(TestCase):
class FileForm(forms.Form): class FileForm(forms.Form):
file = forms.FileField file = forms.FileField
class MockResource(Resource): class MockView(BaseView):
permissions = () permissions = ()
form = FileForm form = FileForm
...@@ -26,7 +26,7 @@ class UploadFilesTests(TestCase): ...@@ -26,7 +26,7 @@ class UploadFilesTests(TestCase):
file = StringIO.StringIO('stuff') file = StringIO.StringIO('stuff')
file.name = 'stuff.txt' file.name = 'stuff.txt'
request = self.factory.post('/', {'file': file}) request = self.factory.post('/', {'file': file})
view = MockResource.as_view() view = MockView.as_view()
response = view(request) response = view(request)
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}') self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
.. ..
>>> from djangorestframework.parsers import FormParser >>> from djangorestframework.parsers import FormParser
>>> from djangorestframework.compat import RequestFactory >>> from djangorestframework.compat import RequestFactory
>>> from djangorestframework.resource import Resource >>> from djangorestframework.views import BaseView
>>> from StringIO import StringIO >>> from StringIO import StringIO
>>> from urllib import urlencode >>> from urllib import urlencode
>>> req = RequestFactory().get('/') >>> req = RequestFactory().get('/')
>>> some_resource = Resource() >>> some_view = BaseView()
>>> some_resource.request = req # Make as if this request had been dispatched >>> some_view.request = req # Make as if this request had been dispatched
FormParser FormParser
============ ============
...@@ -24,7 +24,7 @@ Here is some example data, which would eventually be sent along with a post requ ...@@ -24,7 +24,7 @@ Here is some example data, which would eventually be sent along with a post requ
Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter : Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
>>> FormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'} >>> FormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
True True
However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` : However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
...@@ -36,7 +36,7 @@ However, you can customize this behaviour by subclassing :class:`parsers.FormPar ...@@ -36,7 +36,7 @@ However, you can customize this behaviour by subclassing :class:`parsers.FormPar
This new parser only flattens the lists of parameters that contain a single value. This new parser only flattens the lists of parameters that contain a single value.
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
True True
.. note:: The same functionality is available for :class:`parsers.MultipartParser`. .. note:: The same functionality is available for :class:`parsers.MultipartParser`.
...@@ -61,7 +61,7 @@ The browsers usually strip the parameter completely. A hack to avoid this, and t ...@@ -61,7 +61,7 @@ The browsers usually strip the parameter completely. A hack to avoid this, and t
:class:`parsers.FormParser` strips the values ``_empty`` from all the lists. :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1'} >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1'}
True True
Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it. Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
...@@ -71,7 +71,7 @@ Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a lis ...@@ -71,7 +71,7 @@ Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a lis
... def is_a_list(self, key, val_list): ... def is_a_list(self, key, val_list):
... return key == 'key2' ... return key == 'key2'
... ...
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []} >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
True True
Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
...@@ -81,7 +81,7 @@ from tempfile import TemporaryFile ...@@ -81,7 +81,7 @@ from tempfile import TemporaryFile
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.parsers import MultipartParser from djangorestframework.parsers import MultipartParser
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
from djangorestframework.utils.mediatypes import MediaType from djangorestframework.utils.mediatypes import MediaType
from StringIO import StringIO from StringIO import StringIO
...@@ -122,9 +122,9 @@ class TestMultipartParser(TestCase): ...@@ -122,9 +122,9 @@ class TestMultipartParser(TestCase):
def test_multipartparser(self): def test_multipartparser(self):
"""Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters.""" """Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters."""
post_req = RequestFactory().post('/', self.body, content_type=self.content_type) post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
resource = Resource() view = BaseView()
resource.request = post_req view.request = post_req
parsed = MultipartParser(resource).parse(StringIO(self.body)) parsed = MultipartParser(view).parse(StringIO(self.body))
self.assertEqual(parsed['key1'], 'val1') self.assertEqual(parsed['key1'], 'val1')
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla') self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
...@@ -3,10 +3,10 @@ from django.core.urlresolvers import reverse ...@@ -3,10 +3,10 @@ from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.utils import simplejson as json from django.utils import simplejson as json
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
class MockResource(Resource): class MockView(BaseView):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified""" """Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
permissions = () permissions = ()
...@@ -14,8 +14,8 @@ class MockResource(Resource): ...@@ -14,8 +14,8 @@ class MockResource(Resource):
return reverse('another') return reverse('another')
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', MockResource.as_view()), url(r'^$', MockView.as_view()),
url(r'^another$', MockResource.as_view(), name='another'), url(r'^another$', MockView.as_view(), name='another'),
) )
......
...@@ -3,11 +3,11 @@ from django.test import TestCase ...@@ -3,11 +3,11 @@ from django.test import TestCase
from django.utils import simplejson as json from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.resource import Resource from djangorestframework.views import BaseView
from djangorestframework.permissions import Throttling from djangorestframework.permissions import Throttling
class MockResource(Resource): class MockView(BaseView):
permissions = ( Throttling, ) permissions = ( Throttling, )
throttle = (3, 1) # 3 requests per second throttle = (3, 1) # 3 requests per second
...@@ -15,7 +15,7 @@ class MockResource(Resource): ...@@ -15,7 +15,7 @@ class MockResource(Resource):
return 'foo' return 'foo'
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^$', MockResource.as_view()), (r'^$', MockView.as_view()),
) )
......
...@@ -4,6 +4,8 @@ from django.test import TestCase ...@@ -4,6 +4,8 @@ from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
from djangorestframework.response import ErrorResponse from djangorestframework.response import ErrorResponse
from djangorestframework.views import BaseView
from djangorestframework.resource import Resource
class TestValidatorMixinInterfaces(TestCase): class TestValidatorMixinInterfaces(TestCase):
...@@ -20,7 +22,7 @@ class TestDisabledValidations(TestCase): ...@@ -20,7 +22,7 @@ class TestDisabledValidations(TestCase):
def test_disabled_form_validator_returns_content_unchanged(self): def test_disabled_form_validator_returns_content_unchanged(self):
"""If the view's form attribute is None then FormValidator(view).validate(content) """If the view's form attribute is None then FormValidator(view).validate(content)
should just return the content unmodified.""" should just return the content unmodified."""
class DisabledFormView(object): class DisabledFormView(BaseView):
form = None form = None
view = DisabledFormView() view = DisabledFormView()
...@@ -30,7 +32,7 @@ class TestDisabledValidations(TestCase): ...@@ -30,7 +32,7 @@ class TestDisabledValidations(TestCase):
def test_disabled_form_validator_get_bound_form_returns_none(self): def test_disabled_form_validator_get_bound_form_returns_none(self):
"""If the view's form attribute is None on then """If the view's form attribute is None on then
FormValidator(view).get_bound_form(content) should just return None.""" FormValidator(view).get_bound_form(content) should just return None."""
class DisabledFormView(object): class DisabledFormView(BaseView):
form = None form = None
view = DisabledFormView() view = DisabledFormView()
...@@ -39,11 +41,10 @@ class TestDisabledValidations(TestCase): ...@@ -39,11 +41,10 @@ class TestDisabledValidations(TestCase):
def test_disabled_model_form_validator_returns_content_unchanged(self): def test_disabled_model_form_validator_returns_content_unchanged(self):
"""If the view's form and model attributes are None then """If the view's form is None and does not have a Resource with a model set then
ModelFormValidator(view).validate(content) should just return the content unmodified.""" ModelFormValidator(view).validate(content) should just return the content unmodified."""
class DisabledModelFormView(object): class DisabledModelFormView(BaseView):
form = None form = None
model = None
view = DisabledModelFormView() view = DisabledModelFormView()
content = {'qwerty':'uiop'} content = {'qwerty':'uiop'}
...@@ -51,13 +52,12 @@ class TestDisabledValidations(TestCase): ...@@ -51,13 +52,12 @@ class TestDisabledValidations(TestCase):
def test_disabled_model_form_validator_get_bound_form_returns_none(self): def test_disabled_model_form_validator_get_bound_form_returns_none(self):
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
class DisabledModelFormView(object): class DisabledModelFormView(BaseView):
form = None
model = None model = None
view = DisabledModelFormView() view = DisabledModelFormView()
content = {'qwerty':'uiop'} content = {'qwerty':'uiop'}
self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)# self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)
class TestNonFieldErrors(TestCase): class TestNonFieldErrors(TestCase):
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)""" """Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
...@@ -84,7 +84,7 @@ class TestNonFieldErrors(TestCase): ...@@ -84,7 +84,7 @@ class TestNonFieldErrors(TestCase):
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ErrorResponse was not raised') #pragma: no cover
class TestFormValidation(TestCase): class TestFormValidation(TestCase):
...@@ -95,11 +95,11 @@ class TestFormValidation(TestCase): ...@@ -95,11 +95,11 @@ class TestFormValidation(TestCase):
class MockForm(forms.Form): class MockForm(forms.Form):
qwerty = forms.CharField(required=True) qwerty = forms.CharField(required=True)
class MockFormView(object): class MockFormView(BaseView):
form = MockForm form = MockForm
validators = (FormValidator,) validators = (FormValidator,)
class MockModelFormView(object): class MockModelFormView(BaseView):
form = MockForm form = MockForm
validators = (ModelFormValidator,) validators = (ModelFormValidator,)
...@@ -264,9 +264,12 @@ class TestModelFormValidator(TestCase): ...@@ -264,9 +264,12 @@ class TestModelFormValidator(TestCase):
@property @property
def readonly(self): def readonly(self):
return 'read only' return 'read only'
class MockView(object): class MockResource(Resource):
model = MockModel model = MockModel
class MockView(BaseView):
resource = MockResource
self.validator = ModelFormValidator(MockView) self.validator = ModelFormValidator(MockView)
......
...@@ -3,7 +3,7 @@ from django.test import TestCase ...@@ -3,7 +3,7 @@ from django.test import TestCase
from django.test import Client from django.test import Client
urlpatterns = patterns('djangorestframework.views', urlpatterns = patterns('djangorestframework.utils.staticviews',
url(r'^robots.txt$', 'deny_robots'), url(r'^robots.txt$', 'deny_robots'),
url(r'^favicon.ico$', 'favicon'), url(r'^favicon.ico$', 'favicon'),
url(r'^accounts/login$', 'api_login'), url(r'^accounts/login$', 'api_login'),
......
from django.conf.urls.defaults import patterns
from django.conf import settings
urlpatterns = patterns('djangorestframework.utils.staticviews',
(r'robots.txt', 'deny_robots'),
(r'^accounts/login/$', 'api_login'),
(r'^accounts/logout/$', 'api_logout'),
)
# Only serve favicon in production because otherwise chrome users will pretty much
# permanantly have the django-rest-framework favicon whenever they navigate to
# 127.0.0.1:8000 or whatever, which gets annoying
if not settings.DEBUG:
urlpatterns += patterns('djangorestframework.utils.staticviews',
(r'favicon.ico', 'favicon'),
)
\ No newline at end of file
from django.contrib.auth.views import *
from django.conf import settings
from django.http import HttpResponse
import base64
def deny_robots(request):
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
def favicon(request):
data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
# 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='api_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='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
return logout(request, next_page, template_name, redirect_field_name)
...@@ -159,7 +159,7 @@ class ModelFormValidator(FormValidator): ...@@ -159,7 +159,7 @@ class ModelFormValidator(FormValidator):
otherwise if model is set use that class to create a ModelForm, otherwise return None.""" otherwise if model is set use that class to create a ModelForm, otherwise return None."""
form_cls = getattr(self.view, 'form', None) form_cls = getattr(self.view, 'form', None)
model_cls = getattr(self.view, 'model', None) model_cls = getattr(self.view.resource, 'model', None)
if form_cls: if form_cls:
# Use explict Form # Use explict Form
...@@ -189,9 +189,10 @@ class ModelFormValidator(FormValidator): ...@@ -189,9 +189,10 @@ class ModelFormValidator(FormValidator):
@property @property
def _model_fields_set(self): def _model_fields_set(self):
"""Return a set containing the names of validated fields on the model.""" """Return a set containing the names of validated fields on the model."""
model = getattr(self.view, 'model', None) resource = self.view.resource
fields = getattr(self.view, 'fields', self.fields) model = getattr(resource, 'model', None)
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields) fields = getattr(resource, 'fields', self.fields)
exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields)
model_fields = set(field.name for field in model._meta.fields) model_fields = set(field.name for field in model._meta.fields)
...@@ -203,9 +204,10 @@ class ModelFormValidator(FormValidator): ...@@ -203,9 +204,10 @@ class ModelFormValidator(FormValidator):
@property @property
def _property_fields_set(self): def _property_fields_set(self):
"""Returns a set containing the names of validated properties on the model.""" """Returns a set containing the names of validated properties on the model."""
model = getattr(self.view, 'model', None) resource = self.view.resource
fields = getattr(self.view, 'fields', self.fields) model = getattr(resource, 'model', None)
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields) fields = getattr(resource, 'fields', self.fields)
exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields)
property_fields = set(attr for attr in dir(model) if property_fields = set(attr for attr in dir(model) if
isinstance(getattr(model, attr, None), property) isinstance(getattr(model, attr, None), property)
......
from djangorestframework.modelresource import ModelResource, RootModelResource from djangorestframework.modelresource import InstanceModelResource, ListOrCreateModelResource
from modelresourceexample.models import MyModel from modelresourceexample.models import MyModel
FIELDS = ('foo', 'bar', 'baz', 'absolute_url') FIELDS = ('foo', 'bar', 'baz', 'absolute_url')
class MyModelRootResource(RootModelResource): class MyModelRootResource(ListOrCreateModelResource):
"""A create/list resource for MyModel. """A create/list resource for MyModel.
Available for both authenticated and anonymous access for the purposes of the sandbox.""" Available for both authenticated and anonymous access for the purposes of the sandbox."""
model = MyModel model = MyModel
fields = FIELDS fields = FIELDS
class MyModelResource(ModelResource): class MyModelResource(InstanceModelResource):
"""A read/update/delete resource for MyModel. """A read/update/delete resource for MyModel.
Available for both authenticated and anonymous access for the purposes of the sandbox.""" Available for both authenticated and anonymous access for the purposes of the sandbox."""
model = MyModel model = MyModel
......
...@@ -2,11 +2,8 @@ from django.conf.urls.defaults import patterns, include, url ...@@ -2,11 +2,8 @@ from django.conf.urls.defaults import patterns, include, url
from django.conf import settings from django.conf import settings
from sandbox.views import Sandbox from sandbox.views import Sandbox
urlpatterns = patterns('djangorestframework.views', urlpatterns = patterns('',
(r'robots.txt', 'deny_robots'),
(r'^$', Sandbox.as_view()), (r'^$', Sandbox.as_view()),
(r'^resource-example/', include('resourceexample.urls')), (r'^resource-example/', include('resourceexample.urls')),
(r'^model-resource-example/', include('modelresourceexample.urls')), (r'^model-resource-example/', include('modelresourceexample.urls')),
(r'^mixin/', include('mixin.urls')), (r'^mixin/', include('mixin.urls')),
...@@ -14,14 +11,6 @@ urlpatterns = patterns('djangorestframework.views', ...@@ -14,14 +11,6 @@ urlpatterns = patterns('djangorestframework.views',
(r'^pygments/', include('pygments_api.urls')), (r'^pygments/', include('pygments_api.urls')),
(r'^blog-post/', include('blogpost.urls')), (r'^blog-post/', include('blogpost.urls')),
(r'^accounts/login/$', 'api_login'), (r'^', include('djangorestframework.urls')),
(r'^accounts/logout/$', 'api_logout'),
) )
# Only serve favicon in production because otherwise chrome users will pretty much
# permanantly have the django-rest-framework favicon whenever they navigate to
# 127.0.0.1:8000 or whatever, which gets annoying
if not settings.DEBUG:
urlpatterns += patterns('djangorestframework.views',
(r'favicon.ico', 'favicon'),
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment