aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--djangorestframework/content.py55
-rw-r--r--djangorestframework/methods.py35
-rw-r--r--djangorestframework/response.py2
-rw-r--r--djangorestframework/tests/__init__.py9
-rw-r--r--djangorestframework/tests/accept.py64
-rw-r--r--djangorestframework/tests/content.py120
-rw-r--r--djangorestframework/tests/methods.py52
-rw-r--r--djangorestframework/tests/response.py25
-rw-r--r--djangorestframework/tests/utils.py40
-rw-r--r--djangorestframework/tests/validators.py151
-rw-r--r--djangorestframework/utils.py11
-rw-r--r--docs/templates/layout.html15
12 files changed, 578 insertions, 1 deletions
diff --git a/djangorestframework/content.py b/djangorestframework/content.py
new file mode 100644
index 00000000..94b908f5
--- /dev/null
+++ b/djangorestframework/content.py
@@ -0,0 +1,55 @@
+"""Mixin classes that provide a determine_content(request) method to return the content type and content of a request.
+We use this more generic behaviour to allow for overloaded content in POST forms.
+"""
+
+class ContentMixin(object):
+ """Base class for all ContentMixin classes, which simply defines the interface they provide."""
+
+ def determine_content(self, request):
+ """If the request contains content return a tuple of (content_type, content) otherwise return None.
+ Note that content_type may be None if it is unset.
+ Must be overridden to be implemented."""
+ raise NotImplementedError()
+
+
+class StandardContentMixin(ContentMixin):
+ """Standard HTTP request content behaviour.
+ See RFC 2616 sec 4.3 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3"""
+
+ def determine_content(self, request):
+ """If the request contains content return a tuple of (content_type, content) otherwise return None.
+ Note that content_type may be None if it is unset."""
+
+ if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
+ return None
+ return (request.META.get('CONTENT_TYPE', None), request.raw_post_data)
+
+
+class OverloadedContentMixin(ContentMixin):
+ """HTTP request content behaviour that also allows arbitrary content to be tunneled in form data."""
+
+ """The name to use for the content override field in the POST form."""
+ FORM_PARAM_CONTENT = '_content'
+
+ """The name to use for the content-type override field in the POST form."""
+ FORM_PARAM_CONTENTTYPE = '_contenttype'
+
+ def determine_content(self, request):
+ """If the request contains content return a tuple of (content_type, content) otherwise return None.
+ Note that content_type may be None if it is unset."""
+ if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
+ return None
+
+ content_type = request.META.get('CONTENT_TYPE', None)
+
+ if (request.method == 'POST' and self.FORM_PARAM_CONTENT and
+ request.POST.get(self.FORM_PARAM_CONTENT, None) is not None):
+
+ # Set content type if form contains a none empty FORM_PARAM_CONTENTTYPE field
+ content_type = None
+ if self.FORM_PARAM_CONTENTTYPE and request.POST.get(self.FORM_PARAM_CONTENTTYPE, None):
+ content_type = request.POST.get(self.FORM_PARAM_CONTENTTYPE, None)
+
+ return (content_type, request.POST[self.FORM_PARAM_CONTENT])
+
+ return (content_type, request.raw_post_data) \ No newline at end of file
diff --git a/djangorestframework/methods.py b/djangorestframework/methods.py
new file mode 100644
index 00000000..06a96643
--- /dev/null
+++ b/djangorestframework/methods.py
@@ -0,0 +1,35 @@
+"""Mixin classes that provide a determine_method(request) function to determine the HTTP
+method that a given request should be treated as. We use this more generic behaviour to
+allow for overloaded methods in POST forms.
+
+See Richardson & Ruby's RESTful Web Services for justification.
+"""
+
+class MethodMixin(object):
+ """Base class for all MethodMixin classes, which simply defines the interface they provide."""
+ def determine_method(self, request):
+ """Simply return GET, POST etc... as appropriate."""
+ raise NotImplementedError()
+
+
+class StandardMethodMixin(MethodMixin):
+ """Provide for standard HTTP behaviour, with no overloaded POST."""
+
+ def determine_method(self, request):
+ """Simply return GET, POST etc... as appropriate."""
+ return request.method.upper()
+
+
+class OverloadedPOSTMethodMixin(MethodMixin):
+ """Provide for overloaded POST behaviour."""
+
+ """The name to use for the method override field in the POST form."""
+ FORM_PARAM_METHOD = '_method'
+
+ def determine_method(self, request):
+ """Simply return GET, POST etc... as appropriate, allowing for POST overloading
+ by setting a form field with the requested method name."""
+ method = request.method.upper()
+ if method == 'POST' and self.FORM_PARAM_METHOD and request.POST.has_key(self.FORM_PARAM_METHOD):
+ method = request.POST[self.FORM_PARAM_METHOD].upper()
+ return method \ No newline at end of file
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index 4f23bb0a..e807eeb5 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -106,7 +106,7 @@ class NoContent(object):
class Response(object):
- def __init__(self, status, content=NoContent, headers={}):
+ def __init__(self, status=200, content=NoContent, headers={}):
self.status = status
self.has_content_body = not content is NoContent
self.raw_content = content # content prior to filtering
diff --git a/djangorestframework/tests/__init__.py b/djangorestframework/tests/__init__.py
new file mode 100644
index 00000000..7ac54419
--- /dev/null
+++ b/djangorestframework/tests/__init__.py
@@ -0,0 +1,9 @@
+"""Force import of all modules in this package in order to get the standard test runner to pick up the tests. Yowzers."""
+import os
+
+modules = [filename.rsplit('.', 1)[0]
+ for filename in os.listdir(os.path.dirname(__file__))
+ if filename.endswith('.py') and not filename.startswith('_')]
+
+for module in modules:
+ exec("from djangorestframework.tests.%s import *" % module) \ No newline at end of file
diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py
new file mode 100644
index 00000000..c4964e8a
--- /dev/null
+++ b/djangorestframework/tests/accept.py
@@ -0,0 +1,64 @@
+from django.test import TestCase
+from djangorestframework.tests.utils import RequestFactory
+from djangorestframework.resource import Resource
+
+
+# See: http://www.useragentstring.com/
+MSIE_9_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))'
+MSIE_8_USER_AGENT = 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)'
+MSIE_7_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)'
+FIREFOX_4_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729)'
+CHROME_11_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17'
+SAFARI_5_0_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/531.2+'
+OPERA_11_0_MSIE_USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00'
+OPERA_11_0_OPERA_USER_AGENT = 'Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00'
+
+class UserAgentMungingTest(TestCase):
+ """We need to fake up the accept headers when we deal with MSIE. Blergh.
+ http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
+
+ def setUp(self):
+ class MockResource(Resource):
+ anon_allowed_methods = allowed_methods = ('GET',)
+ def get(self, request, auth):
+ return {'a':1, 'b':2, 'c':3}
+ self.req = RequestFactory()
+ self.MockResource = MockResource
+
+ def test_munge_msie_accept_header(self):
+ """Send MSIE user agent strings and ensure that we get an HTML response,
+ even if we set a */* accept header."""
+ for user_agent in (MSIE_9_USER_AGENT,
+ MSIE_8_USER_AGENT,
+ MSIE_7_USER_AGENT):
+ req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
+ resp = self.MockResource(req)
+ self.assertEqual(resp['Content-Type'], 'text/html')
+
+ def test_dont_munge_msie_accept_header(self):
+ """Turn off _MUNGE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
+ that we get a JSON response if we set a */* accept header."""
+ self.MockResource._MUNGE_IE_ACCEPT_HEADER = False
+
+ for user_agent in (MSIE_9_USER_AGENT,
+ MSIE_8_USER_AGENT,
+ MSIE_7_USER_AGENT):
+ req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
+ resp = self.MockResource(req)
+ self.assertEqual(resp['Content-Type'], 'application/json')
+
+ def test_dont_munge_nice_browsers_accept_header(self):
+ """Send Non-MSIE user agent strings and ensure that we get a JSON response,
+ if we set a */* Accept header. (Other browsers will correctly set the Accept header)"""
+ for user_agent in (FIREFOX_4_0_USER_AGENT,
+ CHROME_11_0_USER_AGENT,
+ SAFARI_5_0_USER_AGENT,
+ OPERA_11_0_MSIE_USER_AGENT,
+ OPERA_11_0_OPERA_USER_AGENT):
+ req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
+ resp = self.MockResource(req)
+ self.assertEqual(resp['Content-Type'], 'application/json')
+
+
+
+
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
new file mode 100644
index 00000000..43769123
--- /dev/null
+++ b/djangorestframework/tests/content.py
@@ -0,0 +1,120 @@
+from django.test import TestCase
+from djangorestframework.tests.utils import RequestFactory
+from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
+
+
+class TestContentMixins(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
+
+ # Interface tests
+
+ def test_content_mixin_interface(self):
+ """Ensure the ContentMixin interface is as expected."""
+ self.assertRaises(NotImplementedError, ContentMixin().determine_content, None)
+
+ def test_standard_content_mixin_interface(self):
+ """Ensure the OverloadedContentMixin interface is as expected."""
+ self.assertTrue(issubclass(StandardContentMixin, ContentMixin))
+ getattr(StandardContentMixin, 'determine_content')
+
+ def test_overloaded_content_mixin_interface(self):
+ """Ensure the OverloadedContentMixin interface is as expected."""
+ self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
+ getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENT')
+ getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENTTYPE')
+ getattr(OverloadedContentMixin, 'determine_content')
+
+
+ # Common functionality to test with both StandardContentMixin and OverloadedContentMixin
+
+ def ensure_determines_no_content_GET(self, mixin):
+ """Ensure determine_content(request) returns None for GET request with no content."""
+ request = self.req.get('/')
+ self.assertEqual(mixin.determine_content(request), None)
+
+ def ensure_determines_form_content_POST(self, mixin):
+ """Ensure determine_content(request) returns content for POST request with content."""
+ form_data = {'qwerty': 'uiop'}
+ request = self.req.post('/', data=form_data)
+ self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
+
+ def ensure_determines_non_form_content_POST(self, mixin):
+ """Ensure determine_content(request) returns (content type, content) for POST request with content."""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ request = self.req.post('/', content, content_type=content_type)
+ self.assertEqual(mixin.determine_content(request), (content_type, content))
+
+ def ensure_determines_form_content_PUT(self, mixin):
+ """Ensure determine_content(request) returns content for PUT request with content."""
+ form_data = {'qwerty': 'uiop'}
+ request = self.req.put('/', data=form_data)
+ self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
+
+ def ensure_determines_non_form_content_PUT(self, mixin):
+ """Ensure determine_content(request) returns (content type, content) for PUT request with content."""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ request = self.req.put('/', content, content_type=content_type)
+ self.assertEqual(mixin.determine_content(request), (content_type, content))
+
+ # StandardContentMixin behavioural tests
+
+ def test_standard_behaviour_determines_no_content_GET(self):
+ """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
+ self.ensure_determines_no_content_GET(StandardContentMixin())
+
+ def test_standard_behaviour_determines_form_content_POST(self):
+ """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
+ self.ensure_determines_form_content_POST(StandardContentMixin())
+
+ def test_standard_behaviour_determines_non_form_content_POST(self):
+ """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
+ self.ensure_determines_non_form_content_POST(StandardContentMixin())
+
+ def test_standard_behaviour_determines_form_content_PUT(self):
+ """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
+ self.ensure_determines_form_content_PUT(StandardContentMixin())
+
+ def test_standard_behaviour_determines_non_form_content_PUT(self):
+ """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
+ self.ensure_determines_non_form_content_PUT(StandardContentMixin())
+
+ # OverloadedContentMixin behavioural tests
+
+ def test_overloaded_behaviour_determines_no_content_GET(self):
+ """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
+ self.ensure_determines_no_content_GET(OverloadedContentMixin())
+
+ def test_overloaded_behaviour_determines_form_content_POST(self):
+ """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
+ self.ensure_determines_form_content_POST(OverloadedContentMixin())
+
+ def test_overloaded_behaviour_determines_non_form_content_POST(self):
+ """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
+ self.ensure_determines_non_form_content_POST(OverloadedContentMixin())
+
+ def test_overloaded_behaviour_determines_form_content_PUT(self):
+ """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
+ self.ensure_determines_form_content_PUT(OverloadedContentMixin())
+
+ def test_overloaded_behaviour_determines_non_form_content_PUT(self):
+ """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
+ self.ensure_determines_non_form_content_PUT(OverloadedContentMixin())
+
+ def test_overloaded_behaviour_allows_content_tunnelling(self):
+ """Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ form_data = {OverloadedContentMixin.FORM_PARAM_CONTENT: content,
+ OverloadedContentMixin.FORM_PARAM_CONTENTTYPE: content_type}
+ request = self.req.post('/', form_data)
+ self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
+
+ def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
+ """Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
+ content = 'qwerty'
+ request = self.req.post('/', {OverloadedContentMixin.FORM_PARAM_CONTENT: content})
+ self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
+
diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py
new file mode 100644
index 00000000..e580ba02
--- /dev/null
+++ b/djangorestframework/tests/methods.py
@@ -0,0 +1,52 @@
+from django.test import TestCase
+from djangorestframework.tests.utils import RequestFactory
+from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
+
+
+class TestMethodMixins(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
+
+ # Interface tests
+
+ def test_method_mixin_interface(self):
+ """Ensure the base ContentMixin interface is as expected."""
+ self.assertRaises(NotImplementedError, MethodMixin().determine_method, None)
+
+ def test_standard_method_mixin_interface(self):
+ """Ensure the StandardMethodMixin interface is as expected."""
+ self.assertTrue(issubclass(StandardMethodMixin, MethodMixin))
+ getattr(StandardMethodMixin, 'determine_method')
+
+ def test_overloaded_method_mixin_interface(self):
+ """Ensure the OverloadedPOSTMethodMixin interface is as expected."""
+ self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
+ getattr(OverloadedPOSTMethodMixin, 'FORM_PARAM_METHOD')
+ getattr(OverloadedPOSTMethodMixin, 'determine_method')
+
+ # Behavioural tests
+
+ def test_standard_behaviour_determines_GET(self):
+ """GET requests identified as GET method with StandardMethodMixin"""
+ request = self.req.get('/')
+ self.assertEqual(StandardMethodMixin().determine_method(request), 'GET')
+
+ def test_standard_behaviour_determines_POST(self):
+ """POST requests identified as POST method with StandardMethodMixin"""
+ request = self.req.post('/')
+ self.assertEqual(StandardMethodMixin().determine_method(request), 'POST')
+
+ def test_overloaded_POST_behaviour_determines_GET(self):
+ """GET requests identified as GET method with OverloadedPOSTMethodMixin"""
+ request = self.req.get('/')
+ self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'GET')
+
+ def test_overloaded_POST_behaviour_determines_POST(self):
+ """POST requests identified as POST method with OverloadedPOSTMethodMixin"""
+ request = self.req.post('/')
+ self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'POST')
+
+ def test_overloaded_POST_behaviour_determines_overloaded_method(self):
+ """POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
+ request = self.req.post('/', {OverloadedPOSTMethodMixin.FORM_PARAM_METHOD: 'DELETE'})
+ self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py
new file mode 100644
index 00000000..c199f300
--- /dev/null
+++ b/djangorestframework/tests/response.py
@@ -0,0 +1,25 @@
+from django.test import TestCase
+from djangorestframework.response import Response
+
+try:
+ import unittest2
+except:
+ unittest2 = None
+else:
+ import warnings
+ warnings.filterwarnings("ignore")
+
+if unittest2:
+ class TestResponse(TestCase, unittest2.TestCase):
+
+ # Interface tests
+
+ # This is mainly to remind myself that the Response interface needs to change slightly
+ @unittest2.expectedFailure
+ def test_response_interface(self):
+ """Ensure the Response interface is as expected."""
+ response = Response()
+ getattr(response, 'status')
+ getattr(response, 'content')
+ getattr(response, 'headers')
+
diff --git a/djangorestframework/tests/utils.py b/djangorestframework/tests/utils.py
new file mode 100644
index 00000000..ef0cb59c
--- /dev/null
+++ b/djangorestframework/tests/utils.py
@@ -0,0 +1,40 @@
+from django.test import Client
+from django.core.handlers.wsgi import WSGIRequest
+
+# From: http://djangosnippets.org/snippets/963/
+# Lovely stuff
+class RequestFactory(Client):
+ """
+ Class that lets you create mock Request objects for use in testing.
+
+ Usage:
+
+ rf = RequestFactory()
+ get_request = rf.get('/hello/')
+ post_request = rf.post('/submit/', {'foo': 'bar'})
+
+ This class re-uses the django.test.client.Client interface, docs here:
+ http://www.djangoproject.com/documentation/testing/#the-test-client
+
+ Once you have a request object you can pass it to any view function,
+ just as if that view had been hooked up using a URLconf.
+
+ """
+ def request(self, **request):
+ """
+ Similar to parent class, but returns the request object as soon as it
+ has created it.
+ """
+ environ = {
+ 'HTTP_COOKIE': self.cookies,
+ 'PATH_INFO': '/',
+ 'QUERY_STRING': '',
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': '',
+ 'SERVER_NAME': 'testserver',
+ 'SERVER_PORT': 80,
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ }
+ environ.update(self.defaults)
+ environ.update(request)
+ return WSGIRequest(environ)
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
new file mode 100644
index 00000000..f72ea60d
--- /dev/null
+++ b/djangorestframework/tests/validators.py
@@ -0,0 +1,151 @@
+from django import forms
+from django.test import TestCase
+from djangorestframework.tests.utils import RequestFactory
+from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin
+from djangorestframework.response import ResponseException
+
+
+class TestValidatorMixins(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
+
+ class MockForm(forms.Form):
+ qwerty = forms.CharField(required=True)
+
+ class MockValidator(FormValidatorMixin):
+ form = MockForm
+
+ class DisabledValidator(FormValidatorMixin):
+ form = None
+
+ self.MockValidator = MockValidator
+ self.DisabledValidator = DisabledValidator
+
+
+ # Interface tests
+
+ def test_validator_mixin_interface(self):
+ """Ensure the ContentMixin interface is as expected."""
+ self.assertRaises(NotImplementedError, ValidatorMixin().validate, None)
+
+ def test_form_validator_mixin_interface(self):
+ """Ensure the OverloadedContentMixin interface is as expected."""
+ self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin))
+ getattr(FormValidatorMixin, 'form')
+ getattr(FormValidatorMixin, 'validate')
+
+ def test_model_form_validator_mixin_interface(self):
+ """Ensure the OverloadedContentMixin interface is as expected."""
+ self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin))
+ getattr(ModelFormValidatorMixin, 'model')
+ getattr(ModelFormValidatorMixin, 'form')
+ getattr(ModelFormValidatorMixin, 'validate')
+
+ # Behavioural tests - FormValidatorMixin
+
+ def test_validate_returns_content_unchanged_if_no_form_is_set(self):
+ """If the form attribute is None then validate(content) should just return the content unmodified."""
+ content = {'qwerty':'uiop'}
+ self.assertEqual(self.DisabledValidator().validate(content), content)
+
+ def test_get_bound_form_returns_none_if_no_form_is_set(self):
+ """If the form attribute is None then get_bound_form(content) should just return None."""
+ content = {'qwerty':'uiop'}
+ self.assertEqual(self.DisabledValidator().get_bound_form(content), None)
+
+ def test_validate_returns_content_unchanged_if_validates_and_does_not_need_cleanup(self):
+ """If the content is already valid and clean then validate(content) should just return the content unmodified."""
+ content = {'qwerty':'uiop'}
+
+ self.assertEqual(self.MockValidator().validate(content), content)
+
+ def test_form_validation_failure_raises_response_exception(self):
+ """If form validation fails a ResourceException 400 (Bad Request) should be raised."""
+ content = {}
+ self.assertRaises(ResponseException, self.MockValidator().validate, content)
+
+ def test_validate_does_not_allow_extra_fields(self):
+ """If some (otherwise valid) content includes fields that are not in the form then validation should fail.
+ It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
+ broken clients more easily (eg submitting content with a misnamed field)"""
+ content = {'qwerty': 'uiop', 'extra': 'extra'}
+ self.assertRaises(ResponseException, self.MockValidator().validate, content)
+
+ def test_validate_allows_extra_fields_if_explicitly_set(self):
+ """If we include an extra_fields paramater on _validate, then allow fields with those names."""
+ content = {'qwerty': 'uiop', 'extra': 'extra'}
+ self.MockValidator()._validate(content, extra_fields=('extra',))
+
+ def test_validate_checks_for_extra_fields_if_explicitly_set(self):
+ """If we include an extra_fields paramater on _validate, then fail unless we have fields with those names."""
+ content = {'qwerty': 'uiop'}
+ try:
+ self.MockValidator()._validate(content, extra_fields=('extra',))
+ except ResponseException, exc:
+ self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field is required.']}})
+ else:
+ self.fail('ResourceException was not raised') #pragma: no cover
+
+ def test_validate_failed_due_to_no_content_returns_appropriate_message(self):
+ """If validation fails due to no content, ensure the response contains a single non-field error"""
+ content = {}
+ try:
+ self.MockValidator().validate(content)
+ except ResponseException, exc:
+ self.assertEqual(exc.response.raw_content, {'errors': ['No content was supplied.']})
+ else:
+ self.fail('ResourceException was not raised') #pragma: no cover
+
+ def test_validate_failed_due_to_field_error_returns_appropriate_message(self):
+ """If validation fails due to a field error, ensure the response contains a single field error"""
+ content = {'qwerty': ''}
+ try:
+ self.MockValidator().validate(content)
+ except ResponseException, exc:
+ self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
+ else:
+ self.fail('ResourceException was not raised') #pragma: no cover
+
+ def test_validate_failed_due_to_invalid_field_returns_appropriate_message(self):
+ """If validation fails due to an invalid field, ensure the response contains a single field error"""
+ content = {'qwerty': 'uiop', 'extra': 'extra'}
+ try:
+ self.MockValidator().validate(content)
+ except ResponseException, exc:
+ self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
+ else:
+ self.fail('ResourceException was not raised') #pragma: no cover
+
+ def test_validate_failed_due_to_multiple_errors_returns_appropriate_message(self):
+ """If validation for multiple reasons, ensure the response contains each error"""
+ content = {'qwerty': '', 'extra': 'extra'}
+ try:
+ self.MockValidator().validate(content)
+ except ResponseException, exc:
+ self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
+ 'extra': ['This field does not exist.']}})
+ else:
+ self.fail('ResourceException was not raised') #pragma: no cover
+
+ def test_validate_failed_due_to_non_field_error_returns_appropriate_message(self):
+ """If validation for with a non-field error, ensure the response a non-field error"""
+ class MockForm(forms.Form):
+ field1 = forms.CharField(required=False)
+ field2 = forms.CharField(required=False)
+ ERROR_TEXT = 'You may not supply both field1 and field2'
+
+ def clean(self):
+ if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
+ raise forms.ValidationError(self.ERROR_TEXT)
+ return self.cleaned_data #pragma: no cover
+
+ class MockValidator(FormValidatorMixin):
+ form = MockForm
+
+ content = {'field1': 'example1', 'field2': 'example2'}
+ try:
+ MockValidator().validate(content)
+ except ResponseException, exc:
+ self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
+ else:
+ self.fail('ResourceException was not raised') #pragma: no cover \ No newline at end of file
diff --git a/djangorestframework/utils.py b/djangorestframework/utils.py
index f9bbc0fe..bc797d60 100644
--- a/djangorestframework/utils.py
+++ b/djangorestframework/utils.py
@@ -9,6 +9,17 @@ except ImportError:
import StringIO
+def as_tuple(obj):
+ """Given obj return a tuple"""
+ if obj is None:
+ return ()
+ elif isinstance(obj, list):
+ return tuple(obj)
+ elif isinstance(obj, tuple):
+ return obj
+ return (obj,)
+
+
def url_resolves(url):
"""Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
try:
diff --git a/docs/templates/layout.html b/docs/templates/layout.html
index 86ac3f33..6ba9e6ff 100644
--- a/docs/templates/layout.html
+++ b/docs/templates/layout.html
@@ -8,4 +8,19 @@
{% block htmltitle %}<title>{% if pagename == 'index' %}Django REST framework{% else %}{{ titleprefix }}{{ title|striptags|e }}{% endif %}</title>{% endblock %}
+{% block extrahead %}
+{{ super() }}
+<script type="text/javascript">
+ var _gaq = _gaq || [];
+ _gaq.push(['_setAccount', 'UA-18852272-2']);
+ _gaq.push(['_trackPageview']);
+
+ (function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ })();
+
+</script>
+{% endblock %}