aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework
diff options
context:
space:
mode:
Diffstat (limited to 'djangorestframework')
-rw-r--r--djangorestframework/__init__.py2
-rw-r--r--djangorestframework/compat.py45
-rw-r--r--djangorestframework/mixins.py5
-rw-r--r--djangorestframework/renderers.py8
-rw-r--r--djangorestframework/resources.py5
-rw-r--r--djangorestframework/serializer.py59
-rw-r--r--djangorestframework/templatetags/add_query_param.py4
-rw-r--r--djangorestframework/tests/accept.py10
-rw-r--r--djangorestframework/tests/serializer.py21
-rw-r--r--djangorestframework/views.py3
10 files changed, 118 insertions, 44 deletions
diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py
index efe7f566..46dd608f 100644
--- a/djangorestframework/__init__.py
+++ b/djangorestframework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '0.3.3'
+__version__ = '0.4.0-dev'
VERSION = __version__ # synonym
diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py
index 83d26f1f..0772e17b 100644
--- a/djangorestframework/compat.py
+++ b/djangorestframework/compat.py
@@ -65,15 +65,45 @@ except ImportError:
environ.update(request)
return WSGIRequest(environ)
-# django.views.generic.View (Django >= 1.3)
+# django.views.generic.View (1.3 <= Django < 1.4)
try:
from django.views.generic import View
- if not hasattr(View, 'head'):
+
+ if django.VERSION < (1, 4):
+ from django.utils.decorators import classonlymethod
+ from django.utils.functional import update_wrapper
+
# First implementation of Django class-based views did not include head method
# in base View class - https://code.djangoproject.com/ticket/15668
class ViewPlusHead(View):
- def head(self, request, *args, **kwargs):
- return self.get(request, *args, **kwargs)
+ @classonlymethod
+ def as_view(cls, **initkwargs):
+ """
+ Main entry point for a request-response process.
+ """
+ # sanitize keyword arguments
+ for key in initkwargs:
+ if key in cls.http_method_names:
+ raise TypeError(u"You tried to pass in the %s method name as a "
+ u"keyword argument to %s(). Don't do that."
+ % (key, cls.__name__))
+ if not hasattr(cls, key):
+ raise TypeError(u"%s() received an invalid keyword %r" % (
+ cls.__name__, key))
+
+ def view(request, *args, **kwargs):
+ self = cls(**initkwargs)
+ if hasattr(self, 'get') and not hasattr(self, 'head'):
+ self.head = self.get
+ return self.dispatch(request, *args, **kwargs)
+
+ # take name and docstring from class
+ update_wrapper(view, cls, updated=())
+
+ # and possible attributes set by decorators
+ # like csrf_exempt from dispatch
+ update_wrapper(view, cls.dispatch, assigned=())
+ return view
View = ViewPlusHead
except ImportError:
@@ -121,6 +151,8 @@ except ImportError:
def view(request, *args, **kwargs):
self = cls(**initkwargs)
+ if hasattr(self, 'get') and not hasattr(self, 'head'):
+ self.head = self.get
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
@@ -154,9 +186,6 @@ except ImportError:
#)
return http.HttpResponseNotAllowed(allowed_methods)
- def head(self, request, *args, **kwargs):
- return self.get(request, *args, **kwargs)
-
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware
@@ -370,6 +399,8 @@ else:
# Markdown is optional
try:
import markdown
+ if markdown.version_info < (2, 0):
+ raise ImportError('Markdown < 2.0 is not supported.')
class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
"""
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 6c8f8179..4a453957 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -181,7 +181,7 @@ class RequestMixin(object):
return parser.parse(stream)
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
- {'error': 'Unsupported media type in request \'%s\'.' %
+ {'detail': 'Unsupported media type in request \'%s\'.' %
content_type})
@property
@@ -274,7 +274,8 @@ class ResponseMixin(object):
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
elif (self._IGNORE_IE_ACCEPT_HEADER and
'HTTP_USER_AGENT' in request.META and
- MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
+ MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and
+ request.META.get('HTTP_X_REQUESTED_WITH', '') != 'XMLHttpRequest'):
# Ignore MSIE's broken accept behavior and do something sensible instead
accept_list = ['text/html', '*/*']
elif 'HTTP_ACCEPT' in request.META:
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index d9aa4028..3d01582c 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -182,6 +182,10 @@ class TemplateRenderer(BaseRenderer):
media_type = None
template = None
+ def __init__(self, view):
+ super(TemplateRenderer, self).__init__(view)
+ self.template = getattr(self.view, "template", self.template)
+
def render(self, obj=None, media_type=None):
"""
Renders *obj* using the :attr:`template` specified on the class.
@@ -202,6 +206,10 @@ class DocumentingTemplateRenderer(BaseRenderer):
template = None
+ def __init__(self, view):
+ super(DocumentingTemplateRenderer, self).__init__(view)
+ self.template = getattr(self.view, "template", self.template)
+
def _get_content(self, view, request, obj, media_type):
"""
Get the content as if it had been rendered by a non-documenting renderer.
diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py
index f170eb45..5e350268 100644
--- a/djangorestframework/resources.py
+++ b/djangorestframework/resources.py
@@ -169,8 +169,9 @@ class FormResource(Resource):
)
# Add any unknown field errors
- for key in unknown_fields:
- field_errors[key] = [u'This field does not exist.']
+ if not self.allow_unknown_form_fields:
+ for key in unknown_fields:
+ field_errors[key] = [u'This field does not exist.']
if field_errors:
detail[u'field_errors'] = field_errors
diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py
index b0c02675..3f05903b 100644
--- a/djangorestframework/serializer.py
+++ b/djangorestframework/serializer.py
@@ -2,7 +2,7 @@
Customizable serialization.
"""
from django.db import models
-from django.db.models.query import QuerySet
+from django.db.models.query import QuerySet, RawQuerySet
from django.utils.encoding import smart_unicode, is_protected_type, smart_str
import inspect
@@ -25,16 +25,9 @@ def _field_to_tuple(field):
def _fields_to_list(fields):
"""
- Return a list of field names.
+ Return a list of field tuples.
"""
- return [_field_to_tuple(field)[0] for field in fields or ()]
-
-
-def _fields_to_dict(fields):
- """
- Return a `dict` of field name -> None, or tuple of fields, or Serializer class
- """
- return dict([_field_to_tuple(field) for field in fields or ()])
+ return [_field_to_tuple(field) for field in fields or ()]
class _SkipField(Exception):
@@ -103,6 +96,11 @@ class Serializer(object):
"""
The maximum depth to serialize to, or `None`.
"""
+
+ parent = None
+ """
+ A reference to the root serializer when descending down into fields.
+ """
def __init__(self, depth=None, stack=[], **kwargs):
if depth is not None:
@@ -110,9 +108,6 @@ class Serializer(object):
self.stack = stack
def get_fields(self, obj):
- """
- Return the set of field names/keys to use for a model instance/dict.
- """
fields = self.fields
# If `fields` is not set, we use the default fields and modify
@@ -123,9 +118,6 @@ class Serializer(object):
exclude = self.exclude or ()
fields = set(default + list(include)) - set(exclude)
- else:
- fields = _fields_to_list(self.fields)
-
return fields
def get_default_fields(self, obj):
@@ -139,15 +131,16 @@ class Serializer(object):
else:
return obj.keys()
- def get_related_serializer(self, key):
- info = _fields_to_dict(self.fields).get(key, None)
-
+ def get_related_serializer(self, info):
# If an element in `fields` is a 2-tuple of (str, tuple)
# then the second element of the tuple is the fields to
# set on the related serializer
+
+ class OnTheFlySerializer(self.__class__):
+ fields = info
+ parent = getattr(self, 'parent') or self
+
if isinstance(info, (list, tuple)):
- class OnTheFlySerializer(self.__class__):
- fields = info
return OnTheFlySerializer
# If an element in `fields` is a 2-tuple of (str, Serializer)
@@ -165,8 +158,9 @@ class Serializer(object):
elif isinstance(info, str) and info in _serializers:
return _serializers[info]
- # Otherwise use `related_serializer` or fall back to `Serializer`
- return getattr(self, 'related_serializer') or Serializer
+ # Otherwise use `related_serializer` or fall back to
+ # `OnTheFlySerializer` preserve custom serialization methods.
+ return getattr(self, 'related_serializer') or OnTheFlySerializer
def serialize_key(self, key):
"""
@@ -175,11 +169,11 @@ class Serializer(object):
"""
return self.rename.get(smart_str(key), smart_str(key))
- def serialize_val(self, key, obj):
+ def serialize_val(self, key, obj, related_info):
"""
Convert a model field or dict value into a serializable representation.
"""
- related_serializer = self.get_related_serializer(key)
+ related_serializer = self.get_related_serializer(related_info)
if self.depth is None:
depth = None
@@ -194,7 +188,8 @@ class Serializer(object):
stack = self.stack[:]
stack.append(obj)
- return related_serializer(depth=depth, stack=stack).serialize(obj)
+ return related_serializer(depth=depth, stack=stack).serialize(
+ obj, request=getattr(self, 'request', None))
def serialize_max_depth(self, obj):
"""
@@ -219,7 +214,7 @@ class Serializer(object):
fields = self.get_fields(instance)
# serialize each required field
- for fname in fields:
+ for fname, related_info in _fields_to_list(fields):
try:
# we first check for a method 'fname' on self,
# 'fname's signature must be 'def fname(self, instance)'
@@ -237,7 +232,7 @@ class Serializer(object):
continue
key = self.serialize_key(fname)
- val = self.serialize_val(fname, obj)
+ val = self.serialize_val(fname, obj, related_info)
data[key] = val
except _SkipField:
pass
@@ -268,15 +263,19 @@ class Serializer(object):
"""
return smart_unicode(obj, strings_only=True)
- def serialize(self, obj):
+ def serialize(self, obj, request=None):
"""
Convert any object into a serializable representation.
"""
+ # Request from related serializer.
+ if request is not None:
+ self.request = request
+
if isinstance(obj, (dict, models.Model)):
# Model instances & dictionaries
return self.serialize_model(obj)
- elif isinstance(obj, (tuple, list, set, QuerySet, types.GeneratorType)):
+ elif isinstance(obj, (tuple, list, set, QuerySet, RawQuerySet, types.GeneratorType)):
# basic iterables
return self.serialize_iter(obj)
elif isinstance(obj, models.Manager):
diff --git a/djangorestframework/templatetags/add_query_param.py b/djangorestframework/templatetags/add_query_param.py
index 4cf0133b..143d7b3f 100644
--- a/djangorestframework/templatetags/add_query_param.py
+++ b/djangorestframework/templatetags/add_query_param.py
@@ -4,7 +4,7 @@ register = Library()
def add_query_param(url, param):
- return unicode(URLObject(url).with_query(param))
+ return unicode(URLObject(url).add_query_param(*param.split('=')))
-register.filter('add_query_param', add_query_param)
+register.filter('add_query_param', add_query_param) \ No newline at end of file
diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py
index 21aba589..7f4eb320 100644
--- a/djangorestframework/tests/accept.py
+++ b/djangorestframework/tests/accept.py
@@ -50,6 +50,16 @@ class UserAgentMungingTest(TestCase):
resp = self.view(req)
self.assertEqual(resp['Content-Type'], 'text/html')
+ def test_dont_munge_msie_with_x_requested_with_header(self):
+ """Send MSIE user agent strings, and an X-Requested-With header, and
+ ensure that we get a JSON response if we set a */* Accept header."""
+ for user_agent in (MSIE_9_USER_AGENT,
+ MSIE_8_USER_AGENT,
+ MSIE_7_USER_AGENT):
+ req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ resp = self.view(req)
+ self.assertEqual(resp['Content-Type'], 'application/json')
+
def test_dont_rewrite_msie_accept_header(self):
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
that we get a JSON response if we set a */* accept header."""
diff --git a/djangorestframework/tests/serializer.py b/djangorestframework/tests/serializer.py
index e8580610..834a60d0 100644
--- a/djangorestframework/tests/serializer.py
+++ b/djangorestframework/tests/serializer.py
@@ -104,6 +104,27 @@ class TestFieldNesting(TestCase):
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
+ def test_serializer_no_fields(self):
+ """
+ Test related serializer works when the fields attr isn't present. Fix for
+ #178.
+ """
+ class NestedM2(Serializer):
+ fields = ('field1', )
+
+ class NestedM3(Serializer):
+ fields = ('field2', )
+
+ class SerializerM2(Serializer):
+ include = [('field', NestedM2)]
+ exclude = ('id', )
+
+ class SerializerM3(Serializer):
+ fields = [('field', NestedM3)]
+
+ self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
+ self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
+
def test_serializer_classname_nesting(self):
"""
Test related model serialization
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 3657fd64..4aa6ca0c 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -156,6 +156,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
description = _remove_leading_indent(description)
+ if not isinstance(description, unicode):
+ description = description.decode('UTF-8')
+
if html:
return self.markup_description(description)
return description