From ec2a300a2bce53486d00003e7c8e28b6147eaa27 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 11:21:41 +0100
Subject: Start refactoring tests
---
djangorestframework/tests/content.py | 27 ++++++++++++++-------------
1 file changed, 14 insertions(+), 13 deletions(-)
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index c5eae2f9..31fcf892 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -1,13 +1,14 @@
# TODO: refactor these tests
-#from django.test import TestCase
-#from djangorestframework.compat import RequestFactory
+from django.test import TestCase
+from djangorestframework.compat import RequestFactory
+from djangorestframework.request import RequestMixin
#from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
#
#
-#class TestContentMixins(TestCase):
-# def setUp(self):
-# self.req = RequestFactory()
-#
+class TestContentMixins(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
+
# # Interface tests
#
# def test_content_mixin_interface(self):
@@ -29,10 +30,10 @@
#
# # 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_no_content_GET(self, view):
+ """Ensure determine_content(request) returns None for GET request with no content."""
+ view.request = self.req.get('/')
+ self.assertEqual(view.RAW_CONTENT, None)
#
# def ensure_determines_form_content_POST(self, mixin):
# """Ensure determine_content(request) returns content for POST request with content."""
@@ -62,9 +63,9 @@
#
# # 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_no_content_GET(self):
+ """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
+ self.ensure_determines_no_content_GET(RequestMixin())
#
# def test_standard_behaviour_determines_form_content_POST(self):
# """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
--
cgit v1.2.3
From 2fe0e584354a6625b0b293aebc7a7820ad50213e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 11:24:14 +0100
Subject: Start refactoring tests
---
djangorestframework/request.py | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index c4381bbf..c5d98286 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -145,6 +145,39 @@ class RequestMixin(object):
self._stream = StringIO(content[self.CONTENT_PARAM])
del(self._raw_content)
+ def parse(self, stream, content_type):
+ """
+ Parse the request content.
+
+ May raise a 415 ResponseException (Unsupported Media Type),
+ or a 400 ResponseException (Bad Request).
+ """
+ parsers = as_tuple(self.parsers)
+
+ parser = None
+ for parser_cls in parsers:
+ if parser_cls.handles(content_type):
+ parser = parser_cls(self)
+ break
+
+ if parser is None:
+ raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
+ {'error': 'Unsupported media type in request \'%s\'.' %
+ content_type.media_type})
+
+ return parser.parse(stream)
+
+ @property
+ def parsed_media_types(self):
+ """Return an list of all the media types that this view can parse."""
+ return [parser.media_type for parser in self.parsers]
+
+ @property
+ def default_parser(self):
+ """Return the view's most preffered emitter.
+ (This has no behavioural effect, but is may be used by documenting emitters)"""
+ return self.parsers[0]
+
method = property(_get_method, _set_method)
content_type = property(_get_content_type, _set_content_type)
accept = property(_get_accept, _set_accept)
--
cgit v1.2.3
From 338b5213fa6232dc6115b214495cf13ded6a3a17 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 11:47:22 +0100
Subject: More tests passing
---
djangorestframework/request.py | 23 +++++++++++++++++++++--
djangorestframework/tests/content.py | 28 +++++++++++++++-------------
2 files changed, 36 insertions(+), 15 deletions(-)
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index c5d98286..f79354a1 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -1,4 +1,8 @@
from djangorestframework.mediatypes import MediaType
+from djangorestframework.utils import as_tuple
+from djangorestframework.response import ResponseException
+from djangorestframework import status
+
#from djangorestframework.requestparsing import parse, load_parser
from django.http.multipartparser import LimitBytes
from StringIO import StringIO
@@ -11,6 +15,8 @@ class RequestMixin(object):
CONTENTTYPE_PARAM = "_content_type"
CONTENT_PARAM = "_content"
+ parsers = ()
+
def _get_method(self):
"""
Returns the HTTP method for the current view.
@@ -33,7 +39,10 @@ class RequestMixin(object):
"""
if not hasattr(self, '_content_type'):
content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
- self._content_type = MediaType(content_type)
+ if content_type:
+ self._content_type = MediaType(content_type)
+ else:
+ self._content_type = None
return self._content_type
@@ -68,8 +77,15 @@ class RequestMixin(object):
if not hasattr(self, '_stream'):
request = self.request
+ try:
+ content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH')))
+ except (ValueError, TypeError):
+ content_length = 0
+
# Currently only supports parsing request body as a stream with 1.3
- if hasattr(request, 'read'):
+ if content_length == 0:
+ return None
+ elif hasattr(request, 'read'):
# It's not at all clear if this needs to be byte limited or not.
# Maybe I'm just being dumb but it looks to me like there's some issues
# with that in Django.
@@ -152,6 +168,9 @@ class RequestMixin(object):
May raise a 415 ResponseException (Unsupported Media Type),
or a 400 ResponseException (Bad Request).
"""
+ if stream is None or content_type is None:
+ return None
+
parsers = as_tuple(self.parsers)
parser = None
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index 31fcf892..5e77472d 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -2,6 +2,7 @@
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.request import RequestMixin
+from djangorestframework.parsers import FormParser, MultipartParser
#from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
#
#
@@ -31,16 +32,17 @@ class TestContentMixins(TestCase):
# # Common functionality to test with both StandardContentMixin and OverloadedContentMixin
#
def ensure_determines_no_content_GET(self, view):
- """Ensure determine_content(request) returns None for GET request with no content."""
+ """Ensure view.RAW_CONTENT returns None for GET request with no content."""
view.request = self.req.get('/')
self.assertEqual(view.RAW_CONTENT, 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_form_content_POST(self, view):
+ """Ensure determine_content(request) returns content for POST request with content."""
+ form_data = {'qwerty': 'uiop'}
+ view.parsers = (FormParser, MultipartParser)
+ view.request = self.req.post('/', data=form_data)
+ self.assertEqual(view.RAW_CONTENT, form_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'
@@ -64,12 +66,12 @@ class TestContentMixins(TestCase):
# # 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."""
+ """Ensure request.RAW_CONTENT returns None for GET request with no content."""
self.ensure_determines_no_content_GET(RequestMixin())
-#
-# 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_form_content_POST(self):
+ """Ensure request.RAW_CONTENT returns content for POST request with content."""
+ self.ensure_determines_form_content_POST(RequestMixin())
#
# def test_standard_behaviour_determines_non_form_content_POST(self):
# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
--
cgit v1.2.3
From d4ed17845650d9a548ff0df362f3469878db2f91 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 11:54:26 +0100
Subject: More tests passing
---
djangorestframework/parsers.py | 43 +++++++----------------
djangorestframework/tests/content.py | 67 ++++++++++++++++++------------------
2 files changed, 46 insertions(+), 64 deletions(-)
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 1503342c..5b236647 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -27,38 +27,7 @@ except ImportError:
class ParserMixin(object):
parsers = ()
- def parse(self, stream, content_type):
- """
- Parse the request content.
-
- May raise a 415 ResponseException (Unsupported Media Type),
- or a 400 ResponseException (Bad Request).
- """
- parsers = as_tuple(self.parsers)
- parser = None
- for parser_cls in parsers:
- if parser_cls.handles(content_type):
- parser = parser_cls(self)
- break
-
- if parser is None:
- raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
- {'error': 'Unsupported media type in request \'%s\'.' %
- content_type.media_type})
-
- return parser.parse(stream)
-
- @property
- def parsed_media_types(self):
- """Return an list of all the media types that this ParserMixin can parse."""
- return [parser.media_type for parser in self.parsers]
-
- @property
- def default_parser(self):
- """Return the ParerMixin's most prefered emitter.
- (This has no behavioural effect, but is may be used by documenting emitters)"""
- return self.parsers[0]
class BaseParser(object):
@@ -122,6 +91,18 @@ class DataFlatener(object):
return False
+class PlainTextParser(BaseParser):
+ """
+ Plain text parser.
+
+ Simply returns the content of the stream
+ """
+ media_type = MediaType('text/plain')
+
+ def parse(self, stream):
+ return stream.read()
+
+
class FormParser(BaseParser, DataFlatener):
"""The default parser for form data.
Return a dict containing a single value for each non-reserved parameter.
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index 5e77472d..c4f93ef2 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -37,50 +37,51 @@ class TestContentMixins(TestCase):
self.assertEqual(view.RAW_CONTENT, None)
def ensure_determines_form_content_POST(self, view):
- """Ensure determine_content(request) returns content for POST request with content."""
+ """Ensure view.RAW_CONTENT returns content for POST request with form content."""
form_data = {'qwerty': 'uiop'}
view.parsers = (FormParser, MultipartParser)
view.request = self.req.post('/', data=form_data)
self.assertEqual(view.RAW_CONTENT, form_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 ensure_determines_non_form_content_POST(self, mixin):
+ """Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ view.parsers = (PlainTextParser,)
+ view.request = self.req.post('/', content, content_type=content_type)
+ self.assertEqual(view.RAW_CONTENT, form_data)
+
+ def ensure_determines_form_content_PUT(self, mixin):
+ """Ensure view.RAW_CONTENT returns content for PUT request with form content."""
+ form_data = {'qwerty': 'uiop'}
+ view.parsers = (FormParser, MultipartParser)
+ view.request = self.req.put('/', data=form_data)
+ self.assertEqual(view.RAW_CONTENT, form_data)
+
+ def ensure_determines_non_form_content_PUT(self, mixin):
+ """Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ view.parsers = (PlainTextParser,)
+ view.request = self.req.post('/', content, content_type=content_type)
+ self.assertEqual(view.RAW_CONTENT, form_data)#
+
def test_standard_behaviour_determines_no_content_GET(self):
"""Ensure request.RAW_CONTENT returns None for GET request with no content."""
self.ensure_determines_no_content_GET(RequestMixin())
def test_standard_behaviour_determines_form_content_POST(self):
- """Ensure request.RAW_CONTENT returns content for POST request with content."""
+ """Ensure request.RAW_CONTENT returns content for POST request with form content."""
self.ensure_determines_form_content_POST(RequestMixin())
-#
-# 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_POST(self):
+ """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
+ self.ensure_determines_non_form_content_POST(RequestMixin())
+
+ 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(RequestMixin())
+
# 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())
--
cgit v1.2.3
From e29a3f4cf12ea4c7cc616f27441b44639c736334 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 11:56:07 +0100
Subject: More tests passing
---
djangorestframework/tests/content.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index c4f93ef2..2e33fd87 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -2,7 +2,7 @@
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.request import RequestMixin
-from djangorestframework.parsers import FormParser, MultipartParser
+from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser
#from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
#
#
@@ -43,28 +43,28 @@ class TestContentMixins(TestCase):
view.request = self.req.post('/', data=form_data)
self.assertEqual(view.RAW_CONTENT, form_data)
- def ensure_determines_non_form_content_POST(self, mixin):
+ def ensure_determines_non_form_content_POST(self, view):
"""Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
content = 'qwerty'
content_type = 'text/plain'
view.parsers = (PlainTextParser,)
view.request = self.req.post('/', content, content_type=content_type)
- self.assertEqual(view.RAW_CONTENT, form_data)
+ self.assertEqual(view.RAW_CONTENT, content)
- def ensure_determines_form_content_PUT(self, mixin):
+ def ensure_determines_form_content_PUT(self, view):
"""Ensure view.RAW_CONTENT returns content for PUT request with form content."""
form_data = {'qwerty': 'uiop'}
view.parsers = (FormParser, MultipartParser)
view.request = self.req.put('/', data=form_data)
self.assertEqual(view.RAW_CONTENT, form_data)
- def ensure_determines_non_form_content_PUT(self, mixin):
+ def ensure_determines_non_form_content_PUT(self, view):
"""Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
content = 'qwerty'
content_type = 'text/plain'
view.parsers = (PlainTextParser,)
view.request = self.req.post('/', content, content_type=content_type)
- self.assertEqual(view.RAW_CONTENT, form_data)#
+ self.assertEqual(view.RAW_CONTENT, content)
def test_standard_behaviour_determines_no_content_GET(self):
"""Ensure request.RAW_CONTENT returns None for GET request with no content."""
--
cgit v1.2.3
From 0fe8d1a15dab8c1da37b4e966ccfe9095f24fa76 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 12:19:28 +0100
Subject: form overloading tests passing
---
djangorestframework/mediatypes.py | 2 +-
djangorestframework/request.py | 8 ++++++++
djangorestframework/tests/content.py | 38 +++++++++++++++++++-----------------
3 files changed, 29 insertions(+), 19 deletions(-)
diff --git a/djangorestframework/mediatypes.py b/djangorestframework/mediatypes.py
index d1641a8f..92d9264c 100644
--- a/djangorestframework/mediatypes.py
+++ b/djangorestframework/mediatypes.py
@@ -63,7 +63,7 @@ class MediaType(object):
"""
return self.media_type == 'application/x-www-form-urlencoded' or \
self.media_type == 'multipart/form-data'
-
+
def as_tuple(self):
return (self.main_type, self.sub_type, self.params)
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index f79354a1..988c0592 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -1,6 +1,7 @@
from djangorestframework.mediatypes import MediaType
from djangorestframework.utils import as_tuple
from djangorestframework.response import ResponseException
+from djangorestframework.parsers import FormParser, MultipartParser
from djangorestframework import status
#from djangorestframework.requestparsing import parse, load_parser
@@ -151,11 +152,18 @@ class RequestMixin(object):
if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not self.content_type.is_form():
return
+ # Temporarily switch to using the form parsers, then parse the content
+ parsers = self.parsers
+ self.parsers = (FormParser, MultipartParser)
content = self.RAW_CONTENT
+ self.parsers = parsers
+
+ # Method overloading - change the method and remove the param from the content
if self.METHOD_PARAM in content:
self.method = content[self.METHOD_PARAM].upper()
del self._raw_content[self.METHOD_PARAM]
+ # Content overloading - rewind the stream and modify the content type
if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content:
self._content_type = MediaType(content[self.CONTENTTYPE_PARAM])
self._stream = StringIO(content[self.CONTENT_PARAM])
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index 2e33fd87..05679b2f 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -67,25 +67,25 @@ class TestContentMixins(TestCase):
self.assertEqual(view.RAW_CONTENT, content)
def test_standard_behaviour_determines_no_content_GET(self):
- """Ensure request.RAW_CONTENT returns None for GET request with no content."""
+ """Ensure view.RAW_CONTENT returns None for GET request with no content."""
self.ensure_determines_no_content_GET(RequestMixin())
def test_standard_behaviour_determines_form_content_POST(self):
- """Ensure request.RAW_CONTENT returns content for POST request with form content."""
+ """Ensure view.RAW_CONTENT returns content for POST request with form content."""
self.ensure_determines_form_content_POST(RequestMixin())
def test_standard_behaviour_determines_non_form_content_POST(self):
- """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
+ """Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
self.ensure_determines_non_form_content_POST(RequestMixin())
def test_standard_behaviour_determines_form_content_PUT(self):
- """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
+ """Ensure view.RAW_CONTENT returns content for PUT request with form content."""
self.ensure_determines_form_content_PUT(RequestMixin())
-# 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())
-#
+ def test_standard_behaviour_determines_non_form_content_PUT(self):
+ """Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
+ self.ensure_determines_non_form_content_PUT(RequestMixin())
+
# # OverloadedContentMixin behavioural tests
#
# def test_overloaded_behaviour_determines_no_content_GET(self):
@@ -108,16 +108,18 @@ class TestContentMixins(TestCase):
# """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.CONTENT_PARAM: content,
-# OverloadedContentMixin.CONTENTTYPE_PARAM: content_type}
-# request = self.req.post('/', form_data)
-# self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
-# self.assertEqual(request.META['CONTENT_TYPE'], content_type)
-#
+ def test_overloaded_behaviour_allows_content_tunnelling(self):
+ """Ensure request.RAW_CONTENT returns content for overloaded POST request"""
+ content = 'qwerty'
+ content_type = 'text/plain'
+ view = RequestMixin()
+ form_data = {view.CONTENT_PARAM: content,
+ view.CONTENTTYPE_PARAM: content_type}
+ view.request = self.req.post('/', form_data)
+ view.parsers = (PlainTextParser,)
+ view.perform_form_overloading()
+ self.assertEqual(view.RAW_CONTENT, 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'
--
cgit v1.2.3
From dad1fa5798912e460ff05283c1c3b37d37b8f007 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 13:13:25 +0100
Subject: method overloading tests passing
---
djangorestframework/tests/content.py | 10 +++----
djangorestframework/tests/methods.py | 53 ++++++++++++++++--------------------
2 files changed, 28 insertions(+), 35 deletions(-)
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index 05679b2f..240601e0 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -1,12 +1,11 @@
-# TODO: refactor these tests
+# TODO: finish off the refactoring
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.request import RequestMixin
from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser
-#from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
-#
-#
-class TestContentMixins(TestCase):
+
+
+class TestContentParsing(TestCase):
def setUp(self):
self.req = RequestFactory()
@@ -125,4 +124,3 @@ class TestContentMixins(TestCase):
# content = 'qwerty'
# request = self.req.post('/', {OverloadedContentMixin.CONTENT_PARAM: content})
# self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
-
diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py
index f19bb3e5..8e694017 100644
--- a/djangorestframework/tests/methods.py
+++ b/djangorestframework/tests/methods.py
@@ -1,12 +1,13 @@
# TODO: Refactor these tests
-#from django.test import TestCase
-#from djangorestframework.compat import RequestFactory
+from django.test import TestCase
+from djangorestframework.compat import RequestFactory
+from djangorestframework.request import RequestMixin
#from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
#
#
-#class TestMethodMixins(TestCase):
-# def setUp(self):
-# self.req = RequestFactory()
+class TestMethodOverloading(TestCase):
+ def setUp(self):
+ self.req = RequestFactory()
#
# # Interface tests
#
@@ -27,27 +28,21 @@
#
# # 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.METHOD_PARAM: 'DELETE'})
-# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
+ def test_standard_behaviour_determines_GET(self):
+ """GET requests identified"""
+ view = RequestMixin()
+ view.request = self.req.get('/')
+ self.assertEqual(view.method, 'GET')
+
+ def test_standard_behaviour_determines_POST(self):
+ """POST requests identified"""
+ view = RequestMixin()
+ view.request = self.req.post('/')
+ self.assertEqual(view.method, 'POST')
+
+ def test_overloaded_POST_behaviour_determines_overloaded_method(self):
+ """POST requests can be overloaded to another method by setting a reserved form field"""
+ view = RequestMixin()
+ view.request = self.req.post('/', {view.METHOD_PARAM: 'DELETE'})
+ view.perform_form_overloading()
+ self.assertEqual(view.method, 'DELETE')
--
cgit v1.2.3
From 92b5a455da87c1d71d81903ea666daafb6b79725 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 13:14:51 +0100
Subject: Test cleanup
---
djangorestframework/tests/methods.py | 27 +++------------------------
1 file changed, 3 insertions(+), 24 deletions(-)
diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py
index 8e694017..7f6acf4f 100644
--- a/djangorestframework/tests/methods.py
+++ b/djangorestframework/tests/methods.py
@@ -1,33 +1,12 @@
-# TODO: Refactor these tests
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.request import RequestMixin
-#from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
-#
-#
+
+
class TestMethodOverloading(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, 'METHOD_PARAM')
-# getattr(OverloadedPOSTMethodMixin, 'determine_method')
-#
-# # Behavioural tests
-#
+
def test_standard_behaviour_determines_GET(self):
"""GET requests identified"""
view = RequestMixin()
--
cgit v1.2.3
From 941742593c50dcb0e1ca426621d107f12c9ee65c Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 13:22:54 +0100
Subject: Remove last bits of ParserMixin
---
djangorestframework/parsers.py | 4 ---
djangorestframework/request.py | 2 +-
djangorestframework/resource.py | 3 +-
djangorestframework/tests/content.py | 53 ++----------------------------------
4 files changed, 5 insertions(+), 57 deletions(-)
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 5b236647..11adeb78 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -24,10 +24,6 @@ try:
except ImportError:
from cgi import parse_qs
-class ParserMixin(object):
- parsers = ()
-
-
class BaseParser(object):
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index 988c0592..71ff8c0b 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -9,7 +9,7 @@ from django.http.multipartparser import LimitBytes
from StringIO import StringIO
class RequestMixin(object):
- """Delegate class that supplements an HttpRequest object with additional behaviour."""
+ """Mixin behaviour to deal with requests."""
USE_FORM_OVERLOADING = True
METHOD_PARAM = "_method"
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 80e5df2a..6ec22073 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -3,7 +3,6 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
from djangorestframework.emitters import EmitterMixin
-from djangorestframework.parsers import ParserMixin
from djangorestframework.authenticators import AuthenticatorMixin
from djangorestframework.validators import FormValidatorMixin
from djangorestframework.response import Response, ResponseException
@@ -19,7 +18,7 @@ from djangorestframework import emitters, parsers, authenticators, status
__all__ = ['Resource']
-class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixin, View):
+class Resource(EmitterMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixin, View):
"""Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization."""
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index 240601e0..b99f30f7 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -1,4 +1,6 @@
-# TODO: finish off the refactoring
+"""
+Tests for content parsing, and form-overloaded content parsing.
+"""
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.request import RequestMixin
@@ -9,27 +11,6 @@ class TestContentParsing(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, 'CONTENT_PARAM')
-# getattr(OverloadedContentMixin, 'CONTENTTYPE_PARAM')
-# getattr(OverloadedContentMixin, 'determine_content')
-#
-#
-# # Common functionality to test with both StandardContentMixin and OverloadedContentMixin
-#
def ensure_determines_no_content_GET(self, view):
"""Ensure view.RAW_CONTENT returns None for GET request with no content."""
view.request = self.req.get('/')
@@ -85,28 +66,6 @@ class TestContentParsing(TestCase):
"""Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
self.ensure_determines_non_form_content_PUT(RequestMixin())
-# # 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 request.RAW_CONTENT returns content for overloaded POST request"""
content = 'qwerty'
@@ -118,9 +77,3 @@ class TestContentParsing(TestCase):
view.parsers = (PlainTextParser,)
view.perform_form_overloading()
self.assertEqual(view.RAW_CONTENT, 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.CONTENT_PARAM: content})
-# self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
--
cgit v1.2.3
From cb9fb6ef2f9ac38c4f1c3946252a542b1f3f15d7 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 13:45:38 +0100
Subject: Refactoring of authentication/permissions
---
djangorestframework/authenticators.py | 20 -----------------
djangorestframework/request.py | 41 ++++++++++++++++++++++++++++++++++-
djangorestframework/resource.py | 10 ++++++---
djangorestframework/response.py | 6 ++---
4 files changed, 50 insertions(+), 27 deletions(-)
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
index 0d267b64..e382de10 100644
--- a/djangorestframework/authenticators.py
+++ b/djangorestframework/authenticators.py
@@ -10,26 +10,6 @@ from djangorestframework.utils import as_tuple
import base64
-class AuthenticatorMixin(object):
- """Adds pluggable authentication behaviour."""
-
- """The set of authenticators to use."""
- authenticators = None
-
- def authenticate(self, request):
- """Attempt to authenticate the request, returning an authentication context or None.
- An authentication context may be any object, although in many cases it will simply be a :class:`User` instance."""
-
- # Attempt authentication against each authenticator in turn,
- # and return None if no authenticators succeed in authenticating the request.
- for authenticator in as_tuple(self.authenticators):
- auth_context = authenticator(self).authenticate(request)
- if auth_context:
- return auth_context
-
- return None
-
-
class BaseAuthenticator(object):
"""All authenticators should extend BaseAuthenticator."""
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index 71ff8c0b..8a4330b4 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -9,7 +9,7 @@ from django.http.multipartparser import LimitBytes
from StringIO import StringIO
class RequestMixin(object):
- """Mixin behaviour to deal with requests."""
+ """Mixin class to provide request parsing behaviour."""
USE_FORM_OVERLOADING = True
METHOD_PARAM = "_method"
@@ -214,3 +214,42 @@ class RequestMixin(object):
+class AuthMixin(object):
+ """Mixin class to provide authentication and permissions."""
+ authenticators = ()
+ permitters = ()
+
+ @property
+ def auth(self):
+ if not hasattr(self, '_auth'):
+ self._auth = self._authenticate()
+ return self._auth
+
+ # 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):
+ if not self.permissions:
+ return
+
+ auth = self.auth
+ for permitter_cls in self.permitters:
+ permitter = permission_cls(self)
+ permitter.permit(auth)
+
+ def _authenticate(self):
+ for authenticator_cls in self.authenticators:
+ authenticator = authenticator_cls(self)
+ auth = authenticator.authenticate(self.request)
+ if auth:
+ return auth
+ return None
+
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 6ec22073..e6290363 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -6,7 +6,7 @@ from djangorestframework.emitters import EmitterMixin
from djangorestframework.authenticators import AuthenticatorMixin
from djangorestframework.validators import FormValidatorMixin
from djangorestframework.response import Response, ResponseException
-from djangorestframework.request import RequestMixin
+from djangorestframework.request import RequestMixin, AuthMixin
from djangorestframework import emitters, parsers, authenticators, status
@@ -18,7 +18,7 @@ from djangorestframework import emitters, parsers, authenticators, status
__all__ = ['Resource']
-class Resource(EmitterMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixin, View):
+class Resource(EmitterMixin, AuthMixin, FormValidatorMixin, RequestMixin, View):
"""Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization."""
@@ -139,7 +139,7 @@ class Resource(EmitterMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixi
# Typically the context will be a user, or None if this is an anonymous request,
# but it could potentially be more complex (eg the context of a request key which
# has been signed against a particular set of permissions)
- auth_context = self.authenticate(request)
+ auth_context = self.auth
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
# self.method, self.content_type, self.CONTENT appropriately.
@@ -173,6 +173,10 @@ class Resource(EmitterMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixi
except ResponseException, exc:
response = exc.response
+
+ except:
+ import traceback
+ traceback.print_exc()
# Always add these headers.
#
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index fb2e14a2..809e1754 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -8,7 +8,7 @@ class NoContent(object):
"""Used to indicate no body in http response.
(We cannot just use None, as that is a valid, serializable response object.)
- TODO: On relflection I'm going to get rid of this and just not support serailized 'None' responses.
+ TODO: On reflection I'm going to get rid of this and just not support serialized 'None' responses.
"""
pass
@@ -23,8 +23,8 @@ class Response(object):
@property
def status_text(self):
- """Return reason text corrosponding to our HTTP response status code.
- Provided for convienience."""
+ """Return reason text corresponding to our HTTP response status code.
+ Provided for convenience."""
return STATUS_CODE_TEXT.get(self.status, '')
--
cgit v1.2.3
From 136c9b5271fc205abffbbe5422b3c345858a533b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 13:52:16 +0100
Subject: Tweak
---
djangorestframework/request.py | 2 ++
djangorestframework/resource.py | 1 -
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index 8a4330b4..7f2cb0bc 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -17,6 +17,7 @@ class RequestMixin(object):
CONTENT_PARAM = "_content"
parsers = ()
+ validators = ()
def _get_method(self):
"""
@@ -169,6 +170,7 @@ class RequestMixin(object):
self._stream = StringIO(content[self.CONTENT_PARAM])
del(self._raw_content)
+
def parse(self, stream, content_type):
"""
Parse the request content.
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index e6290363..c5faf175 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -3,7 +3,6 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
from djangorestframework.emitters import EmitterMixin
-from djangorestframework.authenticators import AuthenticatorMixin
from djangorestframework.validators import FormValidatorMixin
from djangorestframework.response import Response, ResponseException
from djangorestframework.request import RequestMixin, AuthMixin
--
cgit v1.2.3
From a9df917d10e5f84090074e11213eb6d550c174cc Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 15:03:49 +0100
Subject: Lots of validator tests passing after refactor
---
djangorestframework/emitters.py | 23 ++-
djangorestframework/modelresource.py | 8 +-
djangorestframework/request.py | 23 +++
djangorestframework/resource.py | 10 +-
djangorestframework/tests/validators.py | 250 ++++++++++++++++++--------------
djangorestframework/validators.py | 91 +++++++-----
6 files changed, 239 insertions(+), 166 deletions(-)
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index 2769a4f5..36feea05 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -9,7 +9,6 @@ from django.template import RequestContext, loader
from django import forms
from djangorestframework.response import NoContent, ResponseException
-from djangorestframework.validators import FormValidatorMixin
from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.markdownwrapper import apply_markdown
from djangorestframework.breadcrumbs import get_breadcrumbs
@@ -217,15 +216,11 @@ class DocumentingTemplateEmitter(BaseEmitter):
#form_instance = resource.form_instance
# TODO! Reinstate this
- form_instance = None
+ form_instance = getattr(resource, 'bound_form_instance', None)
- if isinstance(resource, FormValidatorMixin):
- # If we already have a bound form instance (IE provided by the input parser, then use that)
- if resource.bound_form_instance is not None:
- form_instance = resource.bound_form_instance
-
+ if not form_instance and hasattr(resource, 'get_bound_form'):
# Otherwise if we have a response that is valid against the form then use that
- if not form_instance and resource.response.has_content_body:
+ if resource.response.has_content_body:
try:
form_instance = resource.get_bound_form(resource.response.cleaned_content)
if form_instance and not form_instance.is_valid():
@@ -233,12 +228,12 @@ class DocumentingTemplateEmitter(BaseEmitter):
except:
form_instance = None
- # If we still don't have a form instance then try to get an unbound form
- if not form_instance:
- try:
- form_instance = resource.get_bound_form()
- except:
- pass
+ # If we still don't have a form instance then try to get an unbound form
+ if not form_instance:
+ try:
+ form_instance = resource.get_bound_form()
+ except:
+ pass
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
if not form_instance:
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index 55a15d6a..7fa37053 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -5,15 +5,14 @@ from django.db.models.fields.related import RelatedField
from djangorestframework.response import Response, ResponseException
from djangorestframework.resource import Resource
-from djangorestframework.validators import ModelFormValidatorMixin
-from djangorestframework import status
+from djangorestframework import status, validators
import decimal
import inspect
import re
-class ModelResource(Resource, ModelFormValidatorMixin):
+class ModelResource(Resource):
"""A specialized type of Resource, for resources that map directly to a Django Model.
Useful things this provides:
@@ -21,6 +20,9 @@ class ModelResource(Resource, ModelFormValidatorMixin):
1. Nice serialization of returned Models and QuerySets.
2. A default set of create/read/update/delete operations."""
+ # List of validators to validate, cleanup and type-ify the request content
+ validators = (validators.ModelFormValidator,)
+
# The model attribute refers to the Django Model which this Resource maps to.
# (The Model's class, rather than an instance of the Model)
model = None
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index 7f2cb0bc..33d6bb2f 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -196,6 +196,29 @@ class RequestMixin(object):
return parser.parse(stream)
+
+ def validate(self, content):
+ """
+ Validate, cleanup, and type-ify the request content.
+ """
+ for validator_cls in self.validators:
+ validator = validator_cls(self)
+ content = validator.validate(content)
+ return content
+
+
+ def get_bound_form(self, content=None):
+ """
+ Return a bound form instance for the given content,
+ if there is an appropriate form validator attached to the view.
+ """
+ for validator_cls in self.validators:
+ if hasattr(validator_cls, 'get_bound_form'):
+ validator = validator_cls(self)
+ return validator.get_bound_form(content)
+ return None
+
+
@property
def parsed_media_types(self):
"""Return an list of all the media types that this view can parse."""
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index c5faf175..02c55663 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -3,10 +3,9 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
from djangorestframework.emitters import EmitterMixin
-from djangorestframework.validators import FormValidatorMixin
from djangorestframework.response import Response, ResponseException
from djangorestframework.request import RequestMixin, AuthMixin
-from djangorestframework import emitters, parsers, authenticators, status
+from djangorestframework import emitters, parsers, authenticators, validators, status
# TODO: Figure how out references and named urls need to work nicely
@@ -17,7 +16,7 @@ from djangorestframework import emitters, parsers, authenticators, status
__all__ = ['Resource']
-class Resource(EmitterMixin, AuthMixin, FormValidatorMixin, RequestMixin, View):
+class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
"""Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization."""
@@ -38,7 +37,10 @@ class Resource(EmitterMixin, AuthMixin, FormValidatorMixin, RequestMixin, View):
parsers = ( parsers.JSONParser,
parsers.FormParser,
parsers.MultipartParser )
-
+
+ # List of validators to validate, cleanup and type-ify the request content
+ validators = (validators.FormValidator,)
+
# List of all authenticating methods to attempt.
authenticators = ( authenticators.UserLoggedInAuthenticator,
authenticators.BasicAuthenticator )
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
index b5d2d566..f7d2d529 100644
--- a/djangorestframework/tests/validators.py
+++ b/djangorestframework/tests/validators.py
@@ -2,7 +2,7 @@ from django import forms
from django.db import models
from django.test import TestCase
from djangorestframework.compat import RequestFactory
-from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin
+from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
from djangorestframework.response import ResponseException
@@ -11,59 +11,68 @@ class TestValidatorMixinInterfaces(TestCase):
def test_validator_mixin_interface(self):
"""Ensure the ValidatorMixin base class interface is as expected."""
- self.assertRaises(NotImplementedError, ValidatorMixin().validate, None)
+ self.assertRaises(NotImplementedError, BaseValidator(None).validate, None)
- def test_form_validator_mixin_interface(self):
- """Ensure the FormValidatorMixin interface is as expected."""
- self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin))
- getattr(FormValidatorMixin, 'form')
- getattr(FormValidatorMixin, 'validate')
+ #def test_form_validator_mixin_interface(self):
+ # """Ensure the FormValidatorMixin interface is as expected."""
+ # self.assertTrue(issubclass(FormValidator, BaseValidator))
+ # getattr(FormValidator, 'form')
+ # getattr(FormValidator, 'validate')
- def test_model_form_validator_mixin_interface(self):
- """Ensure the ModelFormValidatorMixin interface is as expected."""
- self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin))
- getattr(ModelFormValidatorMixin, 'model')
- getattr(ModelFormValidatorMixin, 'form')
- getattr(ModelFormValidatorMixin, 'fields')
- getattr(ModelFormValidatorMixin, 'exclude_fields')
- getattr(ModelFormValidatorMixin, 'validate')
+ #def test_model_form_validator_mixin_interface(self):
+ # """Ensure the ModelFormValidatorMixin interface is as expected."""
+ # self.assertTrue(issubclass(ModelFormValidator, FormValidator))
+ # getattr(ModelFormValidator, 'model')
+ # getattr(ModelFormValidator, 'form')
+ # getattr(ModelFormValidator, 'fields')
+ # getattr(ModelFormValidator, 'exclude_fields')
+ # getattr(ModelFormValidator, 'validate')
class TestDisabledValidations(TestCase):
- """Tests on Validator Mixins with validation disabled by setting form to None"""
+ """Tests on FormValidator with validation disabled by setting form to None"""
def test_disabled_form_validator_returns_content_unchanged(self):
- """If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
- class DisabledFormValidator(FormValidatorMixin):
+ """If the view's form attribute is None then FormValidator(view).validate(content)
+ should just return the content unmodified."""
+ class DisabledFormView(object):
form = None
+ view = DisabledFormView()
content = {'qwerty':'uiop'}
- self.assertEqual(DisabledFormValidator().validate(content), content)
+ self.assertEqual(FormValidator(view).validate(content), content)
def test_disabled_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."""
- class DisabledFormValidator(FormValidatorMixin):
+ """If the view's form attribute is None on then
+ FormValidator(view).get_bound_form(content) should just return None."""
+ class DisabledFormView(object):
form = None
- content = {'qwerty':'uiop'}
- self.assertEqual(DisabledFormValidator().get_bound_form(content), None)
+ view = DisabledFormView()
+ content = {'qwerty':'uiop'}
+ self.assertEqual(FormValidator(view).get_bound_form(content), None)
+
def test_disabled_model_form_validator_returns_content_unchanged(self):
- """If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
- class DisabledModelFormValidator(ModelFormValidatorMixin):
+ """If the view's form and model attributes are None then
+ ModelFormValidator(view).validate(content) should just return the content unmodified."""
+ class DisabledModelFormView(object):
form = None
+ model = None
+ view = DisabledModelFormView()
content = {'qwerty':'uiop'}
- self.assertEqual(DisabledModelFormValidator().validate(content), content)
+ self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
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."""
- class DisabledModelFormValidator(ModelFormValidatorMixin):
+ class DisabledModelFormView(object):
form = None
-
- content = {'qwerty':'uiop'}
- self.assertEqual(DisabledModelFormValidator().get_bound_form(content), None)
-
+ model = None
+
+ view = DisabledModelFormView()
+ content = {'qwerty':'uiop'}
+ self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
class TestNonFieldErrors(TestCase):
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
@@ -80,12 +89,13 @@ class TestNonFieldErrors(TestCase):
raise forms.ValidationError(self.ERROR_TEXT)
return self.cleaned_data #pragma: no cover
- class MockValidator(FormValidatorMixin):
+ class MockView(object):
form = MockForm
+ view = MockView()
content = {'field1': 'example1', 'field2': 'example2'}
try:
- MockValidator().validate(content)
+ FormValidator(view).validate(content)
except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else:
@@ -95,19 +105,21 @@ class TestNonFieldErrors(TestCase):
class TestFormValidation(TestCase):
"""Tests which check basic form validation.
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
- (ModelFormValidatorMixin should behave as FormValidatorMixin if form is set rather than relying on the default ModelForm)"""
+ (ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
def setUp(self):
class MockForm(forms.Form):
qwerty = forms.CharField(required=True)
- class MockFormValidator(FormValidatorMixin):
+ class MockFormView(object):
form = MockForm
-
- class MockModelFormValidator(ModelFormValidatorMixin):
+ validators = (FormValidator,)
+
+ class MockModelFormView(object):
form = MockForm
-
- self.MockFormValidator = MockFormValidator
- self.MockModelFormValidator = MockModelFormValidator
+ validators = (ModelFormValidator,)
+
+ self.MockFormView = MockFormView
+ self.MockModelFormView = MockModelFormView
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
@@ -181,111 +193,129 @@ class TestFormValidation(TestCase):
# Tests on FormValidtionMixin
def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
- self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
def test_form_validation_failure_raises_response_exception(self):
- self.validation_failure_raises_response_exception(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failure_raises_response_exception(validator)
def test_validation_does_not_allow_extra_fields_by_default(self):
- self.validation_does_not_allow_extra_fields_by_default(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_does_not_allow_extra_fields_by_default(validator)
def test_validation_allows_extra_fields_if_explicitly_set(self):
- self.validation_allows_extra_fields_if_explicitly_set(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
- self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
- self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
- self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
- self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
- self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockFormValidator())
+ validator = FormValidator(self.MockFormView())
+ self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
# Same tests on ModelFormValidtionMixin
def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
- self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
def test_modelform_validation_failure_raises_response_exception(self):
- self.validation_failure_raises_response_exception(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failure_raises_response_exception(validator)
def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
- self.validation_does_not_allow_extra_fields_by_default(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_does_not_allow_extra_fields_by_default(validator)
def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
- self.validation_allows_extra_fields_if_explicitly_set(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
- self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
- self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
- self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
- self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockModelFormValidator())
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
- self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockModelFormValidator())
-
-
-class TestModelFormValidator(TestCase):
- """Tests specific to ModelFormValidatorMixin"""
-
- def setUp(self):
- """Create a validator for a model with two fields and a property."""
- class MockModel(models.Model):
- qwerty = models.CharField(max_length=256)
- uiop = models.CharField(max_length=256, blank=True)
-
- @property
- def readonly(self):
- return 'read only'
-
- class MockValidator(ModelFormValidatorMixin):
- model = MockModel
-
- self.MockValidator = MockValidator
-
-
- def test_property_fields_are_allowed_on_model_forms(self):
- """Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
- content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
- self.assertEqual(self.MockValidator().validate(content), content)
-
- def test_property_fields_are_not_required_on_model_forms(self):
- """Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
- content = {'qwerty':'example', 'uiop': 'example'}
- self.assertEqual(self.MockValidator().validate(content), content)
-
- def test_extra_fields_not_allowed_on_model_forms(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': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
- self.assertRaises(ResponseException, self.MockValidator().validate, content)
-
- def test_validate_requires_fields_on_model_forms(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 = {'readonly': 'read only'}
- self.assertRaises(ResponseException, self.MockValidator().validate, content)
-
- def test_validate_does_not_require_blankable_fields_on_model_forms(self):
- """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
- content = {'qwerty':'example', 'readonly': 'read only'}
- self.MockValidator().validate(content)
-
- def test_model_form_validator_uses_model_forms(self):
- self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm))
+ validator = ModelFormValidator(self.MockModelFormView())
+ self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
+
+
+# class TestModelFormValidator(TestCase):
+# """Tests specific to ModelFormValidatorMixin"""
+#
+# def setUp(self):
+# """Create a validator for a model with two fields and a property."""
+# class MockModel(models.Model):
+# qwerty = models.CharField(max_length=256)
+# uiop = models.CharField(max_length=256, blank=True)
+#
+# @property
+# def readonly(self):
+# return 'read only'
+#
+# class MockValidator(ModelFormValidatorMixin):
+# model = MockModel
+#
+# self.MockValidator = MockValidator
+#
+#
+# def test_property_fields_are_allowed_on_model_forms(self):
+# """Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
+# content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
+# self.assertEqual(self.MockValidator().validate(content), content)
+#
+# def test_property_fields_are_not_required_on_model_forms(self):
+# """Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
+# content = {'qwerty':'example', 'uiop': 'example'}
+# self.assertEqual(self.MockValidator().validate(content), content)
+#
+# def test_extra_fields_not_allowed_on_model_forms(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': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
+# self.assertRaises(ResponseException, self.MockValidator().validate, content)
+#
+# def test_validate_requires_fields_on_model_forms(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 = {'readonly': 'read only'}
+# self.assertRaises(ResponseException, self.MockValidator().validate, content)
+#
+# def test_validate_does_not_require_blankable_fields_on_model_forms(self):
+# """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
+# content = {'qwerty':'example', 'readonly': 'read only'}
+# self.MockValidator().validate(content)
+#
+# def test_model_form_validator_uses_model_forms(self):
+# self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm))
diff --git a/djangorestframework/validators.py b/djangorestframework/validators.py
index d96e8d9e..609e6d36 100644
--- a/djangorestframework/validators.py
+++ b/djangorestframework/validators.py
@@ -4,25 +4,31 @@ from django.db import models
from djangorestframework.response import ResponseException
from djangorestframework.utils import as_tuple
-class ValidatorMixin(object):
- """Base class for all ValidatorMixin classes, which simply defines the interface they provide."""
+
+class BaseValidator(object):
+ """Base class for all Validator classes, which simply defines the interface they provide."""
+
+ def __init__(self, view):
+ self.view = view
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
- Raises a ResponseException with status code 400 (Bad Request) on failure.
-
+ Typically raises a ResponseException with status code 400 (Bad Request) on failure.
+
Must be overridden to be implemented."""
raise NotImplementedError()
-class FormValidatorMixin(ValidatorMixin):
- """Validator Mixin that uses forms for validation.
- Extends the ValidatorMixin interface to also provide a get_bound_form() method.
- (Which may be used by some emitters.)"""
+class FormValidator(BaseValidator):
+ """Validator class that uses forms for validation.
+ Also provides a get_bound_form() method which may be used by some renderers.
- """The form class that should be used for validation, or None to turn off form validation."""
- form = None
- bound_form_instance = None
+ The view class should provide `.form` attribute which specifies the form classmethod
+ to be used for validation.
+
+ On calling validate() this validator may set a `.bound_form_instance` attribute on the
+ view, which may be used by some renderers."""
+
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
@@ -44,7 +50,7 @@ class FormValidatorMixin(ValidatorMixin):
if bound_form is None:
return content
- self.bound_form_instance = bound_form
+ self.view.bound_form_instance = bound_form
seen_fields_set = set(content.keys())
form_fields_set = set(bound_form.fields.keys())
@@ -78,7 +84,10 @@ class FormValidatorMixin(ValidatorMixin):
detail[u'errors'] = bound_form.non_field_errors()
# Add standard field errors
- field_errors = dict((key, map(unicode, val)) for (key, val) in bound_form.errors.iteritems() if not key.startswith('__'))
+ field_errors = dict((key, map(unicode, val))
+ for (key, val)
+ in bound_form.errors.iteritems()
+ if not key.startswith('__'))
# Add any unknown field errors
for key in unknown_fields:
@@ -94,20 +103,21 @@ class FormValidatorMixin(ValidatorMixin):
def get_bound_form(self, content=None):
"""Given some content return a Django form bound to that content.
If form validation is turned off (form class attribute is None) then returns None."""
- if not self.form:
+ form_cls = getattr(self.view, 'form', None)
+
+ if not form_cls:
return None
- if not content is None:
+ if content is not None:
if hasattr(content, 'FILES'):
- return self.form(content, content.FILES)
- return self.form(content)
- return self.form()
+ return form_cls(content, content.FILES)
+ return form_cls(content)
+ return form_cls()
-class ModelFormValidatorMixin(FormValidatorMixin):
- """Validator Mixin that uses forms for validation and falls back to a model form if no form is set.
- Extends the ValidatorMixin interface to also provide a get_bound_form() method.
- (Which may be used by some emitters.)"""
+class ModelFormValidator(FormValidator):
+ """Validator class that uses forms for validation and otherwise falls back to a model form if no form is set.
+ Also provides a get_bound_form() method which may be used by some renderers."""
"""The form class that should be used for validation, or None to use model form validation."""
form = None
@@ -148,15 +158,18 @@ class ModelFormValidatorMixin(FormValidatorMixin):
If the form class attribute has been explicitly set then use that class to create a Form,
otherwise if model is set use that class to create a ModelForm, otherwise return None."""
- if self.form:
+ form_cls = getattr(self.view, 'form', None)
+ model_cls = getattr(self.view, 'model', None)
+
+ if form_cls:
# Use explict Form
- return super(ModelFormValidatorMixin, self).get_bound_form(content)
+ return super(ModelFormValidator, self).get_bound_form(content)
- elif self.model:
+ elif model_cls:
# Fall back to ModelForm which we create on the fly
class OnTheFlyModelForm(forms.ModelForm):
class Meta:
- model = self.model
+ model = model_cls
#fields = tuple(self._model_fields_set)
# Instantiate the ModelForm as appropriate
@@ -176,24 +189,32 @@ class ModelFormValidatorMixin(FormValidatorMixin):
@property
def _model_fields_set(self):
"""Return a set containing the names of validated fields on the model."""
- model_fields = set(field.name for field in self.model._meta.fields)
+ model = getattr(self.view, 'model', None)
+ fields = getattr(self.view, 'fields', self.fields)
+ exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
+
+ model_fields = set(field.name for field in model._meta.fields)
- if self.fields:
- return model_fields & set(as_tuple(self.fields))
+ if fields:
+ return model_fields & set(as_tuple(fields))
- return model_fields - set(as_tuple(self.exclude_fields))
+ return model_fields - set(as_tuple(exclude_fields))
@property
def _property_fields_set(self):
"""Returns a set containing the names of validated properties on the model."""
- property_fields = set(attr for attr in dir(self.model) if
- isinstance(getattr(self.model, attr, None), property)
+ model = getattr(self.view, 'model', None)
+ fields = getattr(self.view, 'fields', self.fields)
+ exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
+
+ property_fields = set(attr for attr in dir(model) if
+ isinstance(getattr(model, attr, None), property)
and not attr.startswith('_'))
- if self.fields:
- return property_fields & set(as_tuple(self.fields))
+ if fields:
+ return property_fields & set(as_tuple(fields))
- return property_fields - set(as_tuple(self.exclude_fields))
+ return property_fields - set(as_tuple(exclude_fields))
--
cgit v1.2.3
From 29db0a60fb88686f721da75dc058222c922ffdeb Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 15:06:29 +0100
Subject: Even more validator tests passing after refactor
---
djangorestframework/tests/validators.py | 100 ++++++++++++++++----------------
1 file changed, 50 insertions(+), 50 deletions(-)
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
index f7d2d529..1f14f710 100644
--- a/djangorestframework/tests/validators.py
+++ b/djangorestframework/tests/validators.py
@@ -267,55 +267,55 @@ class TestFormValidation(TestCase):
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
-# class TestModelFormValidator(TestCase):
-# """Tests specific to ModelFormValidatorMixin"""
-#
-# def setUp(self):
-# """Create a validator for a model with two fields and a property."""
-# class MockModel(models.Model):
-# qwerty = models.CharField(max_length=256)
-# uiop = models.CharField(max_length=256, blank=True)
-#
-# @property
-# def readonly(self):
-# return 'read only'
-#
-# class MockValidator(ModelFormValidatorMixin):
-# model = MockModel
-#
-# self.MockValidator = MockValidator
-#
-#
-# def test_property_fields_are_allowed_on_model_forms(self):
-# """Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
-# content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
-# self.assertEqual(self.MockValidator().validate(content), content)
-#
-# def test_property_fields_are_not_required_on_model_forms(self):
-# """Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
-# content = {'qwerty':'example', 'uiop': 'example'}
-# self.assertEqual(self.MockValidator().validate(content), content)
-#
-# def test_extra_fields_not_allowed_on_model_forms(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': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
-# self.assertRaises(ResponseException, self.MockValidator().validate, content)
-#
-# def test_validate_requires_fields_on_model_forms(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 = {'readonly': 'read only'}
-# self.assertRaises(ResponseException, self.MockValidator().validate, content)
-#
-# def test_validate_does_not_require_blankable_fields_on_model_forms(self):
-# """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
-# content = {'qwerty':'example', 'readonly': 'read only'}
-# self.MockValidator().validate(content)
-#
-# def test_model_form_validator_uses_model_forms(self):
-# self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm))
+class TestModelFormValidator(TestCase):
+ """Tests specific to ModelFormValidatorMixin"""
+
+ def setUp(self):
+ """Create a validator for a model with two fields and a property."""
+ class MockModel(models.Model):
+ qwerty = models.CharField(max_length=256)
+ uiop = models.CharField(max_length=256, blank=True)
+
+ @property
+ def readonly(self):
+ return 'read only'
+
+ class MockView(object):
+ model = MockModel
+
+ self.validator = ModelFormValidator(MockView)
+
+
+ def test_property_fields_are_allowed_on_model_forms(self):
+ """Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
+ content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
+ self.assertEqual(self.validator.validate(content), content)
+
+ def test_property_fields_are_not_required_on_model_forms(self):
+ """Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
+ content = {'qwerty':'example', 'uiop': 'example'}
+ self.assertEqual(self.validator.validate(content), content)
+
+ def test_extra_fields_not_allowed_on_model_forms(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': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
+ self.assertRaises(ResponseException, self.validator.validate, content)
+
+ def test_validate_requires_fields_on_model_forms(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 = {'readonly': 'read only'}
+ self.assertRaises(ResponseException, self.validator.validate, content)
+
+ def test_validate_does_not_require_blankable_fields_on_model_forms(self):
+ """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
+ content = {'qwerty':'example', 'readonly': 'read only'}
+ self.validator.validate(content)
+
+ def test_model_form_validator_uses_model_forms(self):
+ self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
--
cgit v1.2.3
From a1ed565081779e3f50e9f0ff280a813a46f3613d Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 15:09:52 +0100
Subject: Cleanup
---
djangorestframework/resource.py | 2 +-
djangorestframework/tests/validators.py | 15 ---------------
2 files changed, 1 insertion(+), 16 deletions(-)
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 02c55663..1a02f8b5 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -39,7 +39,7 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
parsers.MultipartParser )
# List of validators to validate, cleanup and type-ify the request content
- validators = (validators.FormValidator,)
+ validators = ( validators.FormValidator, )
# List of all authenticating methods to attempt.
authenticators = ( authenticators.UserLoggedInAuthenticator,
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
index 1f14f710..a091cf29 100644
--- a/djangorestframework/tests/validators.py
+++ b/djangorestframework/tests/validators.py
@@ -13,21 +13,6 @@ class TestValidatorMixinInterfaces(TestCase):
"""Ensure the ValidatorMixin base class interface is as expected."""
self.assertRaises(NotImplementedError, BaseValidator(None).validate, None)
- #def test_form_validator_mixin_interface(self):
- # """Ensure the FormValidatorMixin interface is as expected."""
- # self.assertTrue(issubclass(FormValidator, BaseValidator))
- # getattr(FormValidator, 'form')
- # getattr(FormValidator, 'validate')
-
- #def test_model_form_validator_mixin_interface(self):
- # """Ensure the ModelFormValidatorMixin interface is as expected."""
- # self.assertTrue(issubclass(ModelFormValidator, FormValidator))
- # getattr(ModelFormValidator, 'model')
- # getattr(ModelFormValidator, 'form')
- # getattr(ModelFormValidator, 'fields')
- # getattr(ModelFormValidator, 'exclude_fields')
- # getattr(ModelFormValidator, 'validate')
-
class TestDisabledValidations(TestCase):
"""Tests on FormValidator with validation disabled by setting form to None"""
--
cgit v1.2.3
From 349ffcaf5f03b55d8ffe92999814ba97da5ca870 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 16:38:00 +0100
Subject: Rename mixins into Mixin class, rename ResponseException to
ErrorResponse, remove NoContent
---
djangorestframework/emitters.py | 147 +++-----------------------------
djangorestframework/modelresource.py | 6 +-
djangorestframework/parsers.py | 4 +-
djangorestframework/resource.py | 27 +++---
djangorestframework/response.py | 27 +++---
djangorestframework/tests/content.py | 2 +-
djangorestframework/tests/emitters.py | 5 +-
djangorestframework/tests/methods.py | 2 +-
djangorestframework/tests/validators.py | 20 ++---
djangorestframework/utils.py | 1 +
djangorestframework/validators.py | 14 +--
11 files changed, 64 insertions(+), 191 deletions(-)
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index 36feea05..2990d313 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -4,11 +4,10 @@ by serializing the output along with documentation regarding the Resource, outpu
and providing forms and links depending on the allowed methods, emitters and parsers on the Resource.
"""
from django.conf import settings
-from django.http import HttpResponse
from django.template import RequestContext, loader
from django import forms
-from djangorestframework.response import NoContent, ResponseException
+from djangorestframework.response import ErrorResponse
from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.markdownwrapper import apply_markdown
from djangorestframework.breadcrumbs import get_breadcrumbs
@@ -18,7 +17,6 @@ from djangorestframework import status
from urllib import quote_plus
import string
import re
-from decimal import Decimal
try:
import json
@@ -26,132 +24,9 @@ except ImportError:
import simplejson as json
-_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
-
-
-class EmitterMixin(object):
- """Adds behaviour for pluggable Emitters to a :class:`.Resource` or Django :class:`View`. class.
-
- 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.
- 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
- REWRITE_IE_ACCEPT_HEADER = True
-
- request = None
- response = None
- emitters = ()
-
- def emit(self, response):
- """Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
- self.response = response
-
- try:
- emitter = self._determine_emitter(self.request)
- except ResponseException, exc:
- emitter = self.default_emitter
- response = exc.response
-
- # Serialize the response content
- if response.has_content_body:
- content = emitter(self).emit(output=response.cleaned_content)
- else:
- content = emitter(self).emit()
-
- # Munge DELETE Response code to allow us to return content
- # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
- if response.status == 204:
- response.status = 200
-
- # Build the HTTP Response
- # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
- resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
- for (key, val) in response.headers.items():
- resp[key] = val
-
- return resp
-
-
- def _determine_emitter(self, request):
- """Return the appropriate emitter for the output, given the client's 'Accept' header,
- and the content types that this Resource knows how to serve.
-
- See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
-
- if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
- # Use _accept parameter override
- accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
- elif self.REWRITE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']):
- accept_list = ['text/html', '*/*']
- elif request.META.has_key('HTTP_ACCEPT'):
- # Use standard HTTP Accept negotiation
- accept_list = request.META["HTTP_ACCEPT"].split(',')
- else:
- # No accept header specified
- return self.default_emitter
-
- # Parse the accept header into a dict of {qvalue: set of media types}
- # We ignore mietype parameters
- accept_dict = {}
- for token in accept_list:
- components = token.split(';')
- mimetype = components[0].strip()
- qvalue = Decimal('1.0')
-
- if len(components) > 1:
- # Parse items that have a qvalue eg text/html;q=0.9
- try:
- (q, num) = components[-1].split('=')
- if q == 'q':
- qvalue = Decimal(num)
- except:
- # Skip malformed entries
- continue
-
- if accept_dict.has_key(qvalue):
- accept_dict[qvalue].add(mimetype)
- else:
- accept_dict[qvalue] = set((mimetype,))
-
- # Convert to a list of sets ordered by qvalue (highest first)
- accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
-
- for accept_set in accept_sets:
- # Return any exact match
- for emitter in self.emitters:
- if emitter.media_type in accept_set:
- return emitter
-
- # Return any subtype match
- for emitter in self.emitters:
- if emitter.media_type.split('/')[0] + '/*' in accept_set:
- return emitter
-
- # Return default
- if '*/*' in accept_set:
- return self.default_emitter
-
-
- raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
- {'detail': 'Could not statisfy the client\'s Accept header',
- 'available_types': self.emitted_media_types})
-
- @property
- def emitted_media_types(self):
- """Return an list of all the media types that this resource can emit."""
- return [emitter.media_type for emitter in self.emitters]
-
- @property
- def default_emitter(self):
- """Return the resource's most prefered emitter.
- (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
- return self.emitters[0]
-
-
# TODO: Rename verbose to something more appropriate
-# TODO: NoContent could be handled more cleanly. It'd be nice if it was handled by default,
+# TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
# and only have an emitter output anything if it explicitly provides support for that.
class BaseEmitter(object):
@@ -162,10 +37,10 @@ class BaseEmitter(object):
def __init__(self, resource):
self.resource = resource
- def emit(self, output=NoContent, verbose=False):
+ def emit(self, output=None, verbose=False):
"""By default emit simply returns the ouput as-is.
Override this method to provide for other behaviour."""
- if output is NoContent:
+ if output is None:
return ''
return output
@@ -177,8 +52,8 @@ class TemplateEmitter(BaseEmitter):
media_type = None
template = None
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
+ def emit(self, output=None, verbose=False):
+ if output is None:
return ''
context = RequestContext(self.request, output)
@@ -276,7 +151,7 @@ class DocumentingTemplateEmitter(BaseEmitter):
return GenericContentForm(resource)
- def emit(self, output=NoContent):
+ def emit(self, output=None):
content = self._get_content(self.resource, self.resource.request, output)
form_instance = self._get_form_instance(self.resource)
@@ -324,8 +199,8 @@ class JSONEmitter(BaseEmitter):
"""Emitter which serializes to JSON"""
media_type = 'application/json'
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
+ def emit(self, output=None, verbose=False):
+ if output is None:
return ''
if verbose:
return json.dumps(output, indent=4, sort_keys=True)
@@ -336,8 +211,8 @@ class XMLEmitter(BaseEmitter):
"""Emitter which serializes to XML."""
media_type = 'application/xml'
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
+ def emit(self, output=None, verbose=False):
+ if output is None:
return ''
return dict2xml(output)
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index 7fa37053..b0a4b1c1 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -3,7 +3,7 @@ from django.db.models import Model
from django.db.models.query import QuerySet
from django.db.models.fields.related import RelatedField
-from djangorestframework.response import Response, ResponseException
+from djangorestframework.response import Response, ErrorResponse
from djangorestframework.resource import Resource
from djangorestframework import status, validators
@@ -370,7 +370,7 @@ class ModelResource(Resource):
# Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist:
- raise ResponseException(status.HTTP_404_NOT_FOUND)
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND)
return instance
@@ -402,7 +402,7 @@ class ModelResource(Resource):
# Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist:
- raise ResponseException(status.HTTP_404_NOT_FOUND, None, {})
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
instance.delete()
return
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 11adeb78..707b61d5 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -9,7 +9,7 @@ We need a method to be able to:
and multipart/form-data. (eg also handle multipart/json)
"""
from django.http.multipartparser import MultiPartParser as DjangoMPParser
-from djangorestframework.response import ResponseException
+from djangorestframework.response import ErrorResponse
from djangorestframework import status
from djangorestframework.utils import as_tuple
from djangorestframework.mediatypes import MediaType
@@ -59,7 +59,7 @@ class JSONParser(BaseParser):
try:
return json.load(stream)
except ValueError, exc:
- raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
+ raise ErrorResponse(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
class DataFlatener(object):
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 1a02f8b5..f4460c1e 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -2,9 +2,8 @@ from django.core.urlresolvers import set_script_prefix
from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
-from djangorestframework.emitters import EmitterMixin
-from djangorestframework.response import Response, ResponseException
-from djangorestframework.request import RequestMixin, AuthMixin
+from djangorestframework.response import Response, ErrorResponse
+from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
from djangorestframework import emitters, parsers, authenticators, validators, status
@@ -16,7 +15,7 @@ from djangorestframework import emitters, parsers, authenticators, validators, s
__all__ = ['Resource']
-class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
+class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
"""Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization."""
@@ -81,7 +80,7 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
def not_implemented(self, operation):
"""Return an HTTP 500 server error if an operation is called which has been allowed by
allowed_methods, but which has not been implemented."""
- raise ResponseException(status.HTTP_500_INTERNAL_SERVER_ERROR,
+ raise ErrorResponse(status.HTTP_500_INTERNAL_SERVER_ERROR,
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
@@ -89,15 +88,15 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
"""Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
if not method in self.callmap.keys():
- raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED,
+ raise ErrorResponse(status.HTTP_501_NOT_IMPLEMENTED,
{'detail': 'Unknown or unsupported method \'%s\'' % method})
if not method in self.allowed_methods:
- raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED,
+ raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
if auth is None and not method in self.anon_allowed_methods:
- raise ResponseException(status.HTTP_403_FORBIDDEN,
+ raise ErrorResponse(status.HTTP_403_FORBIDDEN,
{'detail': 'You do not have permission to access this resource. ' +
'You may need to login or otherwise authenticate the request.'})
@@ -172,7 +171,7 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.cleanup_response(response.raw_content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
response = exc.response
except:
@@ -183,8 +182,12 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
#
# TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
- response.headers['Allow'] = ', '.join(self.allowed_methods)
- response.headers['Vary'] = 'Authenticate, Accept'
+ try:
+ response.headers['Allow'] = ', '.join(self.allowed_methods)
+ response.headers['Vary'] = 'Authenticate, Accept'
- return self.emit(response)
+ return self.emit(response)
+ except:
+ import traceback
+ traceback.print_exc()
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index 809e1754..545a5834 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -1,24 +1,16 @@
from django.core.handlers.wsgi import STATUS_CODE_TEXT
-__all__ =['NoContent', 'Response', ]
-
-
-
-class NoContent(object):
- """Used to indicate no body in http response.
- (We cannot just use None, as that is a valid, serializable response object.)
-
- TODO: On reflection I'm going to get rid of this and just not support serialized 'None' responses.
- """
- pass
+__all__ =['Response', 'ErrorResponse']
+# TODO: remove raw_content/cleaned_content and just use content?
class Response(object):
- def __init__(self, status=200, content=NoContent, headers={}):
+ """An HttpResponse that may include content that hasn't yet been serialized."""
+ def __init__(self, status=200, content=None, headers={}):
self.status = status
- self.has_content_body = not content is NoContent # TODO: remove and just use content
- self.raw_content = content # content prior to filtering - TODO: remove and just use content
- self.cleaned_content = content # content after filtering TODO: remove and just use content
+ self.has_content_body = content is not None
+ self.raw_content = content # content prior to filtering
+ self.cleaned_content = content # content after filtering
self.headers = headers
@property
@@ -28,6 +20,7 @@ class Response(object):
return STATUS_CODE_TEXT.get(self.status, '')
-class ResponseException(BaseException):
- def __init__(self, status, content=NoContent, headers={}):
+class ErrorResponse(BaseException):
+ """An exception representing an HttpResponse that should be returned immediatley."""
+ def __init__(self, status, content=None, headers={}):
self.response = Response(status, content=content, headers=headers)
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index b99f30f7..6695bf68 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -3,7 +3,7 @@ Tests for content parsing, and form-overloaded content parsing.
"""
from django.test import TestCase
from djangorestframework.compat import RequestFactory
-from djangorestframework.request import RequestMixin
+from djangorestframework.mixins import RequestMixin
from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser
diff --git a/djangorestframework/tests/emitters.py b/djangorestframework/tests/emitters.py
index 7d024ccf..21a7eb95 100644
--- a/djangorestframework/tests/emitters.py
+++ b/djangorestframework/tests/emitters.py
@@ -2,7 +2,8 @@ from django.conf.urls.defaults import patterns, url
from django import http
from django.test import TestCase
from djangorestframework.compat import View
-from djangorestframework.emitters import EmitterMixin, BaseEmitter
+from djangorestframework.emitters import BaseEmitter
+from djangorestframework.mixins import ResponseMixin
from djangorestframework.response import Response
DUMMYSTATUS = 200
@@ -11,7 +12,7 @@ DUMMYCONTENT = 'dummycontent'
EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x
EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x
-class MockView(EmitterMixin, View):
+class MockView(ResponseMixin, View):
def get(self, request):
response = Response(DUMMYSTATUS, DUMMYCONTENT)
return self.emit(response)
diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py
index 7f6acf4f..0e74dc94 100644
--- a/djangorestframework/tests/methods.py
+++ b/djangorestframework/tests/methods.py
@@ -1,6 +1,6 @@
from django.test import TestCase
from djangorestframework.compat import RequestFactory
-from djangorestframework.request import RequestMixin
+from djangorestframework.mixins import RequestMixin
class TestMethodOverloading(TestCase):
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
index a091cf29..b6563db6 100644
--- a/djangorestframework/tests/validators.py
+++ b/djangorestframework/tests/validators.py
@@ -3,7 +3,7 @@ from django.db import models
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
-from djangorestframework.response import ResponseException
+from djangorestframework.response import ErrorResponse
class TestValidatorMixinInterfaces(TestCase):
@@ -81,7 +81,7 @@ class TestNonFieldErrors(TestCase):
content = {'field1': 'example1', 'field2': 'example2'}
try:
FormValidator(view).validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else:
self.fail('ResourceException was not raised') #pragma: no cover
@@ -115,14 +115,14 @@ class TestFormValidation(TestCase):
def validation_failure_raises_response_exception(self, validator):
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
content = {}
- self.assertRaises(ResponseException, validator.validate, content)
+ self.assertRaises(ErrorResponse, validator.validate, content)
def validation_does_not_allow_extra_fields_by_default(self, validator):
"""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, validator.validate, content)
+ self.assertRaises(ErrorResponse, validator.validate, content)
def validation_allows_extra_fields_if_explicitly_set(self, validator):
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
@@ -139,7 +139,7 @@ class TestFormValidation(TestCase):
content = {}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
@@ -149,7 +149,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': ''}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
@@ -159,7 +159,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': 'uiop', 'extra': 'extra'}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, 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
@@ -169,7 +169,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': '', 'extra': 'extra'}
try:
validator.validate(content)
- except ResponseException, exc:
+ except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}})
else:
@@ -286,14 +286,14 @@ class TestModelFormValidator(TestCase):
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': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
- self.assertRaises(ResponseException, self.validator.validate, content)
+ self.assertRaises(ErrorResponse, self.validator.validate, content)
def test_validate_requires_fields_on_model_forms(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 = {'readonly': 'read only'}
- self.assertRaises(ResponseException, self.validator.validate, content)
+ self.assertRaises(ErrorResponse, self.validator.validate, content)
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
diff --git a/djangorestframework/utils.py b/djangorestframework/utils.py
index d45e5acf..8b12294c 100644
--- a/djangorestframework/utils.py
+++ b/djangorestframework/utils.py
@@ -14,6 +14,7 @@ except ImportError:
# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
+MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
def as_tuple(obj):
"""Given obj return a tuple"""
diff --git a/djangorestframework/validators.py b/djangorestframework/validators.py
index 609e6d36..c612de55 100644
--- a/djangorestframework/validators.py
+++ b/djangorestframework/validators.py
@@ -1,7 +1,7 @@
"""Mixin classes that provide a validate(content) function to validate and cleanup request content"""
from django import forms
from django.db import models
-from djangorestframework.response import ResponseException
+from djangorestframework.response import ErrorResponse
from djangorestframework.utils import as_tuple
@@ -13,7 +13,7 @@ class BaseValidator(object):
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
- Typically raises a ResponseException with status code 400 (Bad Request) on failure.
+ Typically raises a ErrorResponse with status code 400 (Bad Request) on failure.
Must be overridden to be implemented."""
raise NotImplementedError()
@@ -32,11 +32,11 @@ class FormValidator(BaseValidator):
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
- Raises a ResponseException with status code 400 (Bad Request) on failure.
+ Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied.
- On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
+ On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
return self._validate(content)
@@ -97,7 +97,7 @@ class FormValidator(BaseValidator):
detail[u'field-errors'] = field_errors
# Return HTTP 400 response (BAD REQUEST)
- raise ResponseException(400, detail)
+ raise ErrorResponse(400, detail)
def get_bound_form(self, content=None):
@@ -139,14 +139,14 @@ class ModelFormValidator(FormValidator):
# TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
def validate(self, content):
"""Given some content as input return some cleaned, validated content.
- Raises a ResponseException with status code 400 (Bad Request) on failure.
+ Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Validation is standard form or model form validation,
with an additional constraint that no extra unknown fields may be supplied,
and that all fields specified by the fields class attribute must be supplied,
even if they are not validated by the form/model form.
- On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
+ On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
return self._validate(content, allowed_extra_fields=self._property_fields_set)
--
cgit v1.2.3
From 6096b50dbe20349144aa92660c6c8467f67f50e7 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 16:54:02 +0100
Subject: request.py -> mixins.py
---
djangorestframework/mixins.py | 435 +++++++++++++++++++++++++++++++++++++++++
djangorestframework/request.py | 280 --------------------------
2 files changed, 435 insertions(+), 280 deletions(-)
create mode 100644 djangorestframework/mixins.py
delete mode 100644 djangorestframework/request.py
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
new file mode 100644
index 00000000..9af79c66
--- /dev/null
+++ b/djangorestframework/mixins.py
@@ -0,0 +1,435 @@
+from djangorestframework.mediatypes import MediaType
+from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
+from djangorestframework.response import ErrorResponse
+from djangorestframework.parsers import FormParser, MultipartParser
+from djangorestframework import status
+
+from django.http import HttpResponse
+from django.http.multipartparser import LimitBytes # TODO: Use LimitedStream in compat
+from StringIO import StringIO
+from decimal import Decimal
+import re
+
+
+
+########## Request Mixin ##########
+
+class RequestMixin(object):
+ """Mixin class to provide request parsing behaviour."""
+
+ USE_FORM_OVERLOADING = True
+ METHOD_PARAM = "_method"
+ CONTENTTYPE_PARAM = "_content_type"
+ CONTENT_PARAM = "_content"
+
+ parsers = ()
+ validators = ()
+
+ def _get_method(self):
+ """
+ Returns the HTTP method for the current view.
+ """
+ if not hasattr(self, '_method'):
+ self._method = self.request.method
+ return self._method
+
+
+ def _set_method(self, method):
+ """
+ Set the method for the current view.
+ """
+ self._method = method
+
+
+ def _get_content_type(self):
+ """
+ Returns a MediaType object, representing the request's content type header.
+ """
+ if not hasattr(self, '_content_type'):
+ content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
+ if content_type:
+ self._content_type = MediaType(content_type)
+ else:
+ self._content_type = None
+ return self._content_type
+
+
+ def _set_content_type(self, content_type):
+ """
+ Set the content type. Should be a MediaType object.
+ """
+ self._content_type = content_type
+
+
+ def _get_accept(self):
+ """
+ Returns a list of MediaType objects, representing the request's accept header.
+ """
+ if not hasattr(self, '_accept'):
+ accept = self.request.META.get('HTTP_ACCEPT', '*/*')
+ self._accept = [MediaType(elem) for elem in accept.split(',')]
+ return self._accept
+
+
+ def _set_accept(self):
+ """
+ Set the acceptable media types. Should be a list of MediaType objects.
+ """
+ self._accept = accept
+
+
+ def _get_stream(self):
+ """
+ Returns an object that may be used to stream the request content.
+ """
+ if not hasattr(self, '_stream'):
+ request = self.request
+
+ try:
+ content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH')))
+ except (ValueError, TypeError):
+ content_length = 0
+
+ # Currently only supports parsing request body as a stream with 1.3
+ if content_length == 0:
+ return None
+ elif hasattr(request, 'read'):
+ # It's not at all clear if this needs to be byte limited or not.
+ # Maybe I'm just being dumb but it looks to me like there's some issues
+ # with that in Django.
+ #
+ # Either:
+ # 1. It *can't* be treated as a limited byte stream, and you _do_ need to
+ # respect CONTENT_LENGTH, in which case that ought to be documented,
+ # and there probably ought to be a feature request for it to be
+ # treated as a limited byte stream.
+ # 2. It *can* be treated as a limited byte stream, in which case there's a
+ # minor bug in the test client, and potentially some redundant
+ # code in MultipartParser.
+ #
+ # It's an issue because it affects if you can pass a request off to code that
+ # does something like:
+ #
+ # while stream.read(BUFFER_SIZE):
+ # [do stuff]
+ #
+ #try:
+ # content_length = int(request.META.get('CONTENT_LENGTH',0))
+ #except (ValueError, TypeError):
+ # content_length = 0
+ # self._stream = LimitedStream(request, content_length)
+ self._stream = request
+ else:
+ self._stream = StringIO(request.raw_post_data)
+ return self._stream
+
+
+ def _set_stream(self, stream):
+ """
+ Set the stream representing the request body.
+ """
+ self._stream = stream
+
+
+ def _get_raw_content(self):
+ """
+ Returns the parsed content of the request
+ """
+ if not hasattr(self, '_raw_content'):
+ self._raw_content = self.parse(self.stream, self.content_type)
+ return self._raw_content
+
+
+ def _get_content(self):
+ """
+ Returns the parsed and validated content of the request
+ """
+ if not hasattr(self, '_content'):
+ self._content = self.validate(self.RAW_CONTENT)
+
+ return self._content
+
+
+ def perform_form_overloading(self):
+ """
+ Check the request to see if it is using form POST '_method'/'_content'/'_content_type' overrides.
+ If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply
+ delegating them to the original request.
+ """
+ if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not self.content_type.is_form():
+ return
+
+ # Temporarily switch to using the form parsers, then parse the content
+ parsers = self.parsers
+ self.parsers = (FormParser, MultipartParser)
+ content = self.RAW_CONTENT
+ self.parsers = parsers
+
+ # Method overloading - change the method and remove the param from the content
+ if self.METHOD_PARAM in content:
+ self.method = content[self.METHOD_PARAM].upper()
+ del self._raw_content[self.METHOD_PARAM]
+
+ # Content overloading - rewind the stream and modify the content type
+ if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content:
+ self._content_type = MediaType(content[self.CONTENTTYPE_PARAM])
+ self._stream = StringIO(content[self.CONTENT_PARAM])
+ del(self._raw_content)
+
+
+ def parse(self, stream, content_type):
+ """
+ Parse the request content.
+
+ 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
+
+ parsers = as_tuple(self.parsers)
+
+ parser = None
+ for parser_cls in parsers:
+ if parser_cls.handles(content_type):
+ parser = parser_cls(self)
+ break
+
+ if parser is None:
+ raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
+ {'error': 'Unsupported media type in request \'%s\'.' %
+ content_type.media_type})
+
+ return parser.parse(stream)
+
+
+ def validate(self, content):
+ """
+ Validate, cleanup, and type-ify the request content.
+ """
+ for validator_cls in self.validators:
+ validator = validator_cls(self)
+ content = validator.validate(content)
+ return content
+
+
+ def get_bound_form(self, content=None):
+ """
+ Return a bound form instance for the given content,
+ if there is an appropriate form validator attached to the view.
+ """
+ for validator_cls in self.validators:
+ if hasattr(validator_cls, 'get_bound_form'):
+ validator = validator_cls(self)
+ return validator.get_bound_form(content)
+ return None
+
+
+ @property
+ def parsed_media_types(self):
+ """Return an list of all the media types that this view can parse."""
+ return [parser.media_type for parser in self.parsers]
+
+
+ @property
+ def default_parser(self):
+ """Return the view's most preffered emitter.
+ (This has no behavioural effect, but is may be used by documenting emitters)"""
+ return self.parsers[0]
+
+
+ method = property(_get_method, _set_method)
+ content_type = property(_get_content_type, _set_content_type)
+ accept = property(_get_accept, _set_accept)
+ stream = property(_get_stream, _set_stream)
+ RAW_CONTENT = property(_get_raw_content)
+ CONTENT = property(_get_content)
+
+
+########## ResponseMixin ##########
+
+class ResponseMixin(object):
+ """Adds behaviour for pluggable Emitters to a :class:`.Resource` or Django :class:`View`. class.
+
+ 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.
+ 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
+ REWRITE_IE_ACCEPT_HEADER = True
+
+ #request = None
+ #response = None
+ emitters = ()
+
+ #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 emit(self, response):
+ """Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
+ self.response = response
+
+ try:
+ emitter = self._determine_emitter(self.request)
+ except ErrorResponse, exc:
+ emitter = self.default_emitter
+ response = exc.response
+
+ # Serialize the response content
+ if response.has_content_body:
+ content = emitter(self).emit(output=response.cleaned_content)
+ else:
+ content = emitter(self).emit()
+
+ # Munge DELETE Response code to allow us to return content
+ # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
+ if response.status == 204:
+ response.status = 200
+
+ # Build the HTTP Response
+ # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
+ resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
+ for (key, val) in response.headers.items():
+ resp[key] = val
+
+ return resp
+
+
+ def _determine_emitter(self, request):
+ """Return the appropriate emitter for the output, given the client's 'Accept' header,
+ and the content types that this Resource knows how to serve.
+
+ See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
+
+ if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
+ # Use _accept parameter override
+ accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
+ elif (self.REWRITE_IE_ACCEPT_HEADER and
+ request.META.has_key('HTTP_USER_AGENT') and
+ MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
+ accept_list = ['text/html', '*/*']
+ elif request.META.has_key('HTTP_ACCEPT'):
+ # Use standard HTTP Accept negotiation
+ accept_list = request.META["HTTP_ACCEPT"].split(',')
+ else:
+ # No accept header specified
+ return self.default_emitter
+
+ # Parse the accept header into a dict of {qvalue: set of media types}
+ # We ignore mietype parameters
+ accept_dict = {}
+ for token in accept_list:
+ components = token.split(';')
+ mimetype = components[0].strip()
+ qvalue = Decimal('1.0')
+
+ if len(components) > 1:
+ # Parse items that have a qvalue eg text/html;q=0.9
+ try:
+ (q, num) = components[-1].split('=')
+ if q == 'q':
+ qvalue = Decimal(num)
+ except:
+ # Skip malformed entries
+ continue
+
+ if accept_dict.has_key(qvalue):
+ accept_dict[qvalue].add(mimetype)
+ else:
+ accept_dict[qvalue] = set((mimetype,))
+
+ # Convert to a list of sets ordered by qvalue (highest first)
+ accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
+
+ for accept_set in accept_sets:
+ # Return any exact match
+ for emitter in self.emitters:
+ if emitter.media_type in accept_set:
+ return emitter
+
+ # Return any subtype match
+ for emitter in self.emitters:
+ if emitter.media_type.split('/')[0] + '/*' in accept_set:
+ return emitter
+
+ # Return default
+ if '*/*' in accept_set:
+ return self.default_emitter
+
+
+ raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
+ {'detail': 'Could not satisfy the client\'s Accept header',
+ 'available_types': self.emitted_media_types})
+
+ @property
+ def emitted_media_types(self):
+ """Return an list of all the media types that this resource can emit."""
+ return [emitter.media_type for emitter in self.emitters]
+
+ @property
+ def default_emitter(self):
+ """Return the resource's most prefered emitter.
+ (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
+ return self.emitters[0]
+
+
+########## Auth Mixin ##########
+
+class AuthMixin(object):
+ """Mixin class to provide authentication and permissions."""
+ authenticators = ()
+ permitters = ()
+
+ @property
+ def auth(self):
+ if not hasattr(self, '_auth'):
+ self._auth = self._authenticate()
+ return self._auth
+
+ # 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):
+ if not self.permissions:
+ return
+
+ auth = self.auth
+ for permitter_cls in self.permitters:
+ permitter = permission_cls(self)
+ permitter.permit(auth)
+
+ def _authenticate(self):
+ for authenticator_cls in self.authenticators:
+ authenticator = authenticator_cls(self)
+ auth = authenticator.authenticate(self.request)
+ if auth:
+ return auth
+ return None
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
deleted file mode 100644
index 33d6bb2f..00000000
--- a/djangorestframework/request.py
+++ /dev/null
@@ -1,280 +0,0 @@
-from djangorestframework.mediatypes import MediaType
-from djangorestframework.utils import as_tuple
-from djangorestframework.response import ResponseException
-from djangorestframework.parsers import FormParser, MultipartParser
-from djangorestframework import status
-
-#from djangorestframework.requestparsing import parse, load_parser
-from django.http.multipartparser import LimitBytes
-from StringIO import StringIO
-
-class RequestMixin(object):
- """Mixin class to provide request parsing behaviour."""
-
- USE_FORM_OVERLOADING = True
- METHOD_PARAM = "_method"
- CONTENTTYPE_PARAM = "_content_type"
- CONTENT_PARAM = "_content"
-
- parsers = ()
- validators = ()
-
- def _get_method(self):
- """
- Returns the HTTP method for the current view.
- """
- if not hasattr(self, '_method'):
- self._method = self.request.method
- return self._method
-
-
- def _set_method(self, method):
- """
- Set the method for the current view.
- """
- self._method = method
-
-
- def _get_content_type(self):
- """
- Returns a MediaType object, representing the request's content type header.
- """
- if not hasattr(self, '_content_type'):
- content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
- if content_type:
- self._content_type = MediaType(content_type)
- else:
- self._content_type = None
- return self._content_type
-
-
- def _set_content_type(self, content_type):
- """
- Set the content type. Should be a MediaType object.
- """
- self._content_type = content_type
-
-
- def _get_accept(self):
- """
- Returns a list of MediaType objects, representing the request's accept header.
- """
- if not hasattr(self, '_accept'):
- accept = self.request.META.get('HTTP_ACCEPT', '*/*')
- self._accept = [MediaType(elem) for elem in accept.split(',')]
- return self._accept
-
-
- def _set_accept(self):
- """
- Set the acceptable media types. Should be a list of MediaType objects.
- """
- self._accept = accept
-
-
- def _get_stream(self):
- """
- Returns an object that may be used to stream the request content.
- """
- if not hasattr(self, '_stream'):
- request = self.request
-
- try:
- content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH')))
- except (ValueError, TypeError):
- content_length = 0
-
- # Currently only supports parsing request body as a stream with 1.3
- if content_length == 0:
- return None
- elif hasattr(request, 'read'):
- # It's not at all clear if this needs to be byte limited or not.
- # Maybe I'm just being dumb but it looks to me like there's some issues
- # with that in Django.
- #
- # Either:
- # 1. It *can't* be treated as a limited byte stream, and you _do_ need to
- # respect CONTENT_LENGTH, in which case that ought to be documented,
- # and there probably ought to be a feature request for it to be
- # treated as a limited byte stream.
- # 2. It *can* be treated as a limited byte stream, in which case there's a
- # minor bug in the test client, and potentially some redundant
- # code in MultipartParser.
- #
- # It's an issue because it affects if you can pass a request off to code that
- # does something like:
- #
- # while stream.read(BUFFER_SIZE):
- # [do stuff]
- #
- #try:
- # content_length = int(request.META.get('CONTENT_LENGTH',0))
- #except (ValueError, TypeError):
- # content_length = 0
- # self._stream = LimitedStream(request, content_length)
- self._stream = request
- else:
- self._stream = StringIO(request.raw_post_data)
- return self._stream
-
-
- def _set_stream(self, stream):
- """
- Set the stream representing the request body.
- """
- self._stream = stream
-
-
- def _get_raw_content(self):
- """
- Returns the parsed content of the request
- """
- if not hasattr(self, '_raw_content'):
- self._raw_content = self.parse(self.stream, self.content_type)
- return self._raw_content
-
-
- def _get_content(self):
- """
- Returns the parsed and validated content of the request
- """
- if not hasattr(self, '_content'):
- self._content = self.validate(self.RAW_CONTENT)
-
- return self._content
-
-
- def perform_form_overloading(self):
- """
- Check the request to see if it is using form POST '_method'/'_content'/'_content_type' overrides.
- If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply
- delegating them to the original request.
- """
- if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not self.content_type.is_form():
- return
-
- # Temporarily switch to using the form parsers, then parse the content
- parsers = self.parsers
- self.parsers = (FormParser, MultipartParser)
- content = self.RAW_CONTENT
- self.parsers = parsers
-
- # Method overloading - change the method and remove the param from the content
- if self.METHOD_PARAM in content:
- self.method = content[self.METHOD_PARAM].upper()
- del self._raw_content[self.METHOD_PARAM]
-
- # Content overloading - rewind the stream and modify the content type
- if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content:
- self._content_type = MediaType(content[self.CONTENTTYPE_PARAM])
- self._stream = StringIO(content[self.CONTENT_PARAM])
- del(self._raw_content)
-
-
- def parse(self, stream, content_type):
- """
- Parse the request content.
-
- May raise a 415 ResponseException (Unsupported Media Type),
- or a 400 ResponseException (Bad Request).
- """
- if stream is None or content_type is None:
- return None
-
- parsers = as_tuple(self.parsers)
-
- parser = None
- for parser_cls in parsers:
- if parser_cls.handles(content_type):
- parser = parser_cls(self)
- break
-
- if parser is None:
- raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
- {'error': 'Unsupported media type in request \'%s\'.' %
- content_type.media_type})
-
- return parser.parse(stream)
-
-
- def validate(self, content):
- """
- Validate, cleanup, and type-ify the request content.
- """
- for validator_cls in self.validators:
- validator = validator_cls(self)
- content = validator.validate(content)
- return content
-
-
- def get_bound_form(self, content=None):
- """
- Return a bound form instance for the given content,
- if there is an appropriate form validator attached to the view.
- """
- for validator_cls in self.validators:
- if hasattr(validator_cls, 'get_bound_form'):
- validator = validator_cls(self)
- return validator.get_bound_form(content)
- return None
-
-
- @property
- def parsed_media_types(self):
- """Return an list of all the media types that this view can parse."""
- return [parser.media_type for parser in self.parsers]
-
- @property
- def default_parser(self):
- """Return the view's most preffered emitter.
- (This has no behavioural effect, but is may be used by documenting emitters)"""
- return self.parsers[0]
-
- method = property(_get_method, _set_method)
- content_type = property(_get_content_type, _set_content_type)
- accept = property(_get_accept, _set_accept)
- stream = property(_get_stream, _set_stream)
- RAW_CONTENT = property(_get_raw_content)
- CONTENT = property(_get_content)
-
-
-
-class AuthMixin(object):
- """Mixin class to provide authentication and permissions."""
- authenticators = ()
- permitters = ()
-
- @property
- def auth(self):
- if not hasattr(self, '_auth'):
- self._auth = self._authenticate()
- return self._auth
-
- # 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):
- if not self.permissions:
- return
-
- auth = self.auth
- for permitter_cls in self.permitters:
- permitter = permission_cls(self)
- permitter.permit(auth)
-
- def _authenticate(self):
- for authenticator_cls in self.authenticators:
- authenticator = authenticator_cls(self)
- auth = authenticator.authenticate(self.request)
- if auth:
- return auth
- return None
-
--
cgit v1.2.3
From 18bbda84b9c663fec6eede112a21cf1a48103303 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 17:13:11 +0100
Subject: depercate auth and content arguments to the request handler methods -
yea :)
---
djangorestframework/authenticators.py | 5 +++--
djangorestframework/modelresource.py | 18 +++++++++---------
djangorestframework/resource.py | 14 +++++---------
djangorestframework/tests/accept.py | 2 +-
djangorestframework/tests/authentication.py | 2 +-
djangorestframework/tests/files.py | 7 +++----
djangorestframework/tests/reverse.py | 2 +-
7 files changed, 23 insertions(+), 27 deletions(-)
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
index e382de10..e8331cc7 100644
--- a/djangorestframework/authenticators.py
+++ b/djangorestframework/authenticators.py
@@ -28,8 +28,9 @@ class BaseAuthenticator(object):
The default permission checking on Resource will use the allowed_methods attribute
for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
- The authentication context is passed to the method calls eg Resource.get(request, auth) in order to
- allow them to apply any more fine grained permission checking at the point the response is being generated.
+ The authentication context is available to the method calls eg Resource.get(request)
+ 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.
This function must be overridden to be implemented."""
return None
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index b0a4b1c1..23a87e65 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -341,7 +341,7 @@ class ModelResource(Resource):
return _any(data, self.fields)
- def post(self, request, auth, content, *args, **kwargs):
+ def post(self, request, *args, **kwargs):
# TODO: test creation on a non-existing resource url
# translated related_field into related_field_id
@@ -350,7 +350,7 @@ class ModelResource(Resource):
kwargs[related_name + '_id'] = kwargs[related_name]
del kwargs[related_name]
- all_kw_args = dict(content.items() + kwargs.items())
+ all_kw_args = dict(self.CONTENT.items() + kwargs.items())
if args:
instance = self.model(pk=args[-1], **all_kw_args)
else:
@@ -361,7 +361,7 @@ class ModelResource(Resource):
headers['Location'] = instance.get_absolute_url()
return Response(status.HTTP_201_CREATED, instance, headers)
- def get(self, request, auth, *args, **kwargs):
+ def get(self, request, *args, **kwargs):
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
@@ -374,7 +374,7 @@ class ModelResource(Resource):
return instance
- def put(self, request, auth, content, *args, **kwargs):
+ def put(self, request, *args, **kwargs):
# 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:
if args:
@@ -384,16 +384,16 @@ class ModelResource(Resource):
# Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs)
- for (key, val) in content.items():
+ for (key, val) in self.CONTENT.items():
setattr(instance, key, val)
except self.model.DoesNotExist:
- instance = self.model(**content)
+ instance = self.model(**self.CONTENT)
instance.save()
instance.save()
return instance
- def delete(self, request, auth, *args, **kwargs):
+ def delete(self, request, *args, **kwargs):
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
@@ -413,7 +413,7 @@ class RootModelResource(ModelResource):
allowed_methods = ('GET', 'POST')
queryset = None
- def get(self, request, auth, *args, **kwargs):
+ def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filter(**kwargs)
@@ -427,7 +427,7 @@ class QueryModelResource(ModelResource):
def get_form(self, data=None):
return None
- def get(self, request, auth, *args, **kwargs):
+ def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filer(**kwargs)
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index f4460c1e..0615a164 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -57,22 +57,22 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
callmap = { 'GET': 'get', 'POST': 'post',
'PUT': 'put', 'DELETE': 'delete' }
- def get(self, request, auth, *args, **kwargs):
+ def get(self, request, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('GET')
- def post(self, request, auth, content, *args, **kwargs):
+ def post(self, request, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('POST')
- def put(self, request, auth, content, *args, **kwargs):
+ def put(self, request, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('PUT')
- def delete(self, request, auth, *args, **kwargs):
+ def delete(self, request, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('DELETE')
@@ -154,11 +154,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
# Either generate the response data, deserializing and validating any request data
# TODO: This is going to change to: func(request, *args, **kwargs)
# That'll work out now that we have the lazily evaluated self.CONTENT property.
- if self.method in ('PUT', 'POST'):
- response_obj = func(request, auth_context, self.CONTENT, *args, **kwargs)
-
- else:
- response_obj = func(request, auth_context, *args, **kwargs)
+ response_obj = func(request, *args, **kwargs)
# Allow return value to be either Response, or an object, or None
if isinstance(response_obj, Response):
diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py
index f2a21277..726e1252 100644
--- a/djangorestframework/tests/accept.py
+++ b/djangorestframework/tests/accept.py
@@ -20,7 +20,7 @@ class UserAgentMungingTest(TestCase):
def setUp(self):
class MockResource(Resource):
anon_allowed_methods = allowed_methods = ('GET',)
- def get(self, request, auth):
+ def get(self, request):
return {'a':1, 'b':2, 'c':3}
self.req = RequestFactory()
self.MockResource = MockResource
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index af9c34ca..b2bc4446 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -15,7 +15,7 @@ except ImportError:
class MockResource(Resource):
allowed_methods = ('POST',)
- def post(self, request, auth, content):
+ def post(self, request):
return {'a':1, 'b':2, 'c':3}
urlpatterns = patterns('',
diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py
index e155f181..dd4689a6 100644
--- a/djangorestframework/tests/files.py
+++ b/djangorestframework/tests/files.py
@@ -19,10 +19,9 @@ class UploadFilesTests(TestCase):
allowed_methods = anon_allowed_methods = ('POST',)
form = FileForm
- def post(self, request, auth, content, *args, **kwargs):
- #self.uploaded = content.file
- return {'FILE_NAME': content['file'].name,
- 'FILE_CONTENT': content['file'].read()}
+ def post(self, request, *args, **kwargs):
+ return {'FILE_NAME': self.CONTENT['file'].name,
+ 'FILE_CONTENT': self.CONTENT['file'].read()}
file = StringIO.StringIO('stuff')
file.name = 'stuff.txt'
diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py
index a862e39a..f6a3ea51 100644
--- a/djangorestframework/tests/reverse.py
+++ b/djangorestframework/tests/reverse.py
@@ -14,7 +14,7 @@ class MockResource(Resource):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
anon_allowed_methods = ('GET',)
- def get(self, request, auth):
+ def get(self, request):
return reverse('another')
urlpatterns = patterns('',
--
cgit v1.2.3
From cb4b4f6be6eeac3d2383614998a5e1436cb4226e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 11 Apr 2011 17:18:03 +0100
Subject: Remove some temporary debugging stuff
---
djangorestframework/resource.py | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 0615a164..1e79c79f 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -169,21 +169,13 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
except ErrorResponse, exc:
response = exc.response
-
- except:
- import traceback
- traceback.print_exc()
# Always add these headers.
#
# TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
- try:
- response.headers['Allow'] = ', '.join(self.allowed_methods)
- response.headers['Vary'] = 'Authenticate, Accept'
+ response.headers['Allow'] = ', '.join(self.allowed_methods)
+ response.headers['Vary'] = 'Authenticate, Accept'
- return self.emit(response)
- except:
- import traceback
- traceback.print_exc()
+ return self.emit(response)
--
cgit v1.2.3
From 4692374e0d6f020f8a7a95f3a60094d525c59341 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 25 Apr 2011 01:03:23 +0100
Subject: Generic permissions added, allowed_methods and anon_allowed_methods
now defunct, dispatch now mirrors View.dispatch more nicely
---
djangorestframework/authenticators.py | 12 +-
djangorestframework/mixins.py | 30 ++---
djangorestframework/modelresource.py | 6 +-
djangorestframework/resource.py | 163 +++++++++++-----------------
djangorestframework/tests/accept.py | 5 +-
djangorestframework/tests/authentication.py | 2 -
djangorestframework/tests/files.py | 2 +-
djangorestframework/tests/reverse.py | 8 +-
8 files changed, 98 insertions(+), 130 deletions(-)
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
index e8331cc7..e6f51dd5 100644
--- a/djangorestframework/authenticators.py
+++ b/djangorestframework/authenticators.py
@@ -13,10 +13,10 @@ import base64
class BaseAuthenticator(object):
"""All authenticators should extend BaseAuthenticator."""
- def __init__(self, mixin):
+ def __init__(self, view):
"""Initialise the authenticator with the mixin instance as state,
in case the authenticator needs to access any metadata on the mixin object."""
- self.mixin = mixin
+ self.view = view
def authenticate(self, request):
"""Authenticate the request and return the authentication context or None.
@@ -61,11 +61,13 @@ class BasicAuthenticator(BaseAuthenticator):
class UserLoggedInAuthenticator(BaseAuthenticator):
- """Use Djagno's built-in request session for authentication."""
+ """Use Django's built-in request session for authentication."""
def authenticate(self, request):
if getattr(request, 'user', None) and request.user.is_active:
- # Temporarily request.POST with .RAW_CONTENT, so that we use our more generic request parsing
- request._post = self.mixin.RAW_CONTENT
+ # Temporarily set request.POST to view.RAW_CONTENT,
+ # so that we use our more generic request parsing,
+ # in preference to Django's form-only request parsing.
+ request._post = self.view.RAW_CONTENT
resp = CsrfViewMiddleware().process_view(request, None, (), {})
del(request._post)
if resp is None: # csrf passed
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 9af79c66..53262366 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -396,9 +396,9 @@ class ResponseMixin(object):
########## Auth Mixin ##########
class AuthMixin(object):
- """Mixin class to provide authentication and permissions."""
+ """Mixin class to provide authentication and permission checking."""
authenticators = ()
- permitters = ()
+ permissions = ()
@property
def auth(self):
@@ -406,6 +406,14 @@ class AuthMixin(object):
self._auth = self._authenticate()
return self._auth
+ def _authenticate(self):
+ for authenticator_cls in self.authenticators:
+ authenticator = authenticator_cls(self)
+ auth = authenticator.authenticate(self.request)
+ if auth:
+ return auth
+ return None
+
# TODO?
#@property
#def user(self):
@@ -421,15 +429,11 @@ class AuthMixin(object):
if not self.permissions:
return
- auth = self.auth
- for permitter_cls in self.permitters:
- permitter = permission_cls(self)
- permitter.permit(auth)
+ for permission_cls in self.permissions:
+ permission = permission_cls(self)
+ if not permission.has_permission(self.auth):
+ raise ErrorResponse(status.HTTP_403_FORBIDDEN,
+ {'detail': 'You do not have permission to access this resource. ' +
+ 'You may need to login or otherwise authenticate the request.'})
+
- def _authenticate(self):
- for authenticator_cls in self.authenticators:
- authenticator = authenticator_cls(self)
- auth = authenticator.authenticate(self.request)
- if auth:
- return auth
- return None
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index 23a87e65..24fb62ab 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -410,13 +410,13 @@ class ModelResource(Resource):
class RootModelResource(ModelResource):
"""A Resource which provides default operations for list and create."""
- allowed_methods = ('GET', 'POST')
queryset = None
def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filter(**kwargs)
+ put = delete = http_method_not_allowed
class QueryModelResource(ModelResource):
"""Resource with default operations for list.
@@ -424,10 +424,8 @@ class QueryModelResource(ModelResource):
allowed_methods = ('GET',)
queryset = None
- def get_form(self, data=None):
- return None
-
def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filer(**kwargs)
+ post = put = delete = http_method_not_allowed
\ No newline at end of file
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 1e79c79f..55a9b57d 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
-from djangorestframework import emitters, parsers, authenticators, validators, status
+from djangorestframework import emitters, parsers, authenticators, permissions, validators, status
# TODO: Figure how out references and named urls need to work nicely
@@ -19,11 +19,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
"""Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization."""
- # List of RESTful operations which may be performed on this resource.
- # These are going to get dropped at some point, the allowable methods will be defined simply by
- # which methods are present on the request (in the same way as Django's generic View)
- allowed_methods = ('GET',)
- anon_allowed_methods = ()
+ http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace', 'patch']
# List of emitters the resource can serialize the response with, ordered by preference.
emitters = ( emitters.JSONEmitter,
@@ -37,12 +33,15 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
parsers.FormParser,
parsers.MultipartParser )
- # List of validators to validate, cleanup and type-ify the request content
+ # List of validators to validate, cleanup and normalize the request content
validators = ( validators.FormValidator, )
# List of all authenticating methods to attempt.
authenticators = ( authenticators.UserLoggedInAuthenticator,
authenticators.BasicAuthenticator )
+
+ # List of all permissions required to access the resource
+ permissions = ( permissions.DeleteMePermission, )
# Optional form for input validation and presentation of HTML formatted responses.
form = None
@@ -53,52 +52,14 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
name = None
description = None
- # Map standard HTTP methods to function calls
- callmap = { 'GET': 'get', 'POST': 'post',
- 'PUT': 'put', 'DELETE': 'delete' }
-
- def get(self, request, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('GET')
-
-
- def post(self, request, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('POST')
-
-
- def put(self, request, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('PUT')
-
-
- def delete(self, request, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('DELETE')
+ @property
+ def allowed_methods(self):
+ return [method.upper() for method in self.http_method_names if hasattr(self, method)]
-
- def not_implemented(self, operation):
- """Return an HTTP 500 server error if an operation is called which has been allowed by
- allowed_methods, but which has not been implemented."""
- raise ErrorResponse(status.HTTP_500_INTERNAL_SERVER_ERROR,
- {'detail': '%s operation on this resource has not been implemented' % (operation, )})
-
-
- def check_method_allowed(self, method, auth):
- """Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
-
- if not method in self.callmap.keys():
- raise ErrorResponse(status.HTTP_501_NOT_IMPLEMENTED,
- {'detail': 'Unknown or unsupported method \'%s\'' % method})
-
- if not method in self.allowed_methods:
- raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
- {'detail': 'Method \'%s\' not allowed on this resource.' % method})
-
- if auth is None and not method in self.anon_allowed_methods:
- raise ErrorResponse(status.HTTP_403_FORBIDDEN,
- {'detail': 'You do not have permission to access this resource. ' +
- 'You may need to login or otherwise authenticate the request.'})
+ def http_method_not_allowed(self, request, *args, **kwargs):
+ """Return an HTTP 405 error if an operation is called which does not have a handler method."""
+ raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
+ {'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
def cleanup_response(self, data):
@@ -111,6 +72,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
the EmitterMixin and Emitter classes."""
return data
+
# Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt.
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
@@ -125,57 +87,54 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
4. cleanup the response data
5. serialize response data into response content, using standard HTTP content negotiation
"""
-
- self.request = request
-
- # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
- prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
- set_script_prefix(prefix)
-
try:
- # Authenticate the request, and store any context so that the resource operations can
- # do more fine grained authentication if required.
+ self.request = request
+ self.args = args
+ self.kwargs = kwargs
+
+ # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
+ prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
+ set_script_prefix(prefix)
+
+ try:
+ # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
+ # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
+ self.perform_form_overloading()
+
+ # Authenticate and check request is has the relevant permissions
+ self.check_permissions()
+
+ # Get the appropriate handler method
+ if self.method.lower() in self.http_method_names:
+ handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
+ else:
+ handler = self.http_method_not_allowed
+
+ response_obj = handler(request, *args, **kwargs)
+
+ # Allow return value to be either Response, or an object, or None
+ if isinstance(response_obj, Response):
+ response = response_obj
+ elif response_obj is not None:
+ response = Response(status.HTTP_200_OK, response_obj)
+ else:
+ response = Response(status.HTTP_204_NO_CONTENT)
+
+ # Pre-serialize filtering (eg filter complex objects into natively serializable types)
+ response.cleaned_content = self.cleanup_response(response.raw_content)
+
+ except ErrorResponse, exc:
+ response = exc.response
+
+ # Always add these headers.
#
- # Typically the context will be a user, or None if this is an anonymous request,
- # but it could potentially be more complex (eg the context of a request key which
- # has been signed against a particular set of permissions)
- auth_context = self.auth
-
- # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
- # self.method, self.content_type, self.CONTENT appropriately.
- self.perform_form_overloading()
-
- # Ensure the requested operation is permitted on this resource
- self.check_method_allowed(self.method, auth_context)
-
- # Get the appropriate create/read/update/delete function
- func = getattr(self, self.callmap.get(self.method, None))
+ # TODO - this isn't actually the correct way to set the vary header,
+ # also it's currently sub-obtimal for HTTP caching - need to sort that out.
+ response.headers['Allow'] = ', '.join(self.allowed_methods)
+ response.headers['Vary'] = 'Authenticate, Accept'
- # Either generate the response data, deserializing and validating any request data
- # TODO: This is going to change to: func(request, *args, **kwargs)
- # That'll work out now that we have the lazily evaluated self.CONTENT property.
- response_obj = func(request, *args, **kwargs)
-
- # Allow return value to be either Response, or an object, or None
- if isinstance(response_obj, Response):
- response = response_obj
- elif response_obj is not None:
- response = Response(status.HTTP_200_OK, response_obj)
- else:
- response = Response(status.HTTP_204_NO_CONTENT)
-
- # Pre-serialize filtering (eg filter complex objects into natively serializable types)
- response.cleaned_content = self.cleanup_response(response.raw_content)
-
- except ErrorResponse, exc:
- response = exc.response
-
- # Always add these headers.
- #
- # TODO - this isn't actually the correct way to set the vary header,
- # also it's currently sub-obtimal for HTTP caching - need to sort that out.
- response.headers['Allow'] = ', '.join(self.allowed_methods)
- response.headers['Vary'] = 'Authenticate, Accept'
-
- return self.emit(response)
+ return self.emit(response)
+ except:
+ import traceback
+ traceback.print_exc()
diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py
index 726e1252..b12dc757 100644
--- a/djangorestframework/tests/accept.py
+++ b/djangorestframework/tests/accept.py
@@ -18,10 +18,13 @@ class UserAgentMungingTest(TestCase):
http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
def setUp(self):
+
class MockResource(Resource):
- anon_allowed_methods = allowed_methods = ('GET',)
+ permissions = ()
+
def get(self, request):
return {'a':1, 'b':2, 'c':3}
+
self.req = RequestFactory()
self.MockResource = MockResource
self.view = MockResource.as_view()
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index b2bc4446..c825883d 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -13,8 +13,6 @@ except ImportError:
import simplejson as json
class MockResource(Resource):
- allowed_methods = ('POST',)
-
def post(self, request):
return {'a':1, 'b':2, 'c':3}
diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py
index dd4689a6..4dc3aa40 100644
--- a/djangorestframework/tests/files.py
+++ b/djangorestframework/tests/files.py
@@ -16,7 +16,7 @@ class UploadFilesTests(TestCase):
file = forms.FileField
class MockResource(Resource):
- allowed_methods = anon_allowed_methods = ('POST',)
+ permissions = ()
form = FileForm
def post(self, request, *args, **kwargs):
diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py
index f6a3ea51..1f9071b3 100644
--- a/djangorestframework/tests/reverse.py
+++ b/djangorestframework/tests/reverse.py
@@ -12,7 +12,7 @@ except ImportError:
class MockResource(Resource):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
- anon_allowed_methods = ('GET',)
+ permissions = ()
def get(self, request):
return reverse('another')
@@ -28,5 +28,9 @@ class ReverseTests(TestCase):
urls = 'djangorestframework.tests.reverse'
def test_reversed_urls_are_fully_qualified(self):
- response = self.client.get('/')
+ try:
+ response = self.client.get('/')
+ except:
+ import traceback
+ traceback.print_exc()
self.assertEqual(json.loads(response.content), 'http://testserver/another')
--
cgit v1.2.3
From 84a4fd3ea11a55441cb5b8acd584c76fc325edcc Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 25 Apr 2011 04:48:55 +0100
Subject: tidy up
---
djangorestframework/modelresource.py | 26 ++++-----
djangorestframework/resource.py | 109 +++++++++++++++--------------------
2 files changed, 61 insertions(+), 74 deletions(-)
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index 24fb62ab..a91c79ee 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -341,6 +341,19 @@ class ModelResource(Resource):
return _any(data, self.fields)
+ def get(self, request, *args, **kwargs):
+ try:
+ if args:
+ # If we have any none kwargs then assume the last represents the primrary key
+ instance = self.model.objects.get(pk=args[-1], **kwargs)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+ except self.model.DoesNotExist:
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND)
+
+ return instance
+
def post(self, request, *args, **kwargs):
# TODO: test creation on a non-existing resource url
@@ -361,19 +374,6 @@ class ModelResource(Resource):
headers['Location'] = instance.get_absolute_url()
return Response(status.HTTP_201_CREATED, instance, headers)
- def get(self, request, *args, **kwargs):
- try:
- if args:
- # If we have any none kwargs then assume the last represents the primrary key
- instance = self.model.objects.get(pk=args[-1], **kwargs)
- else:
- # Otherwise assume the kwargs uniquely identify the model
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
- raise ErrorResponse(status.HTTP_404_NOT_FOUND)
-
- return instance
-
def put(self, request, *args, **kwargs):
# 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:
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 55a9b57d..65aa09c6 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -16,8 +16,8 @@ __all__ = ['Resource']
class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
- """Handles incoming requests and maps them to REST operations,
- performing authentication, input deserialization, input validation, output serialization."""
+ """Handles incoming requests and maps them to REST operations.
+ Performs request deserialization, response serialization, authentication and input validation."""
http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace', 'patch']
@@ -73,68 +73,55 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
return data
- # Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt.
+ # Note: session based authentication is explicitly CSRF validated,
+ # all other authentication is CSRF exempt.
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
- """This method is the core of Resource, through which all requests are passed.
+ self.request = request
+ self.args = args
+ self.kwargs = kwargs
- Broadly this consists of the following procedure:
+ # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
+ prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
+ set_script_prefix(prefix)
- 0. ensure the operation is permitted
- 1. deserialize request content into request data, using standard HTTP content types (PUT/POST only)
- 2. cleanup and validate request data (PUT/POST only)
- 3. call the core method to get the response data
- 4. cleanup the response data
- 5. serialize response data into response content, using standard HTTP content negotiation
- """
try:
- self.request = request
- self.args = args
- self.kwargs = kwargs
-
- # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
- prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
- set_script_prefix(prefix)
-
- try:
- # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
- # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
- self.perform_form_overloading()
-
- # Authenticate and check request is has the relevant permissions
- self.check_permissions()
-
- # Get the appropriate handler method
- if self.method.lower() in self.http_method_names:
- handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
- else:
- handler = self.http_method_not_allowed
-
- response_obj = handler(request, *args, **kwargs)
-
- # Allow return value to be either Response, or an object, or None
- if isinstance(response_obj, Response):
- response = response_obj
- elif response_obj is not None:
- response = Response(status.HTTP_200_OK, response_obj)
- else:
- response = Response(status.HTTP_204_NO_CONTENT)
-
- # Pre-serialize filtering (eg filter complex objects into natively serializable types)
- response.cleaned_content = self.cleanup_response(response.raw_content)
-
- except ErrorResponse, exc:
- response = exc.response
-
- # Always add these headers.
- #
- # TODO - this isn't actually the correct way to set the vary header,
- # also it's currently sub-obtimal for HTTP caching - need to sort that out.
- response.headers['Allow'] = ', '.join(self.allowed_methods)
- response.headers['Vary'] = 'Authenticate, Accept'
-
- return self.emit(response)
- except:
- import traceback
- traceback.print_exc()
+ # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
+ # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
+ self.perform_form_overloading()
+
+ # Authenticate and check request is has the relevant permissions
+ self.check_permissions()
+
+ # Get the appropriate handler method
+ if self.method.lower() in self.http_method_names:
+ handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
+ else:
+ handler = self.http_method_not_allowed
+
+ response_obj = handler(request, *args, **kwargs)
+
+ # Allow return value to be either Response, or an object, or None
+ if isinstance(response_obj, Response):
+ response = response_obj
+ elif response_obj is not None:
+ response = Response(status.HTTP_200_OK, response_obj)
+ else:
+ response = Response(status.HTTP_204_NO_CONTENT)
+
+ # Pre-serialize filtering (eg filter complex objects into natively serializable types)
+ response.cleaned_content = self.cleanup_response(response.raw_content)
+
+ except ErrorResponse, exc:
+ response = exc.response
+
+ # Always add these headers.
+ #
+ # TODO - this isn't actually the correct way to set the vary header,
+ # also it's currently sub-obtimal for HTTP caching - need to sort that out.
+ response.headers['Allow'] = ', '.join(self.allowed_methods)
+ response.headers['Vary'] = 'Authenticate, Accept'
+
+ return self.emit(response)
+
--
cgit v1.2.3
From 762a52edde09297e87c640797219c9bb8255d50a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 25 Apr 2011 04:50:28 +0100
Subject: Fix some compat issues with json/simplejson
---
djangorestframework/compat.py | 10 +++++++++-
djangorestframework/emitters.py | 6 +-----
djangorestframework/parsers.py | 13 +++----------
djangorestframework/tests/authentication.py | 12 ++++++------
djangorestframework/tests/reverse.py | 5 +----
examples/blogpost/tests.py | 13 +++++--------
examples/pygments_api/tests.py | 8 ++++----
7 files changed, 29 insertions(+), 38 deletions(-)
diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py
index 3e82bd98..22b57186 100644
--- a/djangorestframework/compat.py
+++ b/djangorestframework/compat.py
@@ -125,4 +125,12 @@ except:
# 'request': self.request
# }
#)
- return http.HttpResponseNotAllowed(allowed_methods)
\ No newline at end of file
+ return http.HttpResponseNotAllowed(allowed_methods)
+
+# parse_qs
+try:
+ # python >= ?
+ from urlparse import parse_qs
+except ImportError:
+ # python <= ?
+ from cgi import parse_qs
\ No newline at end of file
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index 2990d313..39046919 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -5,6 +5,7 @@ and providing forms and links depending on the allowed methods, emitters and par
"""
from django.conf import settings
from django.template import RequestContext, loader
+from django.utils import simplejson as json
from django import forms
from djangorestframework.response import ErrorResponse
@@ -18,11 +19,6 @@ from urllib import quote_plus
import string
import re
-try:
- import json
-except ImportError:
- import simplejson as json
-
# TODO: Rename verbose to something more appropriate
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 707b61d5..caa76277 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -9,20 +9,13 @@ We need a method to be able to:
and multipart/form-data. (eg also handle multipart/json)
"""
from django.http.multipartparser import MultiPartParser as DjangoMPParser
+from django.utils import simplejson as json
+
from djangorestframework.response import ErrorResponse
from djangorestframework import status
from djangorestframework.utils import as_tuple
from djangorestframework.mediatypes import MediaType
-
-try:
- import json
-except ImportError:
- import simplejson as json
-
-try:
- from urlparse import parse_qs
-except ImportError:
- from cgi import parse_qs
+from djangorestframework.compat import parse_qs
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index c825883d..72300506 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -1,16 +1,15 @@
from django.conf.urls.defaults import patterns
from django.test import TestCase
from django.test import Client
-from djangorestframework.compat import RequestFactory
-from djangorestframework.resource import Resource
from django.contrib.auth.models import User
from django.contrib.auth import login
+from django.utils import simplejson as json
+
+from djangorestframework.compat import RequestFactory
+from djangorestframework.resource import Resource
import base64
-try:
- import json
-except ImportError:
- import simplejson as json
+
class MockResource(Resource):
def post(self, request):
@@ -86,3 +85,4 @@ class SessionAuthTests(TestCase):
"""Ensure POSTing form over session authentication without logged in user fails."""
response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
+
diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py
index 1f9071b3..28fee63b 100644
--- a/djangorestframework/tests/reverse.py
+++ b/djangorestframework/tests/reverse.py
@@ -1,13 +1,10 @@
from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
from django.test import TestCase
+from django.utils import simplejson as json
from djangorestframework.resource import Resource
-try:
- import json
-except ImportError:
- import simplejson as json
class MockResource(Resource):
diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py
index dfb4d5f5..14b0914d 100644
--- a/examples/blogpost/tests.py
+++ b/examples/blogpost/tests.py
@@ -1,13 +1,15 @@
"""Test a range of REST API usage of the example application.
"""
-from django.test import TestCase
from django.core.urlresolvers import reverse
+from django.test import TestCase
+from django.utils import simplejson as json
+
+from djangorestframework.compat import RequestFactory
+
from blogpost import views, models
import blogpost
-#import json
-#from rest.utils import xml2dict, dict2xml
class AcceptHeaderTests(TestCase):
"""Test correct behaviour of the Accept header as specified by RFC 2616:
@@ -164,11 +166,6 @@ class AllowedMethodsTests(TestCase):
#above testcases need to probably moved to the core
-from djangorestframework.compat import RequestFactory
-try:
- import json
-except ImportError:
- import simplejson as json
class TestRotation(TestCase):
"""For the example the maximum amount of Blogposts is capped off at views.MAX_POSTS.
diff --git a/examples/pygments_api/tests.py b/examples/pygments_api/tests.py
index 017823b9..a8f085cf 100644
--- a/examples/pygments_api/tests.py
+++ b/examples/pygments_api/tests.py
@@ -1,12 +1,12 @@
from django.test import TestCase
+from django.utils import simplejson as json
+
from djangorestframework.compat import RequestFactory
+
from pygments_api import views
import tempfile, shutil
-try:
- import json
-except ImportError:
- import simplejson as json
+
class TestPygmentsExample(TestCase):
--
cgit v1.2.3
From 028851bfa1ee44b8e92808b18d32278d4a473cc8 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 27 Apr 2011 18:07:28 +0100
Subject: Fix up tests and examples after refactoring
---
djangorestframework/authenticators.py | 8 +++++++-
djangorestframework/modelresource.py | 4 ++--
djangorestframework/resource.py | 8 ++++++--
djangorestframework/tests/authentication.py | 2 ++
examples/blogpost/views.py | 4 ----
examples/mixin/urls.py | 5 +++--
examples/modelresourceexample/views.py | 2 --
examples/pygments_api/views.py | 24 +++++++++++-------------
examples/resourceexample/views.py | 10 ++++------
examples/sandbox/views.py | 3 +--
10 files changed, 36 insertions(+), 34 deletions(-)
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
index e6f51dd5..19181b7d 100644
--- a/djangorestframework/authenticators.py
+++ b/djangorestframework/authenticators.py
@@ -73,4 +73,10 @@ class UserLoggedInAuthenticator(BaseAuthenticator):
if resp is None: # csrf passed
return request.user
return None
-
+
+
+#class DigestAuthentication(BaseAuthentication):
+# pass
+#
+#class OAuthAuthentication(BaseAuthentication):
+# pass
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index a91c79ee..1afd7fa0 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -416,7 +416,7 @@ class RootModelResource(ModelResource):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filter(**kwargs)
- put = delete = http_method_not_allowed
+ put = delete = None
class QueryModelResource(ModelResource):
"""Resource with default operations for list.
@@ -428,4 +428,4 @@ class QueryModelResource(ModelResource):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filer(**kwargs)
- post = put = delete = http_method_not_allowed
\ No newline at end of file
+ post = put = delete = None
\ No newline at end of file
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 65aa09c6..fbf51cfc 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -41,7 +41,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
authenticators.BasicAuthenticator )
# List of all permissions required to access the resource
- permissions = ( permissions.DeleteMePermission, )
+ permissions = ()
# Optional form for input validation and presentation of HTML formatted responses.
form = None
@@ -54,7 +54,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
@property
def allowed_methods(self):
- return [method.upper() for method in self.http_method_names if hasattr(self, method)]
+ return [method.upper() for method in self.http_method_names if getattr(self, method, None)]
def http_method_not_allowed(self, request, *args, **kwargs):
"""Return an HTTP 405 error if an operation is called which does not have a handler method."""
@@ -96,6 +96,9 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
# Get the appropriate handler method
if self.method.lower() in self.http_method_names:
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
+ # If a previously defined method has been disabled
+ if handler is None:
+ handler = self.http_method_not_allowed
else:
handler = self.http_method_not_allowed
@@ -125,3 +128,4 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
return self.emit(response)
+
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index 72300506..f2c249a6 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -7,11 +7,13 @@ from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
from djangorestframework.resource import Resource
+from djangorestframework import permissions
import base64
class MockResource(Resource):
+ permissions = ( permissions.IsAuthenticated, )
def post(self, request):
return {'a':1, 'b':2, 'c':3}
diff --git a/examples/blogpost/views.py b/examples/blogpost/views.py
index 59a3fb9f..9e07aa8a 100644
--- a/examples/blogpost/views.py
+++ b/examples/blogpost/views.py
@@ -8,25 +8,21 @@ MAX_POSTS = 10
class BlogPosts(RootModelResource):
"""A resource with which lists all existing blog posts and creates new blog posts."""
- anon_allowed_methods = allowed_methods = ('GET', 'POST',)
model = models.BlogPost
fields = BLOG_POST_FIELDS
class BlogPostInstance(ModelResource):
"""A resource which represents a single blog post."""
- anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
model = models.BlogPost
fields = BLOG_POST_FIELDS
class Comments(RootModelResource):
"""A resource which lists all existing comments for a given blog post, and creates new blog comments for a given blog post."""
- anon_allowed_methods = allowed_methods = ('GET', 'POST',)
model = models.Comment
fields = COMMENT_FIELDS
class CommentInstance(ModelResource):
"""A resource which represents a single comment."""
- anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
model = models.Comment
fields = COMMENT_FIELDS
diff --git a/examples/mixin/urls.py b/examples/mixin/urls.py
index 05009284..96b630e3 100644
--- a/examples/mixin/urls.py
+++ b/examples/mixin/urls.py
@@ -1,12 +1,13 @@
from djangorestframework.compat import View # Use Django 1.3's django.views.generic.View, or fall back to a clone of that if Django < 1.3
-from djangorestframework.emitters import EmitterMixin, DEFAULT_EMITTERS
+from djangorestframework.mixins import ResponseMixin
+from djangorestframework.emitters import DEFAULT_EMITTERS
from djangorestframework.response import Response
from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
-class ExampleView(EmitterMixin, View):
+class ExampleView(ResponseMixin, View):
"""An example view using Django 1.3's class based views.
Uses djangorestframework's EmitterMixin to provide support for multiple output formats."""
emitters = DEFAULT_EMITTERS
diff --git a/examples/modelresourceexample/views.py b/examples/modelresourceexample/views.py
index e912c019..07f50b65 100644
--- a/examples/modelresourceexample/views.py
+++ b/examples/modelresourceexample/views.py
@@ -7,12 +7,10 @@ class MyModelRootResource(RootModelResource):
"""A create/list resource for MyModel.
Available for both authenticated and anonymous access for the purposes of the sandbox."""
model = MyModel
- allowed_methods = anon_allowed_methods = ('GET', 'POST')
fields = FIELDS
class MyModelResource(ModelResource):
"""A read/update/delete resource for MyModel.
Available for both authenticated and anonymous access for the purposes of the sandbox."""
model = MyModel
- allowed_methods = anon_allowed_methods = ('GET', 'PUT', 'DELETE')
fields = FIELDS
diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py
index 6fb9217a..f1a89702 100644
--- a/examples/pygments_api/views.py
+++ b/examples/pygments_api/views.py
@@ -41,26 +41,25 @@ class PygmentsRoot(Resource):
"""This example demonstrates a simple RESTful Web API aound the awesome pygments library.
This top level resource is used to create highlighted code snippets, and to list all the existing code snippets."""
form = PygmentsForm
- allowed_methods = anon_allowed_methods = ('GET', 'POST',)
- def get(self, request, auth):
+ def get(self, request):
"""Return a list of all currently existing snippets."""
unique_ids = [os.path.split(f)[1] for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)]
return [reverse('pygments-instance', args=[unique_id]) for unique_id in unique_ids]
- def post(self, request, auth, content):
+ def post(self, request):
"""Create a new highlighed snippet and return it's location.
For the purposes of the sandbox example, also ensure we delete the oldest snippets if we have > MAX_FILES."""
unique_id = str(uuid.uuid1())
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
- lexer = get_lexer_by_name(content['lexer'])
- linenos = 'table' if content['linenos'] else False
- options = {'title': content['title']} if content['title'] else {}
- formatter = HtmlFormatter(style=content['style'], linenos=linenos, full=True, **options)
+ lexer = get_lexer_by_name(self.CONTENT['lexer'])
+ linenos = 'table' if self.CONTENT['linenos'] else False
+ options = {'title': self.CONTENT['title']} if self.CONTENT['title'] else {}
+ formatter = HtmlFormatter(style=self.CONTENT['style'], linenos=linenos, full=True, **options)
with open(pathname, 'w') as outfile:
- highlight(content['code'], lexer, formatter, outfile)
+ highlight(self.CONTENT['code'], lexer, formatter, outfile)
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
@@ -70,20 +69,19 @@ class PygmentsRoot(Resource):
class PygmentsInstance(Resource):
"""Simply return the stored highlighted HTML file with the correct mime type.
This Resource only emits HTML and uses a standard HTML emitter rather than the emitters.DocumentingHTMLEmitter class."""
- allowed_methods = anon_allowed_methods = ('GET',)
emitters = (HTMLEmitter,)
- def get(self, request, auth, unique_id):
+ def get(self, request, unique_id):
"""Return the highlighted snippet."""
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname):
- return Resource(status.HTTP_404_NOT_FOUND)
+ return Response(status.HTTP_404_NOT_FOUND)
return open(pathname, 'r').read()
- def delete(self, request, auth, unique_id):
+ def delete(self, request, unique_id):
"""Delete the highlighted snippet."""
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname):
- return Resource(status.HTTP_404_NOT_FOUND)
+ return Response(status.HTTP_404_NOT_FOUND)
return os.remove(pathname)
diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py
index 41d2e5c5..911fd467 100644
--- a/examples/resourceexample/views.py
+++ b/examples/resourceexample/views.py
@@ -8,24 +8,22 @@ from resourceexample.forms import MyForm
class ExampleResource(Resource):
"""A basic read-only resource that points to 3 other resources."""
- allowed_methods = anon_allowed_methods = ('GET',)
- def get(self, request, auth):
+ def get(self, request):
return {"Some other resources": [reverse('another-example-resource', kwargs={'num':num}) for num in range(3)]}
class AnotherExampleResource(Resource):
"""A basic GET-able/POST-able resource."""
- allowed_methods = anon_allowed_methods = ('GET', 'POST')
form = MyForm # Optional form validation on input (Applies in this case the POST method, but can also apply to PUT)
- def get(self, request, auth, num):
+ def get(self, request, num):
"""Handle GET requests"""
if int(num) > 2:
return Response(status.HTTP_404_NOT_FOUND)
return "GET request to AnotherExampleResource %s" % num
- def post(self, request, auth, content, num):
+ def post(self, request, num):
"""Handle POST requests"""
if int(num) > 2:
return Response(status.HTTP_404_NOT_FOUND)
- return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(content))
+ return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT))
diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py
index 561bdb1d..5b84e8e4 100644
--- a/examples/sandbox/views.py
+++ b/examples/sandbox/views.py
@@ -24,9 +24,8 @@ class Sandbox(Resource):
6. A blog posts and comments API.
Please feel free to browse, create, edit and delete the resources in these examples."""
- allowed_methods = anon_allowed_methods = ('GET',)
- def get(self, request, auth):
+ def get(self, request):
return [{'name': 'Simple Resource example', 'url': reverse('example-resource')},
{'name': 'Simple ModelResource example', 'url': reverse('my-model-root-resource')},
{'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')},
--
cgit v1.2.3
From 659898ffaf24f74b62e73c487cd81bad21904790 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 27 Apr 2011 18:08:32 +0100
Subject: Inital pass at generic permissions, throttling etc.
---
djangorestframework/permissions.py | 74 +++++++++++++++++++++++++++++++++
djangorestframework/tests/throttling.py | 38 +++++++++++++++++
2 files changed, 112 insertions(+)
create mode 100644 djangorestframework/permissions.py
create mode 100644 djangorestframework/tests/throttling.py
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
new file mode 100644
index 00000000..98d4b0be
--- /dev/null
+++ b/djangorestframework/permissions.py
@@ -0,0 +1,74 @@
+from django.core.cache import cache
+from djangorestframework import status
+import time
+
+
+class BasePermission(object):
+ """A base class from which all permission classes should inherit."""
+ def __init__(self, view):
+ self.view = view
+
+ def has_permission(self, auth):
+ return True
+
+class IsAuthenticated(BasePermission):
+ """"""
+ def has_permission(self, auth):
+ return auth is not None and auth.is_authenticated()
+
+#class IsUser(BasePermission):
+# """The request has authenticated as a user."""
+# def has_permission(self, auth):
+# pass
+#
+#class IsAdminUser():
+# """The request has authenticated as an admin user."""
+# def has_permission(self, auth):
+# pass
+#
+#class IsUserOrIsAnonReadOnly(BasePermission):
+# """The request has authenticated as a user, or is a read-only request."""
+# def has_permission(self, auth):
+# pass
+#
+#class OAuthTokenInScope(BasePermission):
+# def has_permission(self, auth):
+# pass
+#
+#class UserHasModelPermissions(BasePermission):
+# def has_permission(self, auth):
+# pass
+
+
+class Throttling(BasePermission):
+ """Rate throttling of requests on a per-user basis.
+
+ The rate is set by a 'throttle' attribute on the view class.
+ The attribute is a two tuple of the form (number of requests, duration in seconds).
+
+ The user's id will be used as a unique identifier if the user is authenticated.
+ For anonymous requests, the IP address of the client will be used.
+
+ Previous request information used for throttling is stored in the cache.
+ """
+ def has_permission(self, auth):
+ (num_requests, duration) = getattr(self.view, 'throttle', (0, 0))
+
+ if auth.is_authenticated():
+ ident = str(auth)
+ else:
+ ident = self.view.request.META.get('REMOTE_ADDR', None)
+
+ key = 'throttle_%s' % ident
+ history = cache.get(key, [])
+ now = time.time()
+
+ # Drop any requests from the history which have now passed the throttle duration
+ while history and history[0] < now - duration:
+ history.pop()
+
+ if len(history) >= num_requests:
+ raise ErrorResponse(status.HTTP_503_SERVICE_UNAVAILABLE, {'detail': 'request was throttled'})
+
+ history.insert(0, now)
+ cache.set(key, history, duration)
diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py
new file mode 100644
index 00000000..46383271
--- /dev/null
+++ b/djangorestframework/tests/throttling.py
@@ -0,0 +1,38 @@
+from django.conf.urls.defaults import patterns
+from django.test import TestCase
+from django.utils import simplejson as json
+
+from djangorestframework.compat import RequestFactory
+from djangorestframework.resource import Resource
+from djangorestframework.permissions import Throttling
+
+
+class MockResource(Resource):
+ permissions = ( Throttling, )
+ throttle = (3, 1) # 3 requests per second
+
+ def get(self, request):
+ return 'foo'
+
+urlpatterns = patterns('',
+ (r'^$', MockResource.as_view()),
+)
+
+
+#class ThrottlingTests(TestCase):
+# """Basic authentication"""
+# urls = 'djangorestframework.tests.throttling'
+#
+# def test_requests_are_throttled(self):
+# """Ensure request rate is limited"""
+# for dummy in range(3):
+# response = self.client.get('/')
+# response = self.client.get('/')
+#
+# def test_request_throttling_is_per_user(self):
+# """Ensure request rate is only limited per user, not globally"""
+# pass
+#
+# def test_request_throttling_expires(self):
+# """Ensure request rate is limited for a limited duration only"""
+# pass
--
cgit v1.2.3
From b18302586c0eeea2e09c799544b9c0a855e11755 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 27 Apr 2011 18:36:43 +0100
Subject: Urg. Fix broken merging.
---
djangorestframework/emitters.py | 142 +---------------------------
djangorestframework/parsers.py | 48 ----------
djangorestframework/tests/authentication.py | 6 +-
djangorestframework/tests/reverse.py | 4 -
4 files changed, 2 insertions(+), 198 deletions(-)
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index 0adddca9..2702758b 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -7,149 +7,9 @@ from django import forms
from django.conf import settings
from django.template import RequestContext, loader
from django.utils import simplejson as json
-<<<<<<< local
from django import forms
-=======
->>>>>>> other
-
-from djangorestframework.response import ErrorResponse
-from djangorestframework.utils import dict2xml, url_resolves
-from djangorestframework.markdownwrapper import apply_markdown
-from djangorestframework.breadcrumbs import get_breadcrumbs
-from djangorestframework.description import get_name, get_description
-from djangorestframework import status
-
-from urllib import quote_plus
-import string
-import re
-<<<<<<< local
-=======
-from decimal import Decimal
-
-
-_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
-
-
-class EmitterMixin(object):
- """Adds behaviour for pluggable Emitters to a :class:`.Resource` or Django :class:`View`. class.
-
- 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.
- 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
- REWRITE_IE_ACCEPT_HEADER = True
-
- request = None
- response = None
- emitters = ()
-
- def emit(self, response):
- """Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
- self.response = response
-
- try:
- emitter = self._determine_emitter(self.request)
- except ResponseException, exc:
- emitter = self.default_emitter
- response = exc.response
-
- # Serialize the response content
- if response.has_content_body:
- content = emitter(self).emit(output=response.cleaned_content)
- else:
- content = emitter(self).emit()
-
- # Munge DELETE Response code to allow us to return content
- # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
- if response.status == 204:
- response.status = 200
-
- # Build the HTTP Response
- # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
- resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
- for (key, val) in response.headers.items():
- resp[key] = val
-
- return resp
-
-
- def _determine_emitter(self, request):
- """Return the appropriate emitter for the output, given the client's 'Accept' header,
- and the content types that this Resource knows how to serve.
-
- See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
-
- if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
- # Use _accept parameter override
- accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
- elif self.REWRITE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']):
- accept_list = ['text/html', '*/*']
- elif request.META.has_key('HTTP_ACCEPT'):
- # Use standard HTTP Accept negotiation
- accept_list = request.META["HTTP_ACCEPT"].split(',')
- else:
- # No accept header specified
- return self.default_emitter
-
- # Parse the accept header into a dict of {qvalue: set of media types}
- # We ignore mietype parameters
- accept_dict = {}
- for token in accept_list:
- components = token.split(';')
- mimetype = components[0].strip()
- qvalue = Decimal('1.0')
-
- if len(components) > 1:
- # Parse items that have a qvalue eg text/html;q=0.9
- try:
- (q, num) = components[-1].split('=')
- if q == 'q':
- qvalue = Decimal(num)
- except:
- # Skip malformed entries
- continue
-
- if accept_dict.has_key(qvalue):
- accept_dict[qvalue].add(mimetype)
- else:
- accept_dict[qvalue] = set((mimetype,))
-
- # Convert to a list of sets ordered by qvalue (highest first)
- accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
-
- for accept_set in accept_sets:
- # Return any exact match
- for emitter in self.emitters:
- if emitter.media_type in accept_set:
- return emitter
-
- # Return any subtype match
- for emitter in self.emitters:
- if emitter.media_type.split('/')[0] + '/*' in accept_set:
- return emitter
-
- # Return default
- if '*/*' in accept_set:
- return self.default_emitter
-
-
- raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
- {'detail': 'Could not statisfy the client\'s Accept header',
- 'available_types': self.emitted_media_types})
-
- @property
- def emitted_media_types(self):
- """Return an list of all the media types that this resource can emit."""
- return [emitter.media_type for emitter in self.emitters]
-
- @property
- def default_emitter(self):
- """Return the resource's most prefered emitter.
- (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
- return self.emitters[0]
->>>>>>> other
+from decimal import Decimal
# TODO: Rename verbose to something more appropriate
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 03f8bf8f..96b29a66 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -11,60 +11,12 @@ We need a method to be able to:
from django.http.multipartparser import MultiPartParser as DjangoMPParser
from django.utils import simplejson as json
-<<<<<<< local
from djangorestframework.response import ErrorResponse
-=======
-from djangorestframework.response import ResponseException
->>>>>>> other
from djangorestframework import status
from djangorestframework.utils import as_tuple
from djangorestframework.mediatypes import MediaType
from djangorestframework.compat import parse_qs
-<<<<<<< local
-=======
-try:
- from urlparse import parse_qs
-except ImportError:
- from cgi import parse_qs
-
-class ParserMixin(object):
- parsers = ()
-
- def parse(self, stream, content_type):
- """
- Parse the request content.
-
- May raise a 415 ResponseException (Unsupported Media Type),
- or a 400 ResponseException (Bad Request).
- """
- parsers = as_tuple(self.parsers)
-
- parser = None
- for parser_cls in parsers:
- if parser_cls.handles(content_type):
- parser = parser_cls(self)
- break
-
- if parser is None:
- raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
- {'error': 'Unsupported media type in request \'%s\'.' %
- content_type.media_type})
-
- return parser.parse(stream)
-
- @property
- def parsed_media_types(self):
- """Return an list of all the media types that this ParserMixin can parse."""
- return [parser.media_type for parser in self.parsers]
-
- @property
- def default_parser(self):
- """Return the ParerMixin's most prefered emitter.
- (This has no behavioural effect, but is may be used by documenting emitters)"""
- return self.parsers[0]
->>>>>>> other
-
class BaseParser(object):
"""All parsers should extend BaseParser, specifying a media_type attribute,
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index a43a87b3..248bd87a 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -1,12 +1,8 @@
from django.conf.urls.defaults import patterns
-<<<<<<< local
-from django.test import TestCase
-from django.test import Client
from django.contrib.auth.models import User
from django.contrib.auth import login
-=======
from django.test import Client, TestCase
->>>>>>> other
+
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py
index 63e2080a..49939d0e 100644
--- a/djangorestframework/tests/reverse.py
+++ b/djangorestframework/tests/reverse.py
@@ -5,10 +5,6 @@ from django.utils import simplejson as json
from djangorestframework.resource import Resource
-<<<<<<< local
-
-=======
->>>>>>> other
class MockResource(Resource):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
--
cgit v1.2.3
From 5a59f339c1757767b136de33faa5b67a972141a1 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 27 Apr 2011 18:44:21 +0100
Subject: Urg. Fixing broken merge
---
djangorestframework/authenticators.py | 2 +-
djangorestframework/emitters.py | 8 ++-
djangorestframework/resource.py | 98 ++++++++++++++++++-----------------
examples/blogpost/tests.py | 6 ---
examples/pygments_api/tests.py | 6 ---
5 files changed, 59 insertions(+), 61 deletions(-)
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
index 29fbb818..74d9931a 100644
--- a/djangorestframework/authenticators.py
+++ b/djangorestframework/authenticators.py
@@ -68,7 +68,7 @@ class UserLoggedInAuthenticator(BaseAuthenticator):
if request.method.upper() == 'POST':
# Temporarily replace request.POST with .RAW_CONTENT,
# so that we use our more generic request parsing
- request._post = self.mixin.RAW_CONTENT
+ request._post = self.view.RAW_CONTENT
resp = CsrfViewMiddleware().process_view(request, None, (), {})
del(request._post)
if resp is not None: # csrf failed
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index 2702758b..d2b06c13 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -9,8 +9,14 @@ from django.template import RequestContext, loader
from django.utils import simplejson as json
from django import forms
-from decimal import Decimal
+from djangorestframework.utils import dict2xml, url_resolves
+from djangorestframework.markdownwrapper import apply_markdown
+from djangorestframework.breadcrumbs import get_breadcrumbs
+from djangorestframework.description import get_name, get_description
+from djangorestframework import status
+from decimal import Decimal
+import string
# TODO: Rename verbose to something more appropriate
# TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index fbf51cfc..fdbce8b5 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -77,55 +77,59 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
# all other authentication is CSRF exempt.
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
- self.request = request
- self.args = args
- self.kwargs = kwargs
-
- # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
- prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
- set_script_prefix(prefix)
-
try:
- # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
- # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
- self.perform_form_overloading()
-
- # Authenticate and check request is has the relevant permissions
- self.check_permissions()
-
- # Get the appropriate handler method
- if self.method.lower() in self.http_method_names:
- handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
- # If a previously defined method has been disabled
- if handler is None:
+ self.request = request
+ self.args = args
+ self.kwargs = kwargs
+
+ # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
+ prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
+ set_script_prefix(prefix)
+
+ try:
+ # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
+ # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
+ self.perform_form_overloading()
+
+ # Authenticate and check request is has the relevant permissions
+ self.check_permissions()
+
+ # Get the appropriate handler method
+ if self.method.lower() in self.http_method_names:
+ handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
+ # If a previously defined method has been disabled
+ if handler is None:
+ handler = self.http_method_not_allowed
+ else:
handler = self.http_method_not_allowed
- else:
- handler = self.http_method_not_allowed
-
- response_obj = handler(request, *args, **kwargs)
-
- # Allow return value to be either Response, or an object, or None
- if isinstance(response_obj, Response):
- response = response_obj
- elif response_obj is not None:
- response = Response(status.HTTP_200_OK, response_obj)
- else:
- response = Response(status.HTTP_204_NO_CONTENT)
-
- # Pre-serialize filtering (eg filter complex objects into natively serializable types)
- response.cleaned_content = self.cleanup_response(response.raw_content)
-
- except ErrorResponse, exc:
- response = exc.response
-
- # Always add these headers.
- #
- # TODO - this isn't actually the correct way to set the vary header,
- # also it's currently sub-obtimal for HTTP caching - need to sort that out.
- response.headers['Allow'] = ', '.join(self.allowed_methods)
- response.headers['Vary'] = 'Authenticate, Accept'
-
- return self.emit(response)
+
+ response_obj = handler(request, *args, **kwargs)
+
+ # Allow return value to be either Response, or an object, or None
+ if isinstance(response_obj, Response):
+ response = response_obj
+ elif response_obj is not None:
+ response = Response(status.HTTP_200_OK, response_obj)
+ else:
+ response = Response(status.HTTP_204_NO_CONTENT)
+
+ # Pre-serialize filtering (eg filter complex objects into natively serializable types)
+ response.cleaned_content = self.cleanup_response(response.raw_content)
+
+ except ErrorResponse, exc:
+ response = exc.response
+
+ # Always add these headers.
+ #
+ # TODO - this isn't actually the correct way to set the vary header,
+ # also it's currently sub-obtimal for HTTP caching - need to sort that out.
+ response.headers['Allow'] = ', '.join(self.allowed_methods)
+ response.headers['Vary'] = 'Authenticate, Accept'
+
+ return self.emit(response)
+ except:
+ import traceback
+ traceback.print_exc()
diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py
index 494478d8..9b9a682f 100644
--- a/examples/blogpost/tests.py
+++ b/examples/blogpost/tests.py
@@ -3,10 +3,7 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
-<<<<<<< local
-=======
from django.core.urlresolvers import reverse
->>>>>>> other
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
@@ -170,10 +167,7 @@ class AllowedMethodsTests(TestCase):
#above testcases need to probably moved to the core
-<<<<<<< local
-=======
->>>>>>> other
class TestRotation(TestCase):
"""For the example the maximum amount of Blogposts is capped off at views.MAX_POSTS.
diff --git a/examples/pygments_api/tests.py b/examples/pygments_api/tests.py
index 766defc3..6eb69da5 100644
--- a/examples/pygments_api/tests.py
+++ b/examples/pygments_api/tests.py
@@ -1,18 +1,12 @@
from django.test import TestCase
from django.utils import simplejson as json
-<<<<<<< local
-=======
->>>>>>> other
from djangorestframework.compat import RequestFactory
from pygments_api import views
import tempfile, shutil
-<<<<<<< local
-=======
->>>>>>> other
class TestPygmentsExample(TestCase):
--
cgit v1.2.3
From 5921e5c84e13cafe90061629262f12dfe742c07a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 27 Apr 2011 18:53:54 +0100
Subject: Fix up ModelResource issues
---
djangorestframework/emitters.py | 4 +++-
djangorestframework/modelresource.py | 7 +++++--
djangorestframework/resource.py | 5 +----
examples/blogpost/views.py | 6 +++---
4 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index d2b06c13..87b3e94e 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -15,8 +15,10 @@ from djangorestframework.breadcrumbs import get_breadcrumbs
from djangorestframework.description import get_name, get_description
from djangorestframework import status
-from decimal import Decimal
+from urllib import quote_plus
import string
+import re
+from decimal import Decimal
# TODO: Rename verbose to something more appropriate
# TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index 1afd7fa0..79505c6c 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -408,6 +408,9 @@ class ModelResource(Resource):
return
+class InstanceModelResource(ModelResource):
+ http_method_names = ['get', 'put', 'delete', 'head', 'options', 'trace', 'patch'] # Bit of a hack, these - needs fixing.
+
class RootModelResource(ModelResource):
"""A Resource which provides default operations for list and create."""
queryset = None
@@ -416,7 +419,7 @@ class RootModelResource(ModelResource):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filter(**kwargs)
- put = delete = None
+ http_method_names = ['get', 'post', 'head', 'options', 'trace', 'patch']
class QueryModelResource(ModelResource):
"""Resource with default operations for list.
@@ -428,4 +431,4 @@ class QueryModelResource(ModelResource):
queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filer(**kwargs)
- post = put = delete = None
\ No newline at end of file
+ http_method_names = ['get', 'head', 'options', 'trace', 'patch']
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index fdbce8b5..636fe0ba 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -54,7 +54,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
@property
def allowed_methods(self):
- return [method.upper() for method in self.http_method_names if getattr(self, method, None)]
+ return [method.upper() for method in self.http_method_names if hasattr(self, method)]
def http_method_not_allowed(self, request, *args, **kwargs):
"""Return an HTTP 405 error if an operation is called which does not have a handler method."""
@@ -97,9 +97,6 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
# Get the appropriate handler method
if self.method.lower() in self.http_method_names:
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
- # If a previously defined method has been disabled
- if handler is None:
- handler = self.http_method_not_allowed
else:
handler = self.http_method_not_allowed
diff --git a/examples/blogpost/views.py b/examples/blogpost/views.py
index 9e07aa8a..e47f4a5b 100644
--- a/examples/blogpost/views.py
+++ b/examples/blogpost/views.py
@@ -1,4 +1,4 @@
-from djangorestframework.modelresource import ModelResource, RootModelResource
+from djangorestframework.modelresource import InstanceModelResource, RootModelResource
from blogpost import models
@@ -11,7 +11,7 @@ class BlogPosts(RootModelResource):
model = models.BlogPost
fields = BLOG_POST_FIELDS
-class BlogPostInstance(ModelResource):
+class BlogPostInstance(InstanceModelResource):
"""A resource which represents a single blog post."""
model = models.BlogPost
fields = BLOG_POST_FIELDS
@@ -21,7 +21,7 @@ class Comments(RootModelResource):
model = models.Comment
fields = COMMENT_FIELDS
-class CommentInstance(ModelResource):
+class CommentInstance(InstanceModelResource):
"""A resource which represents a single comment."""
model = models.Comment
fields = COMMENT_FIELDS
--
cgit v1.2.3
From 032cb7959265d7fe631dd3e0a6ff84da88dc71e5 Mon Sep 17 00:00:00 2001
From: markotibold
Date: Wed, 27 Apr 2011 22:28:08 +0200
Subject: fix tiny spelling errors
---
docs/examples/pygments.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/examples/pygments.rst b/docs/examples/pygments.rst
index decc2f65..b4bc2c16 100644
--- a/docs/examples/pygments.rst
+++ b/docs/examples/pygments.rst
@@ -31,12 +31,12 @@ We'll need two resources:
Form validation
---------------
-We'll now add a form to specify what input fields are required when creating a new highlighed code snippet. This will include:
+We'll now add a form to specify what input fields are required when creating a new highlighted code snippet. This will include:
* The code text itself.
* An optional title for the code.
* A flag to determine if line numbers should be included.
-* Which programming langauge to interpret the code snippet as.
+* Which programming language to interpret the code snippet as.
* Which output style to use for the highlighting.
``forms.py``
--
cgit v1.2.3
From 93aa065fa92f64472a3ee80564020a81776be742 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 28 Apr 2011 19:54:30 +0100
Subject: emitters -> renderers
---
djangorestframework/mixins.py | 58 ++++++++++++++++----------------
djangorestframework/modelresource.py | 6 ++--
djangorestframework/resource.py | 22 ++++++------
djangorestframework/tests/breadcrumbs.py | 2 +-
djangorestframework/utils.py | 4 +--
examples/mixin/urls.py | 6 ++--
examples/pygments_api/views.py | 10 +++---
examples/sandbox/views.py | 6 ++--
8 files changed, 57 insertions(+), 57 deletions(-)
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 53262366..43b33f50 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -233,8 +233,8 @@ class RequestMixin(object):
@property
def default_parser(self):
- """Return the view's most preffered emitter.
- (This has no behavioural effect, but is may be used by documenting emitters)"""
+ """Return the view's most preffered renderer.
+ (This has no behavioural effect, but is may be used by documenting renderers)"""
return self.parsers[0]
@@ -249,7 +249,7 @@ class RequestMixin(object):
########## ResponseMixin ##########
class ResponseMixin(object):
- """Adds behaviour for pluggable Emitters to a :class:`.Resource` or Django :class:`View`. class.
+ """Adds behaviour for pluggable Renderers to a :class:`.Resource` or Django :class:`View`. class.
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.
@@ -260,7 +260,7 @@ class ResponseMixin(object):
#request = None
#response = None
- emitters = ()
+ renderers = ()
#def render_to_response(self, obj):
# if isinstance(obj, Response):
@@ -285,21 +285,21 @@ class ResponseMixin(object):
# return content
- def emit(self, response):
+ def render(self, response):
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
self.response = response
try:
- emitter = self._determine_emitter(self.request)
+ renderer = self._determine_renderer(self.request)
except ErrorResponse, exc:
- emitter = self.default_emitter
+ renderer = self.default_renderer
response = exc.response
# Serialize the response content
if response.has_content_body:
- content = emitter(self).emit(output=response.cleaned_content)
+ content = renderer(self).render(output=response.cleaned_content)
else:
- content = emitter(self).emit()
+ content = renderer(self).render()
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
@@ -307,16 +307,16 @@ class ResponseMixin(object):
response.status = 200
# Build the HTTP Response
- # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
- resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
+ # TODO: Check if renderer.mimetype is underspecified, or if a content-type header has been set
+ resp = HttpResponse(content, mimetype=renderer.media_type, status=response.status)
for (key, val) in response.headers.items():
resp[key] = val
return resp
- def _determine_emitter(self, request):
- """Return the appropriate emitter for the output, given the client's 'Accept' header,
+ def _determine_renderer(self, request):
+ """Return the appropriate renderer for the output, given the client's 'Accept' header,
and the content types that this Resource knows how to serve.
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
@@ -333,7 +333,7 @@ class ResponseMixin(object):
accept_list = request.META["HTTP_ACCEPT"].split(',')
else:
# No accept header specified
- return self.default_emitter
+ return self.default_renderer
# Parse the accept header into a dict of {qvalue: set of media types}
# We ignore mietype parameters
@@ -363,34 +363,34 @@ class ResponseMixin(object):
for accept_set in accept_sets:
# Return any exact match
- for emitter in self.emitters:
- if emitter.media_type in accept_set:
- return emitter
+ for renderer in self.renderers:
+ if renderer.media_type in accept_set:
+ return renderer
# Return any subtype match
- for emitter in self.emitters:
- if emitter.media_type.split('/')[0] + '/*' in accept_set:
- return emitter
+ for renderer in self.renderers:
+ if renderer.media_type.split('/')[0] + '/*' in accept_set:
+ return renderer
# Return default
if '*/*' in accept_set:
- return self.default_emitter
+ return self.default_renderer
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
{'detail': 'Could not satisfy the client\'s Accept header',
- 'available_types': self.emitted_media_types})
+ 'available_types': self.renderted_media_types})
@property
- def emitted_media_types(self):
- """Return an list of all the media types that this resource can emit."""
- return [emitter.media_type for emitter in self.emitters]
+ def renderted_media_types(self):
+ """Return an list of all the media types that this resource can render."""
+ return [renderer.media_type for renderer in self.renderers]
@property
- def default_emitter(self):
- """Return the resource's most prefered emitter.
- (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
- return self.emitters[0]
+ def default_renderer(self):
+ """Return the resource's most prefered renderer.
+ (This renderer is used if the client does not send and Accept: header, or sends Accept: */*)"""
+ return self.renderers[0]
########## Auth Mixin ##########
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index 79505c6c..c61cc375 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -49,7 +49,7 @@ class ModelResource(Resource):
#def get_form(self, content=None):
- # """Return a form that may be used in validation and/or rendering an html emitter"""
+ # """Return a form that may be used in validation and/or rendering an html renderer"""
# if self.form:
# return super(self.__class__, self).get_form(content)
#
@@ -121,8 +121,8 @@ class ModelResource(Resource):
elif inspect.isfunction(thing):
if not inspect.getargspec(thing)[0]:
ret = _any(thing())
- elif hasattr(thing, '__emittable__'):
- f = thing.__emittable__
+ elif hasattr(thing, '__rendertable__'):
+ f = thing.__rendertable__
if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
ret = _any(f())
else:
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 636fe0ba..cb4d080c 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -4,13 +4,13 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
-from djangorestframework import emitters, parsers, authenticators, permissions, validators, status
+from djangorestframework import renderers, parsers, authenticators, permissions, validators, status
# TODO: Figure how out references and named urls need to work nicely
# TODO: POST on existing 404 URL, PUT on existing 404 URL
#
-# NEXT: Exceptions on func() -> 500, tracebacks emitted if settings.DEBUG
+# NEXT: Exceptions on func() -> 500, tracebacks renderted if settings.DEBUG
__all__ = ['Resource']
@@ -21,12 +21,12 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace', 'patch']
- # List of emitters the resource can serialize the response with, ordered by preference.
- emitters = ( emitters.JSONEmitter,
- emitters.DocumentingHTMLEmitter,
- emitters.DocumentingXHTMLEmitter,
- emitters.DocumentingPlainTextEmitter,
- emitters.XMLEmitter )
+ # List of renderers the resource can serialize the response with, ordered by preference.
+ renderers = ( renderers.JSONRenderer,
+ renderers.DocumentingHTMLRenderer,
+ renderers.DocumentingXHTMLRenderer,
+ renderers.DocumentingPlainTextRenderer,
+ renderers.XMLRenderer )
# List of parsers the resource can parse the request with.
parsers = ( parsers.JSONParser,
@@ -48,7 +48,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
# Allow name and description for the Resource to be set explicitly,
# overiding the default classname/docstring behaviour.
- # These are used for documentation in the standard html and text emitters.
+ # These are used for documentation in the standard html and text renderers.
name = None
description = None
@@ -69,7 +69,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into
- the EmitterMixin and Emitter classes."""
+ the RendererMixin and Renderer classes."""
return data
@@ -123,7 +123,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
- return self.emit(response)
+ return self.render(response)
except:
import traceback
traceback.print_exc()
diff --git a/djangorestframework/tests/breadcrumbs.py b/djangorestframework/tests/breadcrumbs.py
index cc0d283d..724f2ff5 100644
--- a/djangorestframework/tests/breadcrumbs.py
+++ b/djangorestframework/tests/breadcrumbs.py
@@ -28,7 +28,7 @@ urlpatterns = patterns('',
class BreadcrumbTests(TestCase):
- """Tests the breadcrumb functionality used by the HTML emitter."""
+ """Tests the breadcrumb functionality used by the HTML renderer."""
urls = 'djangorestframework.tests.breadcrumbs'
diff --git a/djangorestframework/utils.py b/djangorestframework/utils.py
index 8b12294c..f60bdee4 100644
--- a/djangorestframework/utils.py
+++ b/djangorestframework/utils.py
@@ -125,7 +125,7 @@ def xml2dict(input):
# Piston:
-class XMLEmitter():
+class XMLRenderer():
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
@@ -156,4 +156,4 @@ class XMLEmitter():
return stream.getvalue()
def dict2xml(input):
- return XMLEmitter().dict2xml(input)
+ return XMLRenderer().dict2xml(input)
diff --git a/examples/mixin/urls.py b/examples/mixin/urls.py
index 96b630e3..f4300f41 100644
--- a/examples/mixin/urls.py
+++ b/examples/mixin/urls.py
@@ -1,6 +1,6 @@
from djangorestframework.compat import View # Use Django 1.3's django.views.generic.View, or fall back to a clone of that if Django < 1.3
from djangorestframework.mixins import ResponseMixin
-from djangorestframework.emitters import DEFAULT_EMITTERS
+from djangorestframework.renderers import DEFAULT_RENDERERS
from djangorestframework.response import Response
from django.conf.urls.defaults import patterns, url
@@ -9,8 +9,8 @@ from django.core.urlresolvers import reverse
class ExampleView(ResponseMixin, View):
"""An example view using Django 1.3's class based views.
- Uses djangorestframework's EmitterMixin to provide support for multiple output formats."""
- emitters = DEFAULT_EMITTERS
+ Uses djangorestframework's RendererMixin to provide support for multiple output formats."""
+ renderers = DEFAULT_RENDERERS
def get(self, request):
response = Response(200, {'description': 'Some example content',
diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py
index 4e6d1230..278e8250 100644
--- a/examples/pygments_api/views.py
+++ b/examples/pygments_api/views.py
@@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse
from djangorestframework.resource import Resource
from djangorestframework.response import Response
-from djangorestframework.emitters import BaseEmitter
+from djangorestframework.renderers import BaseRenderer
from djangorestframework import status
from pygments.formatters import HtmlFormatter
@@ -32,8 +32,8 @@ def remove_oldest_files(dir, max_files):
[os.remove(path) for path in list_dir_sorted_by_ctime(dir)[max_files:]]
-class HTMLEmitter(BaseEmitter):
- """Basic emitter which just returns the content without any further serialization."""
+class HTMLRenderer(BaseRenderer):
+ """Basic renderer which just returns the content without any further serialization."""
media_type = 'text/html'
@@ -68,8 +68,8 @@ class PygmentsRoot(Resource):
class PygmentsInstance(Resource):
"""Simply return the stored highlighted HTML file with the correct mime type.
- This Resource only emits HTML and uses a standard HTML emitter rather than the emitters.DocumentingHTMLEmitter class."""
- emitters = (HTMLEmitter,)
+ This Resource only emits HTML and uses a standard HTML renderer rather than the renderers.DocumentingHTMLRenderer class."""
+ renderers = (HTMLRenderer,)
def get(self, request, unique_id):
"""Return the highlighted snippet."""
diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py
index 5b84e8e4..04e4da41 100644
--- a/examples/sandbox/views.py
+++ b/examples/sandbox/views.py
@@ -11,14 +11,14 @@ class Sandbox(Resource):
All the example APIs allow anonymous access, and can be navigated either through the browser or from the command line...
- bash: curl -X GET http://api.django-rest-framework.org/ # (Use default emitter)
- bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation emitter)
+ bash: curl -X GET http://api.django-rest-framework.org/ # (Use default renderer)
+ bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation renderer)
The examples provided:
1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class.
2. A basic example using the [ModelResource](http://django-rest-framework.org/library/modelresource.html) class.
- 3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [EmitterMixin](http://django-rest-framework.org/library/emitters.html).
+ 3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [RendererMixin](http://django-rest-framework.org/library/renderers.html).
4. A generic object store API.
5. A code highlighting API.
6. A blog posts and comments API.
--
cgit v1.2.3
From b358fbdbe9cbd4ce644c4b2c7b9b4cec0811e14e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Apr 2011 14:32:56 +0100
Subject: More refactoring - move various less core stuff into utils etc
---
djangorestframework/authentication.py | 84 ++++++++++
djangorestframework/authenticators.py | 84 ----------
djangorestframework/breadcrumbs.py | 31 ----
djangorestframework/compat.py | 66 +++++++-
djangorestframework/description.py | 37 -----
djangorestframework/emitters.py | 243 ----------------------------
djangorestframework/markdownwrapper.py | 51 ------
djangorestframework/mediatypes.py | 78 ---------
djangorestframework/mixins.py | 10 +-
djangorestframework/parsers.py | 2 +-
djangorestframework/renderers.py | 243 ++++++++++++++++++++++++++++
djangorestframework/resource.py | 6 +-
djangorestframework/templates/emitter.html | 127 ---------------
djangorestframework/templates/emitter.txt | 8 -
djangorestframework/templates/renderer.html | 127 +++++++++++++++
djangorestframework/templates/renderer.txt | 8 +
djangorestframework/tests/breadcrumbs.py | 2 +-
djangorestframework/tests/description.py | 4 +-
djangorestframework/tests/emitters.py | 76 ---------
djangorestframework/tests/parsers.py | 2 +-
djangorestframework/tests/renderers.py | 76 +++++++++
djangorestframework/utils.py | 159 ------------------
djangorestframework/utils/__init__.py | 158 ++++++++++++++++++
djangorestframework/utils/breadcrumbs.py | 30 ++++
djangorestframework/utils/description.py | 37 +++++
djangorestframework/utils/mediatypes.py | 78 +++++++++
26 files changed, 913 insertions(+), 914 deletions(-)
create mode 100644 djangorestframework/authentication.py
delete mode 100644 djangorestframework/authenticators.py
delete mode 100644 djangorestframework/breadcrumbs.py
delete mode 100644 djangorestframework/description.py
delete mode 100644 djangorestframework/emitters.py
delete mode 100644 djangorestframework/markdownwrapper.py
delete mode 100644 djangorestframework/mediatypes.py
create mode 100644 djangorestframework/renderers.py
delete mode 100644 djangorestframework/templates/emitter.html
delete mode 100644 djangorestframework/templates/emitter.txt
create mode 100644 djangorestframework/templates/renderer.html
create mode 100644 djangorestframework/templates/renderer.txt
delete mode 100644 djangorestframework/tests/emitters.py
create mode 100644 djangorestframework/tests/renderers.py
delete mode 100644 djangorestframework/utils.py
create mode 100644 djangorestframework/utils/__init__.py
create mode 100644 djangorestframework/utils/breadcrumbs.py
create mode 100644 djangorestframework/utils/description.py
create mode 100644 djangorestframework/utils/mediatypes.py
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py
new file mode 100644
index 00000000..894b34fc
--- /dev/null
+++ b/djangorestframework/authentication.py
@@ -0,0 +1,84 @@
+"""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.
+
+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.
+"""
+from django.contrib.auth import authenticate
+from django.middleware.csrf import CsrfViewMiddleware
+from djangorestframework.utils import as_tuple
+import base64
+
+
+class BaseAuthenticator(object):
+ """All authentication should extend BaseAuthenticator."""
+
+ def __init__(self, view):
+ """Initialise the authentication with the mixin instance as state,
+ in case the authentication needs to access any metadata on the mixin object."""
+ self.view = view
+
+ def authenticate(self, request):
+ """Authenticate the request and return the authentication context or None.
+
+ An authentication context might be something as simple as a User object, or it might
+ 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.
+
+ The default permission checking on Resource will use the allowed_methods attribute
+ 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)
+ 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.
+
+ This function must be overridden to be implemented."""
+ return None
+
+
+class BasicAuthenticator(BaseAuthenticator):
+ """Use HTTP Basic authentication"""
+ def authenticate(self, request):
+ from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
+
+ if 'HTTP_AUTHORIZATION' in request.META:
+ auth = request.META['HTTP_AUTHORIZATION'].split()
+ if len(auth) == 2 and auth[0].lower() == "basic":
+ try:
+ 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:
+ return None
+
+ user = authenticate(username=uname, password=passwd)
+ if user is not None and user.is_active:
+ return user
+ return None
+
+
+class UserLoggedInAuthenticator(BaseAuthenticator):
+ """Use Django's built-in request session for authentication."""
+ def authenticate(self, request):
+ 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':
+ # Temporarily replace request.POST with .RAW_CONTENT,
+ # so that we use our more generic request parsing
+ request._post = self.view.RAW_CONTENT
+ resp = CsrfViewMiddleware().process_view(request, None, (), {})
+ del(request._post)
+ if resp is not None: # csrf failed
+ return None
+ return request.user
+ return None
+
+
+#class DigestAuthentication(BaseAuthentication):
+# pass
+#
+#class OAuthAuthentication(BaseAuthentication):
+# pass
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
deleted file mode 100644
index 74d9931a..00000000
--- a/djangorestframework/authenticators.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""The :mod:`authenticators` 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.
-
-The set of authenticators which are use is then specified by setting the :attr:`authenticators` attribute on the class, and listing a set of authenticator classes.
-"""
-from django.contrib.auth import authenticate
-from django.middleware.csrf import CsrfViewMiddleware
-from djangorestframework.utils import as_tuple
-import base64
-
-
-class BaseAuthenticator(object):
- """All authenticators should extend BaseAuthenticator."""
-
- def __init__(self, view):
- """Initialise the authenticator with the mixin instance as state,
- in case the authenticator needs to access any metadata on the mixin object."""
- self.view = view
-
- def authenticate(self, request):
- """Authenticate the request and return the authentication context or None.
-
- An authentication context might be something as simple as a User object, or it might
- 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.
-
- The default permission checking on Resource will use the allowed_methods attribute
- 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)
- 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.
-
- This function must be overridden to be implemented."""
- return None
-
-
-class BasicAuthenticator(BaseAuthenticator):
- """Use HTTP Basic authentication"""
- def authenticate(self, request):
- from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
-
- if 'HTTP_AUTHORIZATION' in request.META:
- auth = request.META['HTTP_AUTHORIZATION'].split()
- if len(auth) == 2 and auth[0].lower() == "basic":
- try:
- 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:
- return None
-
- user = authenticate(username=uname, password=passwd)
- if user is not None and user.is_active:
- return user
- return None
-
-
-class UserLoggedInAuthenticator(BaseAuthenticator):
- """Use Django's built-in request session for authentication."""
- def authenticate(self, request):
- 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':
- # Temporarily replace request.POST with .RAW_CONTENT,
- # so that we use our more generic request parsing
- request._post = self.view.RAW_CONTENT
- resp = CsrfViewMiddleware().process_view(request, None, (), {})
- del(request._post)
- if resp is not None: # csrf failed
- return None
- return request.user
- return None
-
-
-#class DigestAuthentication(BaseAuthentication):
-# pass
-#
-#class OAuthAuthentication(BaseAuthentication):
-# pass
diff --git a/djangorestframework/breadcrumbs.py b/djangorestframework/breadcrumbs.py
deleted file mode 100644
index ba779dd0..00000000
--- a/djangorestframework/breadcrumbs.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.core.urlresolvers import resolve
-from djangorestframework.description import get_name
-
-def get_breadcrumbs(url):
- """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
-
- def breadcrumbs_recursive(url, breadcrumbs_list):
- """Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
-
- # This is just like compsci 101 all over again...
- try:
- (view, unused_args, unused_kwargs) = resolve(url)
- except:
- pass
- else:
- if callable(view):
- breadcrumbs_list.insert(0, (get_name(view), url))
-
- if url == '':
- # All done
- return breadcrumbs_list
-
- elif url.endswith('/'):
- # Drop trailing slash off the end and continue to try to resolve more breadcrumbs
- return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
-
- # Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
- return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
-
- return breadcrumbs_recursive(url, [])
-
diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py
index 22b57186..98fbbb62 100644
--- a/djangorestframework/compat.py
+++ b/djangorestframework/compat.py
@@ -1,9 +1,24 @@
"""Compatability module to provide support for backwards compatability with older versions of django/python"""
+# cStringIO only if it's available
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+
+# parse_qs
+try:
+ # python >= ?
+ from urlparse import parse_qs
+except ImportError:
+ # python <= ?
+ from cgi import parse_qs
+
+
# django.test.client.RequestFactory (Django >= 1.3)
try:
from django.test.client import RequestFactory
-
except ImportError:
from django.test import Client
from django.core.handlers.wsgi import WSGIRequest
@@ -49,7 +64,7 @@ except ImportError:
# django.views.generic.View (Django >= 1.3)
try:
from django.views.generic import View
-except:
+except ImportError:
from django import http
from django.utils.functional import update_wrapper
# from django.utils.log import getLogger
@@ -127,10 +142,47 @@ except:
#)
return http.HttpResponseNotAllowed(allowed_methods)
-# parse_qs
+
try:
- # python >= ?
- from urlparse import parse_qs
+ import markdown
+ import re
+
+ class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
+ """Override markdown's SetextHeaderProcessor, so that ==== headers are
and ---- headers are
.
+
+ We use
for the resource name."""
+
+ # Detect Setext-style header. Must be first 2 lines of block.
+ RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
+
+ def test(self, parent, block):
+ return bool(self.RE.match(block))
+
+ def run(self, parent, blocks):
+ lines = blocks.pop(0).split('\n')
+ # Determine level. ``=`` is 1 and ``-`` is 2.
+ if lines[1].startswith('='):
+ level = 2
+ else:
+ level = 3
+ h = markdown.etree.SubElement(parent, 'h%d' % level)
+ h.text = lines[0].strip()
+ if len(lines) > 2:
+ # Block contains additional lines. Add to master blocks for later.
+ blocks.insert(0, '\n'.join(lines[2:]))
+
+ def apply_markdown(text):
+ """Simple wrapper around markdown.markdown to apply our CustomSetextHeaderProcessor,
+ and also set the base level of '#' style headers to
."""
+ extensions = ['headerid(level=2)']
+ safe_mode = False,
+ output_format = markdown.DEFAULT_OUTPUT_FORMAT
+
+ md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
+ safe_mode=safe_mode,
+ output_format=output_format)
+ md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
+ return md.convert(text)
+
except ImportError:
- # python <= ?
- from cgi import parse_qs
\ No newline at end of file
+ apply_markdown = None
\ No newline at end of file
diff --git a/djangorestframework/description.py b/djangorestframework/description.py
deleted file mode 100644
index f7145c0f..00000000
--- a/djangorestframework/description.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Get a descriptive name and description for a view,
-based on class name and docstring, and override-able by 'name' and 'description' attributes"""
-import re
-
-def get_name(view):
- """Return a name for the view.
-
- If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
- if getattr(view, 'name', None) is not None:
- return view.name
-
- if getattr(view, '__name__', None) is not None:
- name = view.__name__
- elif getattr(view, '__class__', None) is not None: # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
- name = view.__class__.__name__
- else:
- return ''
-
- return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
-
-def get_description(view):
- """Provide a description for the view.
-
- By default this is the view's docstring with nice unindention applied."""
- if getattr(view, 'description', None) is not None:
- return getattr(view, 'description')
-
- if getattr(view, '__doc__', None) is not None:
- whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]
-
- if whitespace_counts:
- whitespace_pattern = '^' + (' ' * min(whitespace_counts))
- return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)
-
- return view.__doc__
-
- return ''
\ No newline at end of file
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
deleted file mode 100644
index 87b3e94e..00000000
--- a/djangorestframework/emitters.py
+++ /dev/null
@@ -1,243 +0,0 @@
-"""Emitters are used to serialize a Resource's output into specific media types.
-django-rest-framework also provides HTML and PlainText emitters that help self-document the API,
-by serializing the output along with documentation regarding the Resource, output status and headers,
-and providing forms and links depending on the allowed methods, emitters and parsers on the Resource.
-"""
-from django import forms
-from django.conf import settings
-from django.template import RequestContext, loader
-from django.utils import simplejson as json
-from django import forms
-
-from djangorestframework.utils import dict2xml, url_resolves
-from djangorestframework.markdownwrapper import apply_markdown
-from djangorestframework.breadcrumbs import get_breadcrumbs
-from djangorestframework.description import get_name, get_description
-from djangorestframework import status
-
-from urllib import quote_plus
-import string
-import re
-from decimal import Decimal
-
-# TODO: Rename verbose to something more appropriate
-# TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
-# and only have an emitter output anything if it explicitly provides support for that.
-
-class BaseEmitter(object):
- """All emitters must extend this class, set the media_type attribute, and
- override the emit() function."""
- media_type = None
-
- def __init__(self, resource):
- self.resource = resource
-
- def emit(self, output=None, verbose=False):
- """By default emit simply returns the ouput as-is.
- Override this method to provide for other behaviour."""
- if output is None:
- return ''
-
- return output
-
-
-class TemplateEmitter(BaseEmitter):
- """Provided for convienience.
- Emit the output by simply rendering it with the given template."""
- media_type = None
- template = None
-
- def emit(self, output=None, verbose=False):
- if output is None:
- return ''
-
- context = RequestContext(self.request, output)
- return self.template.render(context)
-
-
-class DocumentingTemplateEmitter(BaseEmitter):
- """Base class for emitters used to self-document the API.
- Implementing classes should extend this class and set the template attribute."""
- template = None
-
- def _get_content(self, resource, request, output):
- """Get the content as if it had been emitted by a non-documenting emitter.
-
- (Typically this will be the content as it would have been if the Resource had been
- requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
-
- # Find the first valid emitter and emit the content. (Don't use another documenting emitter.)
- emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)]
- if not emitters:
- return '[No emitters were found]'
-
- content = emitters[0](resource).emit(output, verbose=True)
- if not all(char in string.printable for char in content):
- return '[%d bytes of binary content]'
-
- return content
-
-
- def _get_form_instance(self, resource):
- """Get a form, possibly bound to either the input or output data.
- In the absence on of the Resource having an associated form then
- provide a form that can be used to submit arbitrary content."""
- # Get the form instance if we have one bound to the input
- #form_instance = resource.form_instance
- # TODO! Reinstate this
-
- form_instance = getattr(resource, 'bound_form_instance', None)
-
- if not form_instance and hasattr(resource, 'get_bound_form'):
- # Otherwise if we have a response that is valid against the form then use that
- if resource.response.has_content_body:
- try:
- form_instance = resource.get_bound_form(resource.response.cleaned_content)
- if form_instance and not form_instance.is_valid():
- form_instance = None
- except:
- form_instance = None
-
- # If we still don't have a form instance then try to get an unbound form
- if not form_instance:
- try:
- form_instance = resource.get_bound_form()
- except:
- pass
-
- # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
- if not form_instance:
- form_instance = self._get_generic_content_form(resource)
-
- return form_instance
-
-
- def _get_generic_content_form(self, resource):
- """Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
- (Which are typically application/x-www-form-urlencoded)"""
-
- # If we're not using content overloading there's no point in supplying a generic form,
- # as the resource won't treat the form's value as the content of the request.
- if not getattr(resource, 'USE_FORM_OVERLOADING', False):
- return None
-
- # NB. http://jacobian.org/writing/dynamic-form-generation/
- class GenericContentForm(forms.Form):
- def __init__(self, resource):
- """We don't know the names of the fields we want to set until the point the form is instantiated,
- as they are determined by the Resource the form is being created against.
- Add the fields dynamically."""
- super(GenericContentForm, self).__init__()
-
- contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
- initial_contenttype = resource.default_parser.media_type
-
- self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
- choices=contenttype_choices,
- initial=initial_contenttype)
- self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
- widget=forms.Textarea)
-
- # If either of these reserved parameters are turned off then content tunneling is not possible
- if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
- return None
-
- # Okey doke, let's do it
- return GenericContentForm(resource)
-
-
- def emit(self, output=None):
- content = self._get_content(self.resource, self.resource.request, output)
- form_instance = self._get_form_instance(self.resource)
-
- if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
- login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path))
- logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path))
- else:
- login_url = None
- logout_url = None
-
- name = get_name(self.resource)
- description = get_description(self.resource)
-
- markeddown = None
- if apply_markdown:
- try:
- markeddown = apply_markdown(description)
- except AttributeError: # TODO: possibly split the get_description / get_name into a mixin class
- markeddown = None
-
- breadcrumb_list = get_breadcrumbs(self.resource.request.path)
-
- template = loader.get_template(self.template)
- context = RequestContext(self.resource.request, {
- 'content': content,
- 'resource': self.resource,
- 'request': self.resource.request,
- 'response': self.resource.response,
- 'description': description,
- 'name': name,
- 'markeddown': markeddown,
- 'breadcrumblist': breadcrumb_list,
- 'form': form_instance,
- 'login_url': login_url,
- 'logout_url': logout_url,
- 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
- })
-
- ret = template.render(context)
-
- return ret
-
-
-class JSONEmitter(BaseEmitter):
- """Emitter which serializes to JSON"""
- media_type = 'application/json'
-
- def emit(self, output=None, verbose=False):
- if output is None:
- return ''
- if verbose:
- return json.dumps(output, indent=4, sort_keys=True)
- return json.dumps(output)
-
-
-class XMLEmitter(BaseEmitter):
- """Emitter which serializes to XML."""
- media_type = 'application/xml'
-
- def emit(self, output=None, verbose=False):
- if output is None:
- return ''
- return dict2xml(output)
-
-
-class DocumentingHTMLEmitter(DocumentingTemplateEmitter):
- """Emitter which provides a browsable HTML interface for an API.
- See the examples listed in the django-rest-framework documentation to see this in actions."""
- media_type = 'text/html'
- template = 'emitter.html'
-
-
-class DocumentingXHTMLEmitter(DocumentingTemplateEmitter):
- """Identical to DocumentingHTMLEmitter, except with an xhtml media type.
- We need this to be listed in preference to xml in order to return HTML to WebKit based browsers,
- given their Accept headers."""
- media_type = 'application/xhtml+xml'
- template = 'emitter.html'
-
-
-class DocumentingPlainTextEmitter(DocumentingTemplateEmitter):
- """Emitter that serializes the output with the default emitter, but also provides plain-text
- doumentation of the returned status and headers, and of the resource's name and description.
- Useful for browsing an API with command line tools."""
- media_type = 'text/plain'
- template = 'emitter.txt'
-
-DEFAULT_EMITTERS = ( JSONEmitter,
- DocumentingHTMLEmitter,
- DocumentingXHTMLEmitter,
- DocumentingPlainTextEmitter,
- XMLEmitter )
-
-
diff --git a/djangorestframework/markdownwrapper.py b/djangorestframework/markdownwrapper.py
deleted file mode 100644
index 70512440..00000000
--- a/djangorestframework/markdownwrapper.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""If python-markdown is installed expose an apply_markdown(text) function,
-to convert markeddown text into html. Otherwise just set apply_markdown to None.
-
-See: http://www.freewisdom.org/projects/python-markdown/
-"""
-
-__all__ = ['apply_markdown']
-
-try:
- import markdown
- import re
-
- class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
- """Override markdown's SetextHeaderProcessor, so that ==== headers are
and ---- headers are
.
-
- We use
for the resource name."""
-
- # Detect Setext-style header. Must be first 2 lines of block.
- RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
-
- def test(self, parent, block):
- return bool(self.RE.match(block))
-
- def run(self, parent, blocks):
- lines = blocks.pop(0).split('\n')
- # Determine level. ``=`` is 1 and ``-`` is 2.
- if lines[1].startswith('='):
- level = 2
- else:
- level = 3
- h = markdown.etree.SubElement(parent, 'h%d' % level)
- h.text = lines[0].strip()
- if len(lines) > 2:
- # Block contains additional lines. Add to master blocks for later.
- blocks.insert(0, '\n'.join(lines[2:]))
-
- def apply_markdown(text):
- """Simple wrapper around markdown.markdown to apply our CustomSetextHeaderProcessor,
- and also set the base level of '#' style headers to
."""
- extensions = ['headerid(level=2)']
- safe_mode = False,
- output_format = markdown.DEFAULT_OUTPUT_FORMAT
-
- md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
- safe_mode=safe_mode,
- output_format=output_format)
- md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
- return md.convert(text)
-
-except:
- apply_markdown = None
\ No newline at end of file
diff --git a/djangorestframework/mediatypes.py b/djangorestframework/mediatypes.py
deleted file mode 100644
index 92d9264c..00000000
--- a/djangorestframework/mediatypes.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""
-Handling of media types, as found in HTTP Content-Type and Accept headers.
-
-See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
-"""
-
-from django.http.multipartparser import parse_header
-
-
-class MediaType(object):
- def __init__(self, media_type_str):
- self.orig = media_type_str
- self.media_type, self.params = parse_header(media_type_str)
- self.main_type, sep, self.sub_type = self.media_type.partition('/')
-
- def match(self, other):
- """Return true if this MediaType satisfies the constraint of the given MediaType."""
- for key in other.params.keys():
- if key != 'q' and other.params[key] != self.params.get(key, None):
- return False
-
- if other.sub_type != '*' and other.sub_type != self.sub_type:
- return False
-
- if other.main_type != '*' and other.main_type != self.main_type:
- return False
-
- return True
-
- def precedence(self):
- """
- Return a precedence level for the media type given how specific it is.
- """
- if self.main_type == '*':
- return 1
- elif self.sub_type == '*':
- return 2
- elif not self.params or self.params.keys() == ['q']:
- return 3
- return 4
-
- def quality(self):
- """
- Return a quality level for the media type.
- """
- try:
- return Decimal(self.params.get('q', '1.0'))
- except:
- return Decimal(0)
-
- def score(self):
- """
- Return an overall score for a given media type given it's quality and precedence.
- """
- # NB. quality values should only have up to 3 decimal points
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
- return self.quality * 10000 + self.precedence
-
- def is_form(self):
- """
- Return True if the MediaType is a valid form media type as defined by the HTML4 spec.
- (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
- """
- return self.media_type == 'application/x-www-form-urlencoded' or \
- self.media_type == 'multipart/form-data'
-
- def as_tuple(self):
- return (self.main_type, self.sub_type, self.params)
-
- def __repr__(self):
- return "" % (self.as_tuple(),)
-
- def __str__(self):
- return unicode(self).encode('utf-8')
-
- def __unicode__(self):
- return self.orig
-
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 43b33f50..ebeee31a 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -1,4 +1,4 @@
-from djangorestframework.mediatypes import MediaType
+from djangorestframework.utils.mediatypes import MediaType
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
from djangorestframework.response import ErrorResponse
from djangorestframework.parsers import FormParser, MultipartParser
@@ -397,7 +397,7 @@ class ResponseMixin(object):
class AuthMixin(object):
"""Mixin class to provide authentication and permission checking."""
- authenticators = ()
+ authentication = ()
permissions = ()
@property
@@ -407,9 +407,9 @@ class AuthMixin(object):
return self._auth
def _authenticate(self):
- for authenticator_cls in self.authenticators:
- authenticator = authenticator_cls(self)
- auth = authenticator.authenticate(self.request)
+ for authentication_cls in self.authentication:
+ authentication = authentication_cls(self)
+ auth = authentication.authenticate(self.request)
if auth:
return auth
return None
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 96b29a66..6d6bd5ce 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -14,7 +14,7 @@ from django.utils import simplejson as json
from djangorestframework.response import ErrorResponse
from djangorestframework import status
from djangorestframework.utils import as_tuple
-from djangorestframework.mediatypes import MediaType
+from djangorestframework.utils.mediatypes import MediaType
from djangorestframework.compat import parse_qs
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
new file mode 100644
index 00000000..e53dc061
--- /dev/null
+++ b/djangorestframework/renderers.py
@@ -0,0 +1,243 @@
+"""Renderers are used to serialize a Resource's output into specific media types.
+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,
+and providing forms and links depending on the allowed methods, renderers and parsers on the Resource.
+"""
+from django import forms
+from django.conf import settings
+from django.template import RequestContext, loader
+from django.utils import simplejson as json
+from django import forms
+
+from djangorestframework.utils import dict2xml, url_resolves
+from djangorestframework.compat import apply_markdown
+from djangorestframework.utils.breadcrumbs import get_breadcrumbs
+from djangorestframework.utils.description import get_name, get_description
+from djangorestframework import status
+
+from urllib import quote_plus
+import string
+import re
+from decimal import Decimal
+
+# TODO: Rename verbose to something more appropriate
+# TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
+# and only have an renderer output anything if it explicitly provides support for that.
+
+class BaseRenderer(object):
+ """All renderers must extend this class, set the media_type attribute, and
+ override the render() function."""
+ media_type = None
+
+ def __init__(self, resource):
+ self.resource = resource
+
+ def render(self, output=None, verbose=False):
+ """By default render simply returns the ouput as-is.
+ Override this method to provide for other behaviour."""
+ if output is None:
+ return ''
+
+ return output
+
+
+class TemplateRenderer(BaseRenderer):
+ """Provided for convienience.
+ Emit the output by simply rendering it with the given template."""
+ media_type = None
+ template = None
+
+ def render(self, output=None, verbose=False):
+ if output is None:
+ return ''
+
+ context = RequestContext(self.request, output)
+ return self.template.render(context)
+
+
+class DocumentingTemplateRenderer(BaseRenderer):
+ """Base class for renderers used to self-document the API.
+ Implementing classes should extend this class and set the template attribute."""
+ template = None
+
+ def _get_content(self, resource, request, output):
+ """Get the content as if it had been renderted by a non-documenting renderer.
+
+ (Typically this will be the content as it would have been if the Resource had been
+ requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
+
+ # Find the first valid renderer and render the content. (Don't use another documenting renderer.)
+ renderers = [renderer for renderer in resource.renderers if not isinstance(renderer, DocumentingTemplateRenderer)]
+ if not renderers:
+ return '[No renderers were found]'
+
+ content = renderers[0](resource).render(output, verbose=True)
+ if not all(char in string.printable for char in content):
+ return '[%d bytes of binary content]'
+
+ return content
+
+
+ def _get_form_instance(self, resource):
+ """Get a form, possibly bound to either the input or output data.
+ In the absence on of the Resource having an associated form then
+ provide a form that can be used to submit arbitrary content."""
+ # Get the form instance if we have one bound to the input
+ #form_instance = resource.form_instance
+ # TODO! Reinstate this
+
+ form_instance = getattr(resource, 'bound_form_instance', None)
+
+ if not form_instance and hasattr(resource, 'get_bound_form'):
+ # Otherwise if we have a response that is valid against the form then use that
+ if resource.response.has_content_body:
+ try:
+ form_instance = resource.get_bound_form(resource.response.cleaned_content)
+ if form_instance and not form_instance.is_valid():
+ form_instance = None
+ except:
+ form_instance = None
+
+ # If we still don't have a form instance then try to get an unbound form
+ if not form_instance:
+ try:
+ form_instance = resource.get_bound_form()
+ except:
+ pass
+
+ # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
+ if not form_instance:
+ form_instance = self._get_generic_content_form(resource)
+
+ return form_instance
+
+
+ def _get_generic_content_form(self, resource):
+ """Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
+ (Which are typically application/x-www-form-urlencoded)"""
+
+ # If we're not using content overloading there's no point in supplying a generic form,
+ # as the resource won't treat the form's value as the content of the request.
+ if not getattr(resource, 'USE_FORM_OVERLOADING', False):
+ return None
+
+ # NB. http://jacobian.org/writing/dynamic-form-generation/
+ class GenericContentForm(forms.Form):
+ def __init__(self, resource):
+ """We don't know the names of the fields we want to set until the point the form is instantiated,
+ as they are determined by the Resource the form is being created against.
+ Add the fields dynamically."""
+ super(GenericContentForm, self).__init__()
+
+ contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
+ initial_contenttype = resource.default_parser.media_type
+
+ self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
+ choices=contenttype_choices,
+ initial=initial_contenttype)
+ self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
+ widget=forms.Textarea)
+
+ # If either of these reserved parameters are turned off then content tunneling is not possible
+ if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
+ return None
+
+ # Okey doke, let's do it
+ return GenericContentForm(resource)
+
+
+ def render(self, output=None):
+ content = self._get_content(self.resource, self.resource.request, output)
+ form_instance = self._get_form_instance(self.resource)
+
+ if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
+ login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path))
+ logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path))
+ else:
+ login_url = None
+ logout_url = None
+
+ name = get_name(self.resource)
+ description = get_description(self.resource)
+
+ markeddown = None
+ if apply_markdown:
+ try:
+ markeddown = apply_markdown(description)
+ except AttributeError: # TODO: possibly split the get_description / get_name into a mixin class
+ markeddown = None
+
+ breadcrumb_list = get_breadcrumbs(self.resource.request.path)
+
+ template = loader.get_template(self.template)
+ context = RequestContext(self.resource.request, {
+ 'content': content,
+ 'resource': self.resource,
+ 'request': self.resource.request,
+ 'response': self.resource.response,
+ 'description': description,
+ 'name': name,
+ 'markeddown': markeddown,
+ 'breadcrumblist': breadcrumb_list,
+ 'form': form_instance,
+ 'login_url': login_url,
+ 'logout_url': logout_url,
+ 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
+ })
+
+ ret = template.render(context)
+
+ return ret
+
+
+class JSONRenderer(BaseRenderer):
+ """Renderer which serializes to JSON"""
+ media_type = 'application/json'
+
+ def render(self, output=None, verbose=False):
+ if output is None:
+ return ''
+ if verbose:
+ return json.dumps(output, indent=4, sort_keys=True)
+ return json.dumps(output)
+
+
+class XMLRenderer(BaseRenderer):
+ """Renderer which serializes to XML."""
+ media_type = 'application/xml'
+
+ def render(self, output=None, verbose=False):
+ if output is None:
+ return ''
+ return dict2xml(output)
+
+
+class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
+ """Renderer which provides a browsable HTML interface for an API.
+ See the examples listed in the django-rest-framework documentation to see this in actions."""
+ media_type = 'text/html'
+ template = 'renderer.html'
+
+
+class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
+ """Identical to DocumentingHTMLRenderer, except with an xhtml media type.
+ We need this to be listed in preference to xml in order to return HTML to WebKit based browsers,
+ given their Accept headers."""
+ media_type = 'application/xhtml+xml'
+ template = 'renderer.html'
+
+
+class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
+ """Renderer that serializes the output with the default renderer, but also provides plain-text
+ doumentation of the returned status and headers, and of the resource's name and description.
+ Useful for browsing an API with command line tools."""
+ media_type = 'text/plain'
+ template = 'renderer.txt'
+
+DEFAULT_RENDERERS = ( JSONRenderer,
+ DocumentingHTMLRenderer,
+ DocumentingXHTMLRenderer,
+ DocumentingPlainTextRenderer,
+ XMLRenderer )
+
+
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index cb4d080c..7879da7c 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
-from djangorestframework import renderers, parsers, authenticators, permissions, validators, status
+from djangorestframework import renderers, parsers, authentication, permissions, validators, status
# TODO: Figure how out references and named urls need to work nicely
@@ -37,8 +37,8 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
validators = ( validators.FormValidator, )
# List of all authenticating methods to attempt.
- authenticators = ( authenticators.UserLoggedInAuthenticator,
- authenticators.BasicAuthenticator )
+ authentication = ( authentication.UserLoggedInAuthenticator,
+ authentication.BasicAuthenticator )
# List of all permissions required to access the resource
permissions = ()
diff --git a/djangorestframework/templates/emitter.html b/djangorestframework/templates/emitter.html
deleted file mode 100644
index 1931ad39..00000000
--- a/djangorestframework/templates/emitter.html
+++ /dev/null
@@ -1,127 +0,0 @@
-{% load urlize_quoted_links %}{% load add_query_param %}
-
-
-
-
-
-
- Django REST framework - {{ name }}
-
-
-
- {% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} Log out{% endif %}{% else %}Anonymous {% if login_url %}Log in{% endif %}{% endif %}
-
-
-
-
- {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
- {{breadcrumb_name}} {% if not forloop.last %}›{% endif %}
- {% endfor %}
-
-
-
-
-
-
{{ name }}
-
{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}
-
-
{{ response.status }} {{ response.status_text }}{% autoescape off %}
-{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
-{% endfor %}
-{{ content|urlize_quoted_links }}
{% endautoescape %}
-
- {% if 'GET' in resource.allowed_methods %}
-
- {% endif %}
-
- {% comment %} *** Only display the POST/PUT/DELETE forms if we have a bound form, and if method ***
- *** tunneling via POST forms is enabled. ***
- *** (We could display only the POST form if method tunneling is disabled, but I think ***
- *** the user experience would be confusing, so we simply turn all forms off. *** {% endcomment %}
-
- {% if resource.METHOD_PARAM and form %}
- {% if 'POST' in resource.allowed_methods %}
-
- {% endif %}
-
- {% if 'PUT' in resource.allowed_methods %}
-
- {% endif %}
-
- {% if 'DELETE' in resource.allowed_methods %}
-
- {% endif %}
- {% endif %}
-
-
-
-
-
\ No newline at end of file
diff --git a/djangorestframework/templates/emitter.txt b/djangorestframework/templates/emitter.txt
deleted file mode 100644
index 5be8c117..00000000
--- a/djangorestframework/templates/emitter.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-{{ name }}
-
-{{ description }}
-
-{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
-{% for key, val in response.headers.items %}{{ key }}: {{ val }}
-{% endfor %}
-{{ content }}{% endautoescape %}
diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html
new file mode 100644
index 00000000..105ea0a2
--- /dev/null
+++ b/djangorestframework/templates/renderer.html
@@ -0,0 +1,127 @@
+{% load urlize_quoted_links %}{% load add_query_param %}
+
+
+
+
+
+
+ Django REST framework - {{ name }}
+
+
+
+ {% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} Log out{% endif %}{% else %}Anonymous {% if login_url %}Log in{% endif %}{% endif %}
+
+
+
+
+ {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
+ {{breadcrumb_name}} {% if not forloop.last %}›{% endif %}
+ {% endfor %}
+
+
+
+
+
+
{{ name }}
+
{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}
+
+
{{ response.status }} {{ response.status_text }}{% autoescape off %}
+{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
+{% endfor %}
+{{ content|urlize_quoted_links }}
{% endautoescape %}
+
+ {% if 'GET' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% comment %} *** Only display the POST/PUT/DELETE forms if we have a bound form, and if method ***
+ *** tunneling via POST forms is enabled. ***
+ *** (We could display only the POST form if method tunneling is disabled, but I think ***
+ *** the user experience would be confusing, so we simply turn all forms off. *** {% endcomment %}
+
+ {% if resource.METHOD_PARAM and form %}
+ {% if 'POST' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% if 'PUT' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% if 'DELETE' in resource.allowed_methods %}
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+
\ No newline at end of file
diff --git a/djangorestframework/templates/renderer.txt b/djangorestframework/templates/renderer.txt
new file mode 100644
index 00000000..5be8c117
--- /dev/null
+++ b/djangorestframework/templates/renderer.txt
@@ -0,0 +1,8 @@
+{{ name }}
+
+{{ description }}
+
+{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
+{% for key, val in response.headers.items %}{{ key }}: {{ val }}
+{% endfor %}
+{{ content }}{% endautoescape %}
diff --git a/djangorestframework/tests/breadcrumbs.py b/djangorestframework/tests/breadcrumbs.py
index 724f2ff5..2f9a7e9d 100644
--- a/djangorestframework/tests/breadcrumbs.py
+++ b/djangorestframework/tests/breadcrumbs.py
@@ -1,6 +1,6 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
-from djangorestframework.breadcrumbs import get_breadcrumbs
+from djangorestframework.utils.breadcrumbs import get_breadcrumbs
from djangorestframework.resource import Resource
class Root(Resource):
diff --git a/djangorestframework/tests/description.py b/djangorestframework/tests/description.py
index 3e3f7b21..d34e2d11 100644
--- a/djangorestframework/tests/description.py
+++ b/djangorestframework/tests/description.py
@@ -1,7 +1,7 @@
from django.test import TestCase
from djangorestframework.resource import Resource
-from djangorestframework.markdownwrapper import apply_markdown
-from djangorestframework.description import get_name, get_description
+from djangorestframework.compat import apply_markdown
+from djangorestframework.utils.description import get_name, get_description
# We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring
diff --git a/djangorestframework/tests/emitters.py b/djangorestframework/tests/emitters.py
deleted file mode 100644
index 21a7eb95..00000000
--- a/djangorestframework/tests/emitters.py
+++ /dev/null
@@ -1,76 +0,0 @@
-from django.conf.urls.defaults import patterns, url
-from django import http
-from django.test import TestCase
-from djangorestframework.compat import View
-from djangorestframework.emitters import BaseEmitter
-from djangorestframework.mixins import ResponseMixin
-from djangorestframework.response import Response
-
-DUMMYSTATUS = 200
-DUMMYCONTENT = 'dummycontent'
-
-EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x
-EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x
-
-class MockView(ResponseMixin, View):
- def get(self, request):
- response = Response(DUMMYSTATUS, DUMMYCONTENT)
- return self.emit(response)
-
-class EmitterA(BaseEmitter):
- media_type = 'mock/emittera'
-
- def emit(self, output, verbose=False):
- return EMITTER_A_SERIALIZER(output)
-
-class EmitterB(BaseEmitter):
- media_type = 'mock/emitterb'
-
- def emit(self, output, verbose=False):
- return EMITTER_B_SERIALIZER(output)
-
-
-urlpatterns = patterns('',
- url(r'^$', MockView.as_view(emitters=[EmitterA, EmitterB])),
-)
-
-
-class EmitterIntegrationTests(TestCase):
- """End-to-end testing of emitters using an EmitterMixin on a generic view."""
-
- urls = 'djangorestframework.tests.emitters'
-
- def test_default_emitter_serializes_content(self):
- """If the Accept header is not set the default emitter should serialize the response."""
- resp = self.client.get('/')
- self.assertEquals(resp['Content-Type'], EmitterA.media_type)
- self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT))
- self.assertEquals(resp.status_code, DUMMYSTATUS)
-
- def test_default_emitter_serializes_content_on_accept_any(self):
- """If the Accept header is set to */* the default emitter should serialize the response."""
- resp = self.client.get('/', HTTP_ACCEPT='*/*')
- self.assertEquals(resp['Content-Type'], EmitterA.media_type)
- self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT))
- self.assertEquals(resp.status_code, DUMMYSTATUS)
-
- def test_specified_emitter_serializes_content_default_case(self):
- """If the Accept header is set the specified emitter should serialize the response.
- (In this case we check that works for the default emitter)"""
- resp = self.client.get('/', HTTP_ACCEPT=EmitterA.media_type)
- self.assertEquals(resp['Content-Type'], EmitterA.media_type)
- self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT))
- self.assertEquals(resp.status_code, DUMMYSTATUS)
-
- def test_specified_emitter_serializes_content_non_default_case(self):
- """If the Accept header is set the specified emitter should serialize the response.
- (In this case we check that works for a non-default emitter)"""
- resp = self.client.get('/', HTTP_ACCEPT=EmitterB.media_type)
- self.assertEquals(resp['Content-Type'], EmitterB.media_type)
- self.assertEquals(resp.content, EMITTER_B_SERIALIZER(DUMMYCONTENT))
- self.assertEquals(resp.status_code, DUMMYSTATUS)
-
- def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
- """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
- resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
- self.assertEquals(resp.status_code, 406)
\ No newline at end of file
diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py
index 4753f6f3..00ebc812 100644
--- a/djangorestframework/tests/parsers.py
+++ b/djangorestframework/tests/parsers.py
@@ -82,7 +82,7 @@ from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.parsers import MultipartParser
from djangorestframework.resource import Resource
-from djangorestframework.mediatypes import MediaType
+from djangorestframework.utils.mediatypes import MediaType
from StringIO import StringIO
def encode_multipart_formdata(fields, files):
diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py
new file mode 100644
index 00000000..df0d9c8d
--- /dev/null
+++ b/djangorestframework/tests/renderers.py
@@ -0,0 +1,76 @@
+from django.conf.urls.defaults import patterns, url
+from django import http
+from django.test import TestCase
+from djangorestframework.compat import View
+from djangorestframework.renderers import BaseRenderer
+from djangorestframework.mixins import ResponseMixin
+from djangorestframework.response import Response
+
+DUMMYSTATUS = 200
+DUMMYCONTENT = 'dummycontent'
+
+RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
+RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
+
+class MockView(ResponseMixin, View):
+ def get(self, request):
+ response = Response(DUMMYSTATUS, DUMMYCONTENT)
+ return self.render(response)
+
+class RendererA(BaseRenderer):
+ media_type = 'mock/renderera'
+
+ def render(self, output, verbose=False):
+ return RENDERER_A_SERIALIZER(output)
+
+class RendererB(BaseRenderer):
+ media_type = 'mock/rendererb'
+
+ def render(self, output, verbose=False):
+ return RENDERER_B_SERIALIZER(output)
+
+
+urlpatterns = patterns('',
+ url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
+)
+
+
+class RendererIntegrationTests(TestCase):
+ """End-to-end testing of renderers using an RendererMixin on a generic view."""
+
+ urls = 'djangorestframework.tests.renderers'
+
+ def test_default_renderer_serializes_content(self):
+ """If the Accept header is not set the default renderer should serialize the response."""
+ resp = self.client.get('/')
+ self.assertEquals(resp['Content-Type'], RendererA.media_type)
+ self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
+ self.assertEquals(resp.status_code, DUMMYSTATUS)
+
+ def test_default_renderer_serializes_content_on_accept_any(self):
+ """If the Accept header is set to */* the default renderer should serialize the response."""
+ resp = self.client.get('/', HTTP_ACCEPT='*/*')
+ self.assertEquals(resp['Content-Type'], RendererA.media_type)
+ self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
+ self.assertEquals(resp.status_code, DUMMYSTATUS)
+
+ def test_specified_renderer_serializes_content_default_case(self):
+ """If the Accept header is set the specified renderer should serialize the response.
+ (In this case we check that works for the default renderer)"""
+ resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
+ self.assertEquals(resp['Content-Type'], RendererA.media_type)
+ self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
+ self.assertEquals(resp.status_code, DUMMYSTATUS)
+
+ def test_specified_renderer_serializes_content_non_default_case(self):
+ """If the Accept header is set the specified renderer should serialize the response.
+ (In this case we check that works for a non-default renderer)"""
+ resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
+ self.assertEquals(resp['Content-Type'], RendererB.media_type)
+ self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
+ self.assertEquals(resp.status_code, DUMMYSTATUS)
+
+ def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
+ """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
+ resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
+ self.assertEquals(resp.status_code, 406)
\ No newline at end of file
diff --git a/djangorestframework/utils.py b/djangorestframework/utils.py
deleted file mode 100644
index f60bdee4..00000000
--- a/djangorestframework/utils.py
+++ /dev/null
@@ -1,159 +0,0 @@
-import re
-import xml.etree.ElementTree as ET
-from django.utils.encoding import smart_unicode
-from django.utils.xmlutils import SimplerXMLGenerator
-from django.core.urlresolvers import resolve
-from django.conf import settings
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-
-
-#def admin_media_prefix(request):
-# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
-# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
-
-MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
-
-def as_tuple(obj):
- """Given 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:
- resolve(url)
- except:
- return False
- return True
-
-
-# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
-#class object_dict(dict):
-# """object view of dict, you can
-# >>> a = object_dict()
-# >>> a.fish = 'fish'
-# >>> a['fish']
-# 'fish'
-# >>> a['water'] = 'water'
-# >>> a.water
-# 'water'
-# >>> a.test = {'value': 1}
-# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
-# >>> a.test, a.test2.name, a.test2.value
-# (1, 'test2', 2)
-# """
-# def __init__(self, initd=None):
-# if initd is None:
-# initd = {}
-# dict.__init__(self, initd)
-#
-# def __getattr__(self, item):
-# d = self.__getitem__(item)
-# # if value is the only key in object, you can omit it
-# if isinstance(d, dict) and 'value' in d and len(d) == 1:
-# return d['value']
-# else:
-# return d
-#
-# def __setattr__(self, item, value):
-# self.__setitem__(item, value)
-
-
-# From xml2dict
-class XML2Dict(object):
-
- def __init__(self):
- pass
-
- def _parse_node(self, node):
- node_tree = {}
- # Save attrs and text, hope there will not be a child with same name
- if node.text:
- node_tree = node.text
- for (k,v) in node.attrib.items():
- k,v = self._namespace_split(k, v)
- node_tree[k] = v
- #Save childrens
- for child in node.getchildren():
- tag, tree = self._namespace_split(child.tag, self._parse_node(child))
- if tag not in node_tree: # the first time, so store it in dict
- node_tree[tag] = tree
- continue
- old = node_tree[tag]
- if not isinstance(old, list):
- node_tree.pop(tag)
- node_tree[tag] = [old] # multi times, so change old dict to a list
- node_tree[tag].append(tree) # add the new one
-
- return node_tree
-
-
- def _namespace_split(self, tag, value):
- """
- Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
- ns = http://cs.sfsu.edu/csc867/myscheduler
- name = patients
- """
- result = re.compile("\{(.*)\}(.*)").search(tag)
- if result:
- 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())
-
- def fromstring(self, s):
- """parse a string"""
- t = ET.fromstring(s)
- unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
- return root_tree
-
-
-def xml2dict(input):
- return XML2Dict().fromstring(input)
-
-
-# Piston:
-class XMLRenderer():
- def _to_xml(self, xml, data):
- if isinstance(data, (list, tuple)):
- for item in data:
- xml.startElement("list-item", {})
- self._to_xml(xml, item)
- xml.endElement("list-item")
-
- elif isinstance(data, dict):
- for key, value in data.iteritems():
- xml.startElement(key, {})
- self._to_xml(xml, value)
- xml.endElement(key)
-
- else:
- xml.characters(smart_unicode(data))
-
- def dict2xml(self, data):
- stream = StringIO.StringIO()
-
- xml = SimplerXMLGenerator(stream, "utf-8")
- xml.startDocument()
- xml.startElement("root", {})
-
- self._to_xml(xml, data)
-
- xml.endElement("root")
- xml.endDocument()
- return stream.getvalue()
-
-def dict2xml(input):
- return XMLRenderer().dict2xml(input)
diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py
new file mode 100644
index 00000000..9dc769be
--- /dev/null
+++ b/djangorestframework/utils/__init__.py
@@ -0,0 +1,158 @@
+from django.utils.encoding import smart_unicode
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.core.urlresolvers import resolve
+from django.conf import settings
+
+from djangorestframework.compat import StringIO
+
+import re
+import xml.etree.ElementTree as ET
+
+
+#def admin_media_prefix(request):
+# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
+# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
+
+MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
+
+def as_tuple(obj):
+ """Given 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:
+ resolve(url)
+ except:
+ return False
+ return True
+
+
+# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
+#class object_dict(dict):
+# """object view of dict, you can
+# >>> a = object_dict()
+# >>> a.fish = 'fish'
+# >>> a['fish']
+# 'fish'
+# >>> a['water'] = 'water'
+# >>> a.water
+# 'water'
+# >>> a.test = {'value': 1}
+# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
+# >>> a.test, a.test2.name, a.test2.value
+# (1, 'test2', 2)
+# """
+# def __init__(self, initd=None):
+# if initd is None:
+# initd = {}
+# dict.__init__(self, initd)
+#
+# def __getattr__(self, item):
+# d = self.__getitem__(item)
+# # if value is the only key in object, you can omit it
+# if isinstance(d, dict) and 'value' in d and len(d) == 1:
+# return d['value']
+# else:
+# return d
+#
+# def __setattr__(self, item, value):
+# self.__setitem__(item, value)
+
+
+# From xml2dict
+class XML2Dict(object):
+
+ def __init__(self):
+ pass
+
+ def _parse_node(self, node):
+ node_tree = {}
+ # Save attrs and text, hope there will not be a child with same name
+ if node.text:
+ node_tree = node.text
+ for (k,v) in node.attrib.items():
+ k,v = self._namespace_split(k, v)
+ node_tree[k] = v
+ #Save childrens
+ for child in node.getchildren():
+ tag, tree = self._namespace_split(child.tag, self._parse_node(child))
+ if tag not in node_tree: # the first time, so store it in dict
+ node_tree[tag] = tree
+ continue
+ old = node_tree[tag]
+ if not isinstance(old, list):
+ node_tree.pop(tag)
+ node_tree[tag] = [old] # multi times, so change old dict to a list
+ node_tree[tag].append(tree) # add the new one
+
+ return node_tree
+
+
+ def _namespace_split(self, tag, value):
+ """
+ Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
+ ns = http://cs.sfsu.edu/csc867/myscheduler
+ name = patients
+ """
+ result = re.compile("\{(.*)\}(.*)").search(tag)
+ if result:
+ 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())
+
+ def fromstring(self, s):
+ """parse a string"""
+ t = ET.fromstring(s)
+ unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
+ return root_tree
+
+
+def xml2dict(input):
+ return XML2Dict().fromstring(input)
+
+
+# Piston:
+class XMLRenderer():
+ def _to_xml(self, xml, data):
+ if isinstance(data, (list, tuple)):
+ for item in data:
+ xml.startElement("list-item", {})
+ self._to_xml(xml, item)
+ xml.endElement("list-item")
+
+ elif isinstance(data, dict):
+ for key, value in data.iteritems():
+ xml.startElement(key, {})
+ self._to_xml(xml, value)
+ xml.endElement(key)
+
+ else:
+ xml.characters(smart_unicode(data))
+
+ def dict2xml(self, data):
+ stream = StringIO.StringIO()
+
+ xml = SimplerXMLGenerator(stream, "utf-8")
+ xml.startDocument()
+ xml.startElement("root", {})
+
+ self._to_xml(xml, data)
+
+ xml.endElement("root")
+ xml.endDocument()
+ return stream.getvalue()
+
+def dict2xml(input):
+ return XMLRenderer().dict2xml(input)
diff --git a/djangorestframework/utils/breadcrumbs.py b/djangorestframework/utils/breadcrumbs.py
new file mode 100644
index 00000000..1e604efc
--- /dev/null
+++ b/djangorestframework/utils/breadcrumbs.py
@@ -0,0 +1,30 @@
+from django.core.urlresolvers import resolve
+from djangorestframework.utils.description import get_name
+
+def get_breadcrumbs(url):
+ """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
+
+ def breadcrumbs_recursive(url, breadcrumbs_list):
+ """Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
+
+ try:
+ (view, unused_args, unused_kwargs) = resolve(url)
+ except:
+ pass
+ else:
+ if callable(view):
+ breadcrumbs_list.insert(0, (get_name(view), url))
+
+ if url == '':
+ # All done
+ return breadcrumbs_list
+
+ elif url.endswith('/'):
+ # Drop trailing slash off the end and continue to try to resolve more breadcrumbs
+ return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
+
+ # Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
+ return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
+
+ return breadcrumbs_recursive(url, [])
+
diff --git a/djangorestframework/utils/description.py b/djangorestframework/utils/description.py
new file mode 100644
index 00000000..f7145c0f
--- /dev/null
+++ b/djangorestframework/utils/description.py
@@ -0,0 +1,37 @@
+"""Get a descriptive name and description for a view,
+based on class name and docstring, and override-able by 'name' and 'description' attributes"""
+import re
+
+def get_name(view):
+ """Return a name for the view.
+
+ If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
+ if getattr(view, 'name', None) is not None:
+ return view.name
+
+ if getattr(view, '__name__', None) is not None:
+ name = view.__name__
+ elif getattr(view, '__class__', None) is not None: # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
+ name = view.__class__.__name__
+ else:
+ return ''
+
+ return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
+
+def get_description(view):
+ """Provide a description for the view.
+
+ By default this is the view's docstring with nice unindention applied."""
+ if getattr(view, 'description', None) is not None:
+ return getattr(view, 'description')
+
+ if getattr(view, '__doc__', None) is not None:
+ whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]
+
+ if whitespace_counts:
+ whitespace_pattern = '^' + (' ' * min(whitespace_counts))
+ return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)
+
+ return view.__doc__
+
+ return ''
\ No newline at end of file
diff --git a/djangorestframework/utils/mediatypes.py b/djangorestframework/utils/mediatypes.py
new file mode 100644
index 00000000..92d9264c
--- /dev/null
+++ b/djangorestframework/utils/mediatypes.py
@@ -0,0 +1,78 @@
+"""
+Handling of media types, as found in HTTP Content-Type and Accept headers.
+
+See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
+"""
+
+from django.http.multipartparser import parse_header
+
+
+class MediaType(object):
+ def __init__(self, media_type_str):
+ self.orig = media_type_str
+ self.media_type, self.params = parse_header(media_type_str)
+ self.main_type, sep, self.sub_type = self.media_type.partition('/')
+
+ def match(self, other):
+ """Return true if this MediaType satisfies the constraint of the given MediaType."""
+ for key in other.params.keys():
+ if key != 'q' and other.params[key] != self.params.get(key, None):
+ return False
+
+ if other.sub_type != '*' and other.sub_type != self.sub_type:
+ return False
+
+ if other.main_type != '*' and other.main_type != self.main_type:
+ return False
+
+ return True
+
+ def precedence(self):
+ """
+ Return a precedence level for the media type given how specific it is.
+ """
+ if self.main_type == '*':
+ return 1
+ elif self.sub_type == '*':
+ return 2
+ elif not self.params or self.params.keys() == ['q']:
+ return 3
+ return 4
+
+ def quality(self):
+ """
+ Return a quality level for the media type.
+ """
+ try:
+ return Decimal(self.params.get('q', '1.0'))
+ except:
+ return Decimal(0)
+
+ def score(self):
+ """
+ Return an overall score for a given media type given it's quality and precedence.
+ """
+ # NB. quality values should only have up to 3 decimal points
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
+ return self.quality * 10000 + self.precedence
+
+ def is_form(self):
+ """
+ Return True if the MediaType is a valid form media type as defined by the HTML4 spec.
+ (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
+ """
+ return self.media_type == 'application/x-www-form-urlencoded' or \
+ self.media_type == 'multipart/form-data'
+
+ def as_tuple(self):
+ return (self.main_type, self.sub_type, self.params)
+
+ def __repr__(self):
+ return "" % (self.as_tuple(),)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __unicode__(self):
+ return self.orig
+
--
cgit v1.2.3
From 8756664e064a18afc4713d921c318cd968f18433 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 2 May 2011 19:49:12 +0100
Subject: emitters -> renderers
---
djangorestframework/mixins.py | 90 +++++++++++++++++++++++++++++++++-
djangorestframework/modelresource.py | 94 +++---------------------------------
djangorestframework/renderers.py | 2 +-
djangorestframework/resource.py | 10 ++--
examples/blogpost/models.py | 5 +-
examples/blogpost/views.py | 7 ++-
examples/mixin/urls.py | 2 +-
examples/pygments_api/views.py | 2 +-
8 files changed, 111 insertions(+), 101 deletions(-)
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index ebeee31a..6bd83bfa 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -6,6 +6,7 @@ from djangorestframework import status
from django.http import HttpResponse
from django.http.multipartparser import LimitBytes # TODO: Use LimitedStream in compat
+
from StringIO import StringIO
from decimal import Decimal
import re
@@ -233,7 +234,7 @@ class RequestMixin(object):
@property
def default_parser(self):
- """Return the view's most preffered renderer.
+ """Return the view's most preferred renderer.
(This has no behavioural effect, but is may be used by documenting renderers)"""
return self.parsers[0]
@@ -437,3 +438,90 @@ class AuthMixin(object):
'You may need to login or otherwise authenticate the request.'})
+########## Model Mixins ##########
+
+class ReadModelMixin(object):
+ """Behaviour to read a model instance on GET requests"""
+ def get(self, request, *args, **kwargs):
+ try:
+ if args:
+ # If we have any none kwargs then assume the last represents the primrary key
+ instance = self.model.objects.get(pk=args[-1], **kwargs)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+ except self.model.DoesNotExist:
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND)
+
+ return instance
+
+
+class CreateModelMixin(object):
+ """Behaviour to create a model instance on POST requests"""
+ def post(self, request, *args, **kwargs):
+ # 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)]:
+ if kwargs.has_key(related_name):
+ kwargs[related_name + '_id'] = kwargs[related_name]
+ del kwargs[related_name]
+
+ all_kw_args = dict(self.CONTENT.items() + kwargs.items())
+ if args:
+ instance = self.model(pk=args[-1], **all_kw_args)
+ else:
+ instance = self.model(**all_kw_args)
+ instance.save()
+ headers = {}
+ if hasattr(instance, 'get_absolute_url'):
+ headers['Location'] = instance.get_absolute_url()
+ return Response(status.HTTP_201_CREATED, instance, headers)
+
+
+class UpdateModelMixin(object):
+ """Behaviour to update a model instance on PUT requests"""
+ def put(self, request, *args, **kwargs):
+ # 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:
+ if args:
+ # If we have any none kwargs then assume the last represents the primrary key
+ instance = self.model.objects.get(pk=args[-1], **kwargs)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+
+ for (key, val) in self.CONTENT.items():
+ setattr(instance, key, val)
+ except self.model.DoesNotExist:
+ instance = self.model(**self.CONTENT)
+ instance.save()
+
+ instance.save()
+ return instance
+
+
+class DeleteModelMixin(object):
+ """Behaviour to delete a model instance on DELETE requests"""
+ def delete(self, request, *args, **kwargs):
+ try:
+ if args:
+ # If we have any none kwargs then assume the last represents the primrary key
+ instance = self.model.objects.get(pk=args[-1], **kwargs)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+ except self.model.DoesNotExist:
+ raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
+
+ instance.delete()
+ return
+
+
+class ListModelMixin(object):
+ """Behaviour to list a set of model instances on GET requests"""
+ queryset = None
+
+ def get(self, request, *args, **kwargs):
+ queryset = self.queryset if self.queryset else self.model.objects.all()
+ return queryset.filter(**kwargs)
+
+
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
index c61cc375..c286e586 100644
--- a/djangorestframework/modelresource.py
+++ b/djangorestframework/modelresource.py
@@ -341,94 +341,16 @@ class ModelResource(Resource):
return _any(data, self.fields)
- def get(self, request, *args, **kwargs):
- try:
- if args:
- # If we have any none kwargs then assume the last represents the primrary key
- instance = self.model.objects.get(pk=args[-1], **kwargs)
- else:
- # Otherwise assume the kwargs uniquely identify the model
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
- raise ErrorResponse(status.HTTP_404_NOT_FOUND)
-
- return instance
-
- def post(self, request, *args, **kwargs):
- # TODO: test creation on a non-existing resource url
-
- # translated related_field into related_field_id
- for related_name in [field.name for field in self.model._meta.fields if isinstance(field, RelatedField)]:
- if kwargs.has_key(related_name):
- kwargs[related_name + '_id'] = kwargs[related_name]
- del kwargs[related_name]
-
- all_kw_args = dict(self.CONTENT.items() + kwargs.items())
- if args:
- instance = self.model(pk=args[-1], **all_kw_args)
- else:
- instance = self.model(**all_kw_args)
- instance.save()
- headers = {}
- if hasattr(instance, 'get_absolute_url'):
- headers['Location'] = instance.get_absolute_url()
- return Response(status.HTTP_201_CREATED, instance, headers)
-
- def put(self, request, *args, **kwargs):
- # 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:
- if args:
- # If we have any none kwargs then assume the last represents the primrary key
- instance = self.model.objects.get(pk=args[-1], **kwargs)
- else:
- # Otherwise assume the kwargs uniquely identify the model
- instance = self.model.objects.get(**kwargs)
-
- for (key, val) in self.CONTENT.items():
- setattr(instance, key, val)
- except self.model.DoesNotExist:
- instance = self.model(**self.CONTENT)
- instance.save()
-
- instance.save()
- return instance
-
- def delete(self, request, *args, **kwargs):
- try:
- if args:
- # If we have any none kwargs then assume the last represents the primrary key
- instance = self.model.objects.get(pk=args[-1], **kwargs)
- else:
- # Otherwise assume the kwargs uniquely identify the model
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
- raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
-
- instance.delete()
- return
-class InstanceModelResource(ModelResource):
- http_method_names = ['get', 'put', 'delete', 'head', 'options', 'trace', 'patch'] # Bit of a hack, these - needs fixing.
+class InstanceModelResource(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelResource):
+ """A view which provides default operations for read/update/delete against a model instance."""
+ pass
-class RootModelResource(ModelResource):
+class ListOrCreateModelResource(CreateModelMixin, ListModelMixin, ModelResource):
"""A Resource which provides default operations for list and create."""
- queryset = None
-
- def get(self, request, *args, **kwargs):
- queryset = self.queryset if self.queryset else self.model.objects.all()
- return queryset.filter(**kwargs)
-
- http_method_names = ['get', 'post', 'head', 'options', 'trace', 'patch']
-
-class QueryModelResource(ModelResource):
- """Resource with default operations for list.
- TODO: provide filter/order/num_results/paging, and a create operation to create queries."""
- allowed_methods = ('GET',)
- queryset = None
-
- def get(self, request, *args, **kwargs):
- queryset = self.queryset if self.queryset else self.model.objects.all()
- return queryset.filer(**kwargs)
+ pass
- http_method_names = ['get', 'head', 'options', 'trace', 'patch']
+class ListModelResource(ListModelMixin, ModelResource):
+ """Resource with default operations for list."""
+ pass
\ No newline at end of file
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index e53dc061..2a07894e 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -43,7 +43,7 @@ class BaseRenderer(object):
class TemplateRenderer(BaseRenderer):
"""Provided for convienience.
- Emit the output by simply rendering it with the given template."""
+ Render the output by simply rendering it with the given template."""
media_type = None
template = None
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 7879da7c..e06873ae 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -19,14 +19,12 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
"""Handles incoming requests and maps them to REST operations.
Performs request deserialization, response serialization, authentication and input validation."""
- http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace', 'patch']
-
# List of renderers the resource can serialize the response with, ordered by preference.
renderers = ( renderers.JSONRenderer,
- renderers.DocumentingHTMLRenderer,
- renderers.DocumentingXHTMLRenderer,
- renderers.DocumentingPlainTextRenderer,
- renderers.XMLRenderer )
+ renderers.DocumentingHTMLRenderer,
+ renderers.DocumentingXHTMLRenderer,
+ renderers.DocumentingPlainTextRenderer,
+ renderers.XMLRenderer )
# List of parsers the resource can parse the request with.
parsers = ( parsers.JSONParser,
diff --git a/examples/blogpost/models.py b/examples/blogpost/models.py
index 01a91e15..3489c596 100644
--- a/examples/blogpost/models.py
+++ b/examples/blogpost/models.py
@@ -12,6 +12,8 @@ RATING_CHOICES = ((0, 'Awful'),
(3, 'Good'),
(4, 'Excellent'))
+MAX_POSTS = 10
+
class BlogPost(models.Model):
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
title = models.CharField(max_length=128)
@@ -38,9 +40,10 @@ class BlogPost(models.Model):
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(self.__class__, self).save(*args, **kwargs)
- for obj in self.__class__.objects.order_by('-pk')[10:]:
+ for obj in self.__class__.objects.order_by('-pk')[MAX_POSTS:]:
obj.delete()
+
class Comment(models.Model):
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
username = models.CharField(max_length=128)
diff --git a/examples/blogpost/views.py b/examples/blogpost/views.py
index e47f4a5b..c4b54f73 100644
--- a/examples/blogpost/views.py
+++ b/examples/blogpost/views.py
@@ -1,12 +1,11 @@
-from djangorestframework.modelresource import InstanceModelResource, RootModelResource
+from djangorestframework.modelresource import InstanceModelResource, ListOrCreateModelResource
from blogpost import models
BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
COMMENT_FIELDS = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
-MAX_POSTS = 10
-class BlogPosts(RootModelResource):
+class BlogPosts(ListOrCreateModelResource):
"""A resource with which lists all existing blog posts and creates new blog posts."""
model = models.BlogPost
fields = BLOG_POST_FIELDS
@@ -16,7 +15,7 @@ class BlogPostInstance(InstanceModelResource):
model = models.BlogPost
fields = BLOG_POST_FIELDS
-class Comments(RootModelResource):
+class Comments(ListOrCreateModelResource):
"""A resource which lists all existing comments for a given blog post, and creates new blog comments for a given blog post."""
model = models.Comment
fields = COMMENT_FIELDS
diff --git a/examples/mixin/urls.py b/examples/mixin/urls.py
index f4300f41..1d25f6c7 100644
--- a/examples/mixin/urls.py
+++ b/examples/mixin/urls.py
@@ -15,7 +15,7 @@ class ExampleView(ResponseMixin, View):
def get(self, request):
response = Response(200, {'description': 'Some example content',
'url': reverse('mixin-view')})
- return self.emit(response)
+ return self.render(response)
urlpatterns = patterns('',
diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py
index 278e8250..253b0907 100644
--- a/examples/pygments_api/views.py
+++ b/examples/pygments_api/views.py
@@ -68,7 +68,7 @@ class PygmentsRoot(Resource):
class PygmentsInstance(Resource):
"""Simply return the stored highlighted HTML file with the correct mime type.
- This Resource only emits HTML and uses a standard HTML renderer rather than the renderers.DocumentingHTMLRenderer class."""
+ This Resource only renders HTML and uses a standard HTML renderer rather than the renderers.DocumentingHTMLRenderer class."""
renderers = (HTMLRenderer,)
def get(self, request, unique_id):
--
cgit v1.2.3
From d373b3a067796b8e181be9368fa24e89c572c45e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 4 May 2011 09:21:17 +0100
Subject: Decouple views and resources
---
djangorestframework/authentication.py | 6 +-
djangorestframework/mixins.py | 78 ++----
djangorestframework/modelresource.py | 356 --------------------------
djangorestframework/permissions.py | 6 +
djangorestframework/renderers.py | 2 +-
djangorestframework/resource.py | 367 ++++++++++++++++++---------
djangorestframework/response.py | 2 +-
djangorestframework/templates/api_login.html | 2 +-
djangorestframework/tests/accept.py | 10 +-
djangorestframework/tests/authentication.py | 6 +-
djangorestframework/tests/breadcrumbs.py | 12 +-
djangorestframework/tests/description.py | 28 +-
djangorestframework/tests/files.py | 6 +-
djangorestframework/tests/parsers.py | 22 +-
djangorestframework/tests/reverse.py | 8 +-
djangorestframework/tests/throttling.py | 6 +-
djangorestframework/tests/validators.py | 29 ++-
djangorestframework/tests/views.py | 2 +-
djangorestframework/urls.py | 16 ++
djangorestframework/utils/staticviews.py | 65 +++++
djangorestframework/validators.py | 16 +-
djangorestframework/views.py | 213 +++++++++++-----
examples/modelresourceexample/views.py | 6 +-
examples/urls.py | 15 +-
24 files changed, 592 insertions(+), 687 deletions(-)
delete mode 100644 djangorestframework/modelresource.py
create mode 100644 djangorestframework/urls.py
create mode 100644 djangorestframework/utils/staticviews.py
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py
index 894b34fc..9dd5c958 100644
--- a/djangorestframework/authentication.py
+++ b/djangorestframework/authentication.py
@@ -1,6 +1,6 @@
"""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.
"""
@@ -25,10 +25,10 @@ class BaseAuthenticator(object):
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.
- 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.
- 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
checking at the point the response is being generated.
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 6bd83bfa..467ce0e0 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -1,3 +1,4 @@
+""""""
from djangorestframework.utils.mediatypes import MediaType
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
from djangorestframework.response import ErrorResponse
@@ -12,6 +13,14 @@ from decimal import Decimal
import re
+__all__ = ['RequestMixin',
+ 'ResponseMixin',
+ 'AuthMixin',
+ 'ReadModelMixin',
+ 'CreateModelMixin',
+ 'UpdateModelMixin',
+ 'DeleteModelMixin',
+ 'ListModelMixin']
########## Request Mixin ##########
@@ -250,7 +259,7 @@ class RequestMixin(object):
########## ResponseMixin ##########
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.
Also supports overidding the content type by specifying an _accept= parameter in the URL.
@@ -259,32 +268,8 @@ class ResponseMixin(object):
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
REWRITE_IE_ACCEPT_HEADER = True
- #request = None
- #response = None
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):
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
@@ -318,7 +303,7 @@ class ResponseMixin(object):
def _determine_renderer(self, request):
"""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"""
@@ -415,17 +400,6 @@ class AuthMixin(object):
return auth
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):
if not self.permissions:
return
@@ -443,14 +417,15 @@ class AuthMixin(object):
class ReadModelMixin(object):
"""Behaviour to read a model instance on GET requests"""
def get(self, request, *args, **kwargs):
+ model = self.resource.model
try:
if args:
# 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:
# Otherwise assume the kwargs uniquely identify the model
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
+ instance = model.objects.get(**kwargs)
+ except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
return instance
@@ -459,17 +434,18 @@ class ReadModelMixin(object):
class CreateModelMixin(object):
"""Behaviour to create a model instance on POST requests"""
def post(self, request, *args, **kwargs):
+ model = self.resource.model
# 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):
kwargs[related_name + '_id'] = kwargs[related_name]
del kwargs[related_name]
all_kw_args = dict(self.CONTENT.items() + kwargs.items())
if args:
- instance = self.model(pk=args[-1], **all_kw_args)
+ instance = model(pk=args[-1], **all_kw_args)
else:
- instance = self.model(**all_kw_args)
+ instance = model(**all_kw_args)
instance.save()
headers = {}
if hasattr(instance, 'get_absolute_url'):
@@ -480,19 +456,20 @@ class CreateModelMixin(object):
class UpdateModelMixin(object):
"""Behaviour to update a model instance on PUT requests"""
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
try:
if args:
# 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:
# 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():
setattr(instance, key, val)
- except self.model.DoesNotExist:
- instance = self.model(**self.CONTENT)
+ except model.DoesNotExist:
+ instance = model(**self.CONTENT)
instance.save()
instance.save()
@@ -502,14 +479,15 @@ class UpdateModelMixin(object):
class DeleteModelMixin(object):
"""Behaviour to delete a model instance on DELETE requests"""
def delete(self, request, *args, **kwargs):
+ model = self.resource.model
try:
if args:
# 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:
# Otherwise assume the kwargs uniquely identify the model
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
+ instance = model.objects.get(**kwargs)
+ except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
instance.delete()
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
deleted file mode 100644
index c286e586..00000000
--- a/djangorestframework/modelresource.py
+++ /dev/null
@@ -1,356 +0,0 @@
-from django.forms import ModelForm
-from django.db.models import Model
-from django.db.models.query import QuerySet
-from django.db.models.fields.related import RelatedField
-
-from djangorestframework.response import Response, ErrorResponse
-from djangorestframework.resource import Resource
-from djangorestframework import status, validators
-
-import decimal
-import inspect
-import re
-
-
-class ModelResource(Resource):
- """A specialized type of Resource, for resources that map directly to a Django Model.
- Useful things this provides:
-
- 0. Default input validation based on ModelForms.
- 1. Nice serialization of returned Models and QuerySets.
- 2. A default set of create/read/update/delete operations."""
-
- # List of validators to validate, cleanup and type-ify the request content
- validators = (validators.ModelFormValidator,)
-
- # The model attribute refers to the Django Model which this Resource maps to.
- # (The Model's class, rather than an instance of the Model)
- model = None
-
- # By default the set of returned fields will be the set of:
- #
- # 0. All the fields on the model, excluding 'id'.
- # 1. All the properties on the model.
- # 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
- #
- # If you wish to override this behaviour,
- # you should explicitly set the fields attribute on your class.
- fields = None
-
- # By default the form used with be a ModelForm for self.model
- # If you wish to override this behaviour or provide a sub-classed ModelForm
- # you should explicitly set the form attribute on your class.
- form = None
-
- # By default the set of input fields will be the same as the set of output fields
- # If you wish to override this behaviour you should explicitly set the
- # form_fields attribute on your class.
- #form_fields = None
-
-
- #def get_form(self, content=None):
- # """Return a form that may be used in validation and/or rendering an html renderer"""
- # if self.form:
- # return super(self.__class__, self).get_form(content)
- #
- # elif self.model:
- #
- # class NewModelForm(ModelForm):
- # class Meta:
- # model = self.model
- # fields = self.form_fields if self.form_fields else None
- #
- # if content and isinstance(content, Model):
- # return NewModelForm(instance=content)
- # elif content:
- # return NewModelForm(content)
- #
- # return NewModelForm()
- #
- # return None
-
-
- #def cleanup_request(self, data, form_instance):
- # """Override cleanup_request to drop read-only fields from the input prior to validation.
- # This ensures that we don't error out with 'non-existent field' when these fields are supplied,
- # and allows for a pragmatic approach to resources which include read-only elements.
- #
- # I would actually like to be strict and verify the value of correctness of the values in these fields,
- # although that gets tricky as it involves validating at the point that we get the model instance.
- #
- # See here for another example of this approach:
- # http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide
- # https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041"""
- # read_only_fields = set(self.fields) - set(self.form_instance.fields)
- # input_fields = set(data.keys())
- #
- # clean_data = {}
- # for key in input_fields - read_only_fields:
- # clean_data[key] = data[key]
- #
- # return super(ModelResource, self).cleanup_request(clean_data, form_instance)
-
-
- def cleanup_response(self, data):
- """A munging of Piston's pre-serialization. Returns a dict"""
-
- def _any(thing, fields=()):
- """
- Dispatch, all types are routed through here.
- """
- ret = None
-
- if isinstance(thing, QuerySet):
- ret = _qs(thing, fields=fields)
- elif isinstance(thing, (tuple, list)):
- ret = _list(thing)
- elif isinstance(thing, dict):
- ret = _dict(thing)
- elif isinstance(thing, int):
- ret = thing
- elif isinstance(thing, bool):
- ret = thing
- elif isinstance(thing, type(None)):
- ret = thing
- elif isinstance(thing, decimal.Decimal):
- ret = str(thing)
- elif isinstance(thing, Model):
- ret = _model(thing, fields=fields)
- #elif isinstance(thing, HttpResponse): TRC
- # raise HttpStatusCode(thing)
- elif inspect.isfunction(thing):
- if not inspect.getargspec(thing)[0]:
- ret = _any(thing())
- elif hasattr(thing, '__rendertable__'):
- f = thing.__rendertable__
- if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
- ret = _any(f())
- else:
- ret = unicode(thing) # TRC TODO: Change this back!
-
- return ret
-
- def _fk(data, field):
- """
- Foreign keys.
- """
- return _any(getattr(data, field.name))
-
- def _related(data, fields=()):
- """
- Foreign keys.
- """
- return [ _model(m, fields) for m in data.iterator() ]
-
- def _m2m(data, field, fields=()):
- """
- Many to many (re-route to `_model`.)
- """
- return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
-
-
- def _method_fields(data, fields):
- if not data:
- return { }
-
- has = dir(data)
- ret = dict()
-
- for field in fields:
- if field in has:
- ret[field] = getattr(data, field)
-
- return ret
-
- def _model(data, fields=()):
- """
- Models. Will respect the `fields` and/or
- `exclude` on the handler (see `typemapper`.)
- """
- ret = { }
- #handler = self.in_typemapper(type(data), self.anonymous) # TRC
- handler = None # TRC
- get_absolute_url = False
-
- if handler or fields:
- v = lambda f: getattr(data, f.attname)
-
- if not fields:
- """
- Fields was not specified, try to find teh correct
- version in the typemapper we were sent.
- """
- mapped = self.in_typemapper(type(data), self.anonymous)
- get_fields = set(mapped.fields)
- exclude_fields = set(mapped.exclude).difference(get_fields)
-
- if not get_fields:
- get_fields = set([ f.attname.replace("_id", "", 1)
- for f in data._meta.fields ])
-
- # sets can be negated.
- for exclude in exclude_fields:
- if isinstance(exclude, basestring):
- get_fields.discard(exclude)
-
- elif isinstance(exclude, re._pattern_type):
- for field in get_fields.copy():
- if exclude.match(field):
- get_fields.discard(field)
-
- get_absolute_url = True
-
- else:
- get_fields = set(fields)
- if 'absolute_url' in get_fields: # MOVED (TRC)
- get_absolute_url = True
-
- met_fields = _method_fields(handler, get_fields) # TRC
-
- for f in data._meta.local_fields:
- if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
- if not f.rel:
- if f.attname in get_fields:
- ret[f.attname] = _any(v(f))
- get_fields.remove(f.attname)
- else:
- if f.attname[:-3] in get_fields:
- ret[f.name] = _fk(data, f)
- get_fields.remove(f.name)
-
- for mf in data._meta.many_to_many:
- if mf.serialize and mf.attname not in met_fields:
- if mf.attname in get_fields:
- ret[mf.name] = _m2m(data, mf)
- get_fields.remove(mf.name)
-
- # try to get the remainder of fields
- for maybe_field in get_fields:
-
- if isinstance(maybe_field, (list, tuple)):
- model, fields = maybe_field
- inst = getattr(data, model, None)
-
- if inst:
- if hasattr(inst, 'all'):
- ret[model] = _related(inst, fields)
- elif callable(inst):
- if len(inspect.getargspec(inst)[0]) == 1:
- ret[model] = _any(inst(), fields)
- else:
- ret[model] = _model(inst, fields)
-
- elif maybe_field in met_fields:
- # Overriding normal field which has a "resource method"
- # so you can alter the contents of certain fields without
- # using different names.
- ret[maybe_field] = _any(met_fields[maybe_field](data))
-
- else:
- maybe = getattr(data, maybe_field, None)
- if maybe:
- if callable(maybe):
- if len(inspect.getargspec(maybe)[0]) == 1:
- ret[maybe_field] = _any(maybe())
- else:
- ret[maybe_field] = _any(maybe)
- else:
- pass # TRC
- #handler_f = getattr(handler or self.handler, maybe_field, None)
- #
- #if handler_f:
- # ret[maybe_field] = _any(handler_f(data))
-
- else:
- # Add absolute_url if it exists
- get_absolute_url = True
-
- # Add all the fields
- for f in data._meta.fields:
- if f.attname != 'id':
- ret[f.attname] = _any(getattr(data, f.attname))
-
- # Add all the propertiess
- klass = data.__class__
- for attr in dir(klass):
- if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
- #if attr.endswith('_url') or attr.endswith('_uri'):
- # ret[attr] = self.make_absolute(_any(getattr(data, attr)))
- #else:
- ret[attr] = _any(getattr(data, attr))
- #fields = dir(data.__class__) + ret.keys()
- #add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
- #print add_ons
- ###print dir(data.__class__)
- #from django.db.models import Model
- #model_fields = dir(Model)
-
- #for attr in dir(data):
- ## #if attr.startswith('_'):
- ## # continue
- # if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
- # print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
-
- #for k in add_ons:
- # ret[k] = _any(getattr(data, k))
-
- # TRC
- # resouce uri
- #if self.in_typemapper(type(data), self.anonymous):
- # handler = self.in_typemapper(type(data), self.anonymous)
- # if hasattr(handler, 'resource_uri'):
- # url_id, fields = handler.resource_uri()
- # ret['resource_uri'] = permalink( lambda: (url_id,
- # (getattr(data, f) for f in fields) ) )()
-
- # TRC
- #if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
- # try: ret['resource_uri'] = data.get_api_url()
- # except: pass
-
- # absolute uri
- if hasattr(data, 'get_absolute_url') and get_absolute_url:
- try: ret['absolute_url'] = data.get_absolute_url()
- except: pass
-
- #for key, val in ret.items():
- # if key.endswith('_url') or key.endswith('_uri'):
- # ret[key] = self.add_domain(val)
-
- return ret
-
- def _qs(data, fields=()):
- """
- Querysets.
- """
- return [ _any(v, fields) for v in data ]
-
- def _list(data):
- """
- Lists.
- """
- return [ _any(v) for v in data ]
-
- def _dict(data):
- """
- Dictionaries.
- """
- return dict([ (k, _any(v)) for k, v in data.iteritems() ])
-
- # Kickstart the seralizin'.
- return _any(data, self.fields)
-
-
-
-
-class InstanceModelResource(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelResource):
- """A view which provides default operations for read/update/delete against a model instance."""
- pass
-
-class ListOrCreateModelResource(CreateModelMixin, ListModelMixin, ModelResource):
- """A Resource which provides default operations for list and create."""
- pass
-
-class ListModelResource(ListModelMixin, ModelResource):
- """Resource with default operations for list."""
- pass
\ No newline at end of file
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
index 98d4b0be..d98651e0 100644
--- a/djangorestframework/permissions.py
+++ b/djangorestframework/permissions.py
@@ -11,6 +11,12 @@ class BasePermission(object):
def has_permission(self, auth):
return True
+
+class FullAnonAccess(BasePermission):
+ """"""
+ def has_permission(self, auth):
+ return True
+
class IsAuthenticated(BasePermission):
""""""
def has_permission(self, auth):
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index 2a07894e..9e4e2053 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -1,4 +1,4 @@
-"""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,
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.
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index e06873ae..04442498 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -1,130 +1,251 @@
-from django.core.urlresolvers import set_script_prefix
-from django.views.decorators.csrf import csrf_exempt
+from django.db.models import Model
+from django.db.models.query import QuerySet
+from django.db.models.fields.related import RelatedField
-from djangorestframework.compat import View
-from djangorestframework.response import Response, ErrorResponse
-from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
-from djangorestframework import renderers, parsers, authentication, permissions, validators, status
+import decimal
+import inspect
+import re
-# TODO: Figure how out references and named urls need to work nicely
-# TODO: POST on existing 404 URL, PUT on existing 404 URL
-#
-# NEXT: Exceptions on func() -> 500, tracebacks renderted if settings.DEBUG
-
-__all__ = ['Resource']
-
-
-class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
- """Handles incoming requests and maps them to REST operations.
- Performs request deserialization, response serialization, authentication and input validation."""
-
- # List of renderers the resource can serialize the response with, ordered by preference.
- renderers = ( renderers.JSONRenderer,
- renderers.DocumentingHTMLRenderer,
- renderers.DocumentingXHTMLRenderer,
- renderers.DocumentingPlainTextRenderer,
- renderers.XMLRenderer )
-
- # List of parsers the resource can parse the request with.
- parsers = ( parsers.JSONParser,
- parsers.FormParser,
- parsers.MultipartParser )
-
- # List of validators to validate, cleanup and normalize the request content
- validators = ( validators.FormValidator, )
-
- # List of all authenticating methods to attempt.
- authentication = ( authentication.UserLoggedInAuthenticator,
- authentication.BasicAuthenticator )
-
- # List of all permissions required to access the resource
- permissions = ()
-
- # Optional form for input validation and presentation of HTML formatted responses.
- form = None
-
- # Allow name and description for the Resource to be set explicitly,
- # overiding the default classname/docstring behaviour.
- # These are used for documentation in the standard html and text renderers.
- name = None
- description = None
-
- @property
- def allowed_methods(self):
- return [method.upper() for method in self.http_method_names if hasattr(self, method)]
-
- def http_method_not_allowed(self, request, *args, **kwargs):
- """Return an HTTP 405 error if an operation is called which does not have a handler method."""
- raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
- {'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
-
-
- def cleanup_response(self, data):
- """Perform any resource-specific data filtering prior to the standard HTTP
- content-type serialization.
-
- Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
-
- TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into
- the RendererMixin and Renderer classes."""
- return data
-
-
- # Note: session based authentication is explicitly CSRF validated,
- # all other authentication is CSRF exempt.
- @csrf_exempt
- def dispatch(self, request, *args, **kwargs):
- try:
- self.request = request
- self.args = args
- self.kwargs = kwargs
-
- # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
- prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
- set_script_prefix(prefix)
-
- try:
- # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
- # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
- self.perform_form_overloading()
-
- # Authenticate and check request is has the relevant permissions
- self.check_permissions()
-
- # Get the appropriate handler method
- if self.method.lower() in self.http_method_names:
- handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
- else:
- handler = self.http_method_not_allowed
-
- response_obj = handler(request, *args, **kwargs)
-
- # Allow return value to be either Response, or an object, or None
- if isinstance(response_obj, Response):
- response = response_obj
- elif response_obj is not None:
- response = Response(status.HTTP_200_OK, response_obj)
- else:
- response = Response(status.HTTP_204_NO_CONTENT)
-
- # Pre-serialize filtering (eg filter complex objects into natively serializable types)
- response.cleaned_content = self.cleanup_response(response.raw_content)
+class Resource(object):
+ """A Resource determines how an object maps to a serializable entity.
+ Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets."""
- except ErrorResponse, exc:
- response = exc.response
+ # The model attribute refers to the Django Model which this Resource maps to.
+ # (The Model's class, rather than an instance of the Model)
+ model = None
- # Always add these headers.
- #
- # TODO - this isn't actually the correct way to set the vary header,
- # also it's currently sub-obtimal for HTTP caching - need to sort that out.
- response.headers['Allow'] = ', '.join(self.allowed_methods)
- response.headers['Vary'] = 'Authenticate, Accept'
-
- return self.render(response)
- except:
- import traceback
- traceback.print_exc()
-
+ # By default the set of returned fields will be the set of:
+ #
+ # 0. All the fields on the model, excluding 'id'.
+ # 1. All the properties on the model.
+ # 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
+ #
+ # If you wish to override this behaviour,
+ # you should explicitly set the fields attribute on your class.
+ fields = None
+
+ @classmethod
+ def object_to_serializable(self, data):
+ """A (horrible) munging of Piston's pre-serialization. Returns a dict"""
+
+ def _any(thing, fields=()):
+ """
+ Dispatch, all types are routed through here.
+ """
+ ret = None
+
+ if isinstance(thing, QuerySet):
+ ret = _qs(thing, fields=fields)
+ elif isinstance(thing, (tuple, list)):
+ ret = _list(thing)
+ elif isinstance(thing, dict):
+ ret = _dict(thing)
+ elif isinstance(thing, int):
+ ret = thing
+ elif isinstance(thing, bool):
+ ret = thing
+ elif isinstance(thing, type(None)):
+ ret = thing
+ elif isinstance(thing, decimal.Decimal):
+ ret = str(thing)
+ elif isinstance(thing, Model):
+ ret = _model(thing, fields=fields)
+ #elif isinstance(thing, HttpResponse): TRC
+ # raise HttpStatusCode(thing)
+ elif inspect.isfunction(thing):
+ if not inspect.getargspec(thing)[0]:
+ ret = _any(thing())
+ elif hasattr(thing, '__rendertable__'):
+ f = thing.__rendertable__
+ if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
+ ret = _any(f())
+ else:
+ ret = unicode(thing) # TRC TODO: Change this back!
+
+ return ret
+
+ def _fk(data, field):
+ """
+ Foreign keys.
+ """
+ return _any(getattr(data, field.name))
+
+ def _related(data, fields=()):
+ """
+ Foreign keys.
+ """
+ return [ _model(m, fields) for m in data.iterator() ]
+
+ def _m2m(data, field, fields=()):
+ """
+ Many to many (re-route to `_model`.)
+ """
+ return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
+
+ def _method_fields(data, fields):
+ if not data:
+ return { }
+
+ has = dir(data)
+ ret = dict()
+
+ for field in fields:
+ if field in has:
+ ret[field] = getattr(data, field)
+
+ return ret
+
+ def _model(data, fields=()):
+ """
+ Models. Will respect the `fields` and/or
+ `exclude` on the handler (see `typemapper`.)
+ """
+ ret = { }
+ #handler = self.in_typemapper(type(data), self.anonymous) # TRC
+ handler = None # TRC
+ get_absolute_url = False
+
+ if fields:
+ v = lambda f: getattr(data, f.attname)
+
+ get_fields = set(fields)
+ if 'absolute_url' in get_fields: # MOVED (TRC)
+ get_absolute_url = True
+
+ met_fields = _method_fields(handler, get_fields) # TRC
+
+ for f in data._meta.local_fields:
+ if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
+ if not f.rel:
+ if f.attname in get_fields:
+ ret[f.attname] = _any(v(f))
+ get_fields.remove(f.attname)
+ else:
+ if f.attname[:-3] in get_fields:
+ ret[f.name] = _fk(data, f)
+ get_fields.remove(f.name)
+
+ for mf in data._meta.many_to_many:
+ if mf.serialize and mf.attname not in met_fields:
+ if mf.attname in get_fields:
+ ret[mf.name] = _m2m(data, mf)
+ get_fields.remove(mf.name)
+
+ # try to get the remainder of fields
+ for maybe_field in get_fields:
+
+ if isinstance(maybe_field, (list, tuple)):
+ model, fields = maybe_field
+ inst = getattr(data, model, None)
+
+ if inst:
+ if hasattr(inst, 'all'):
+ ret[model] = _related(inst, fields)
+ elif callable(inst):
+ if len(inspect.getargspec(inst)[0]) == 1:
+ ret[model] = _any(inst(), fields)
+ else:
+ ret[model] = _model(inst, fields)
+
+ elif maybe_field in met_fields:
+ # Overriding normal field which has a "resource method"
+ # so you can alter the contents of certain fields without
+ # using different names.
+ ret[maybe_field] = _any(met_fields[maybe_field](data))
+
+ else:
+ maybe = getattr(data, maybe_field, None)
+ if maybe:
+ if callable(maybe):
+ if len(inspect.getargspec(maybe)[0]) == 1:
+ ret[maybe_field] = _any(maybe())
+ else:
+ ret[maybe_field] = _any(maybe)
+ else:
+ pass # TRC
+ #handler_f = getattr(handler or self.handler, maybe_field, None)
+ #
+ #if handler_f:
+ # ret[maybe_field] = _any(handler_f(data))
+
+ else:
+ # Add absolute_url if it exists
+ get_absolute_url = True
+
+ # Add all the fields
+ for f in data._meta.fields:
+ if f.attname != 'id':
+ ret[f.attname] = _any(getattr(data, f.attname))
+
+ # Add all the propertiess
+ klass = data.__class__
+ for attr in dir(klass):
+ if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
+ #if attr.endswith('_url') or attr.endswith('_uri'):
+ # ret[attr] = self.make_absolute(_any(getattr(data, attr)))
+ #else:
+ ret[attr] = _any(getattr(data, attr))
+ #fields = dir(data.__class__) + ret.keys()
+ #add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
+ #print add_ons
+ ###print dir(data.__class__)
+ #from django.db.models import Model
+ #model_fields = dir(Model)
+
+ #for attr in dir(data):
+ ## #if attr.startswith('_'):
+ ## # continue
+ # if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
+ # print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
+
+ #for k in add_ons:
+ # ret[k] = _any(getattr(data, k))
+
+ # TRC
+ # resouce uri
+ #if self.in_typemapper(type(data), self.anonymous):
+ # handler = self.in_typemapper(type(data), self.anonymous)
+ # if hasattr(handler, 'resource_uri'):
+ # url_id, fields = handler.resource_uri()
+ # ret['resource_uri'] = permalink( lambda: (url_id,
+ # (getattr(data, f) for f in fields) ) )()
+
+ # TRC
+ #if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
+ # try: ret['resource_uri'] = data.get_api_url()
+ # except: pass
+
+ # absolute uri
+ if hasattr(data, 'get_absolute_url') and get_absolute_url:
+ try: ret['absolute_url'] = data.get_absolute_url()
+ except: pass
+
+ #for key, val in ret.items():
+ # if key.endswith('_url') or key.endswith('_uri'):
+ # ret[key] = self.add_domain(val)
+
+ return ret
+
+ def _qs(data, fields=()):
+ """
+ Querysets.
+ """
+ return [ _any(v, fields) for v in data ]
+
+ def _list(data):
+ """
+ Lists.
+ """
+ return [ _any(v) for v in data ]
+
+ def _dict(data):
+ """
+ Dictionaries.
+ """
+ return dict([ (k, _any(v)) for k, v in data.iteritems() ])
+
+ # Kickstart the seralizin'.
+ return _any(data, self.fields)
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index 545a5834..9b3c5851 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -21,6 +21,6 @@ class Response(object):
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={}):
self.response = Response(status, content=content, headers=headers)
diff --git a/djangorestframework/templates/api_login.html b/djangorestframework/templates/api_login.html
index ef383a0b..9d06e851 100644
--- a/djangorestframework/templates/api_login.html
+++ b/djangorestframework/templates/api_login.html
@@ -18,7 +18,7 @@