This commit is contained in:
Tom Christie 2012-08-24 16:06:23 +01:00
commit 7557777c66
18 changed files with 139 additions and 51 deletions

View File

@ -34,6 +34,12 @@ Paul Oswald <poswald>
Sean C. Farley <scfarley> Sean C. Farley <scfarley>
Daniel Izquierdo <izquierdo> Daniel Izquierdo <izquierdo>
Can Yavuz <tschan> Can Yavuz <tschan>
Shawn Lewis <shawnlewis>
Adam Ness <greylurk>
<yetist>
Max Arnold <max-arnold>
Ralph Broenink <ralphje>
Simon Pantzare <pilt>
THANKS TO: THANKS TO:

View File

@ -1,6 +1,11 @@
Release Notes Release Notes
============= =============
0.4.0-dev
---------
* Markdown < 2.0 is no longer supported.
0.3.3 0.3.3
----- -----

View File

@ -26,7 +26,7 @@ We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframewor
Requirements: Requirements:
* Python (2.5, 2.6, 2.7 supported) * Python (2.5, 2.6, 2.7 supported)
* Django (1.2, 1.3, 1.4-alpha supported) * Django (1.2, 1.3, 1.4 supported)
Installation Notes Installation Notes

View File

@ -1,3 +1,3 @@
__version__ = '0.3.3' __version__ = '0.4.0-dev'
VERSION = __version__ # synonym VERSION = __version__ # synonym

View File

@ -65,15 +65,45 @@ except ImportError:
environ.update(request) environ.update(request)
return WSGIRequest(environ) return WSGIRequest(environ)
# django.views.generic.View (Django >= 1.3) # django.views.generic.View (1.3 <= Django < 1.4)
try: try:
from django.views.generic import View 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 # First implementation of Django class-based views did not include head method
# in base View class - https://code.djangoproject.com/ticket/15668 # in base View class - https://code.djangoproject.com/ticket/15668
class ViewPlusHead(View): class ViewPlusHead(View):
def head(self, request, *args, **kwargs): @classonlymethod
return self.get(request, *args, **kwargs) 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 View = ViewPlusHead
except ImportError: except ImportError:
@ -121,6 +151,8 @@ except ImportError:
def view(request, *args, **kwargs): def view(request, *args, **kwargs):
self = cls(**initkwargs) self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
return self.dispatch(request, *args, **kwargs) return self.dispatch(request, *args, **kwargs)
# take name and docstring from class # take name and docstring from class
@ -154,9 +186,6 @@ except ImportError:
#) #)
return http.HttpResponseNotAllowed(allowed_methods) 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. # PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4): if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
@ -370,6 +399,8 @@ else:
# Markdown is optional # Markdown is optional
try: try:
import markdown import markdown
if markdown.version_info < (2, 0):
raise ImportError('Markdown < 2.0 is not supported.')
class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor): class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
""" """

View File

@ -181,7 +181,7 @@ class RequestMixin(object):
return parser.parse(stream) return parser.parse(stream)
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
{'error': 'Unsupported media type in request \'%s\'.' % {'detail': 'Unsupported media type in request \'%s\'.' %
content_type}) content_type})
@property @property
@ -274,7 +274,8 @@ class ResponseMixin(object):
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)] accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
elif (self._IGNORE_IE_ACCEPT_HEADER and elif (self._IGNORE_IE_ACCEPT_HEADER and
'HTTP_USER_AGENT' in request.META 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 # Ignore MSIE's broken accept behavior and do something sensible instead
accept_list = ['text/html', '*/*'] accept_list = ['text/html', '*/*']
elif 'HTTP_ACCEPT' in request.META: elif 'HTTP_ACCEPT' in request.META:

View File

@ -182,6 +182,10 @@ class TemplateRenderer(BaseRenderer):
media_type = None media_type = None
template = 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): def render(self, obj=None, media_type=None):
""" """
Renders *obj* using the :attr:`template` specified on the class. Renders *obj* using the :attr:`template` specified on the class.
@ -202,6 +206,10 @@ class DocumentingTemplateRenderer(BaseRenderer):
template = None 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): def _get_content(self, view, request, obj, media_type):
""" """
Get the content as if it had been rendered by a non-documenting renderer. Get the content as if it had been rendered by a non-documenting renderer.

View File

@ -169,8 +169,9 @@ class FormResource(Resource):
) )
# Add any unknown field errors # Add any unknown field errors
for key in unknown_fields: if not self.allow_unknown_form_fields:
field_errors[key] = [u'This field does not exist.'] for key in unknown_fields:
field_errors[key] = [u'This field does not exist.']
if field_errors: if field_errors:
detail[u'field_errors'] = field_errors detail[u'field_errors'] = field_errors

View File

@ -2,7 +2,7 @@
Customizable serialization. Customizable serialization.
""" """
from django.db import models 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 from django.utils.encoding import smart_unicode, is_protected_type, smart_str
import inspect import inspect
@ -25,16 +25,9 @@ def _field_to_tuple(field):
def _fields_to_list(fields): 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 ()] return [_field_to_tuple(field) 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 ()])
class _SkipField(Exception): class _SkipField(Exception):
@ -104,15 +97,17 @@ class Serializer(object):
The maximum depth to serialize to, or `None`. 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): def __init__(self, depth=None, stack=[], **kwargs):
if depth is not None: if depth is not None:
self.depth = depth self.depth = depth
self.stack = stack self.stack = stack
def get_fields(self, obj): def get_fields(self, obj):
"""
Return the set of field names/keys to use for a model instance/dict.
"""
fields = self.fields fields = self.fields
# If `fields` is not set, we use the default fields and modify # If `fields` is not set, we use the default fields and modify
@ -123,9 +118,6 @@ class Serializer(object):
exclude = self.exclude or () exclude = self.exclude or ()
fields = set(default + list(include)) - set(exclude) fields = set(default + list(include)) - set(exclude)
else:
fields = _fields_to_list(self.fields)
return fields return fields
def get_default_fields(self, obj): def get_default_fields(self, obj):
@ -139,15 +131,16 @@ class Serializer(object):
else: else:
return obj.keys() return obj.keys()
def get_related_serializer(self, key): def get_related_serializer(self, info):
info = _fields_to_dict(self.fields).get(key, None)
# If an element in `fields` is a 2-tuple of (str, tuple) # If an element in `fields` is a 2-tuple of (str, tuple)
# then the second element of the tuple is the fields to # then the second element of the tuple is the fields to
# set on the related serializer # set on the related serializer
class OnTheFlySerializer(self.__class__):
fields = info
parent = getattr(self, 'parent') or self
if isinstance(info, (list, tuple)): if isinstance(info, (list, tuple)):
class OnTheFlySerializer(self.__class__):
fields = info
return OnTheFlySerializer return OnTheFlySerializer
# If an element in `fields` is a 2-tuple of (str, Serializer) # 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: elif isinstance(info, str) and info in _serializers:
return _serializers[info] return _serializers[info]
# Otherwise use `related_serializer` or fall back to `Serializer` # Otherwise use `related_serializer` or fall back to
return getattr(self, 'related_serializer') or Serializer # `OnTheFlySerializer` preserve custom serialization methods.
return getattr(self, 'related_serializer') or OnTheFlySerializer
def serialize_key(self, key): def serialize_key(self, key):
""" """
@ -175,11 +169,11 @@ class Serializer(object):
""" """
return self.rename.get(smart_str(key), smart_str(key)) 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. 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: if self.depth is None:
depth = None depth = None
@ -194,7 +188,8 @@ class Serializer(object):
stack = self.stack[:] stack = self.stack[:]
stack.append(obj) 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): def serialize_max_depth(self, obj):
""" """
@ -219,7 +214,7 @@ class Serializer(object):
fields = self.get_fields(instance) fields = self.get_fields(instance)
# serialize each required field # serialize each required field
for fname in fields: for fname, related_info in _fields_to_list(fields):
try: try:
# we first check for a method 'fname' on self, # we first check for a method 'fname' on self,
# 'fname's signature must be 'def fname(self, instance)' # 'fname's signature must be 'def fname(self, instance)'
@ -237,7 +232,7 @@ class Serializer(object):
continue continue
key = self.serialize_key(fname) key = self.serialize_key(fname)
val = self.serialize_val(fname, obj) val = self.serialize_val(fname, obj, related_info)
data[key] = val data[key] = val
except _SkipField: except _SkipField:
pass pass
@ -268,15 +263,19 @@ class Serializer(object):
""" """
return smart_unicode(obj, strings_only=True) return smart_unicode(obj, strings_only=True)
def serialize(self, obj): def serialize(self, obj, request=None):
""" """
Convert any object into a serializable representation. 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)): if isinstance(obj, (dict, models.Model)):
# Model instances & dictionaries # Model instances & dictionaries
return self.serialize_model(obj) 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 # basic iterables
return self.serialize_iter(obj) return self.serialize_iter(obj)
elif isinstance(obj, models.Manager): elif isinstance(obj, models.Manager):

View File

@ -4,7 +4,7 @@ register = Library()
def add_query_param(url, param): 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)

View File

@ -50,6 +50,16 @@ class UserAgentMungingTest(TestCase):
resp = self.view(req) resp = self.view(req)
self.assertEqual(resp['Content-Type'], 'text/html') 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): def test_dont_rewrite_msie_accept_header(self):
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure """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.""" that we get a JSON response if we set a */* accept header."""

View File

@ -104,6 +104,27 @@ class TestFieldNesting(TestCase):
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}}) self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}}) 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): def test_serializer_classname_nesting(self):
""" """
Test related model serialization Test related model serialization

View File

@ -156,6 +156,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
description = _remove_leading_indent(description) description = _remove_leading_indent(description)
if not isinstance(description, unicode):
description = description.decode('UTF-8')
if html: if html:
return self.markup_description(description) return self.markup_description(description)
return description return description

View File

@ -7,7 +7,7 @@ Alternative frameworks
There are a number of alternative REST frameworks for Django: There are a number of alternative REST frameworks for Django:
* `django-piston <https://bitbucket.org/jespern/django-piston/wiki/Home>`_ is very mature, and has a large community behind it. This project was originally based on piston code in parts. * `django-piston <https://bitbucket.org/jespern/django-piston/wiki/Home>`_ is very mature, and has a large community behind it. This project was originally based on piston code in parts.
* `django-tasypie <https://github.com/toastdriven/django-tastypie>`_ is also very good, and has a very active and helpful developer community and maintainers. * `django-tastypie <https://github.com/toastdriven/django-tastypie>`_ is also very good, and has a very active and helpful developer community and maintainers.
* Other interesting projects include `dagny <https://github.com/zacharyvoase/dagny>`_ and `dj-webmachine <http://benoitc.github.com/dj-webmachine/>`_ * Other interesting projects include `dagny <https://github.com/zacharyvoase/dagny>`_ and `dj-webmachine <http://benoitc.github.com/dj-webmachine/>`_

View File

@ -1,6 +1,6 @@
# Documentation requires Django & Sphinx, and their dependencies... # Documentation requires Django & Sphinx, and their dependencies...
Django==1.2.4 Django>=1.2.4
Jinja2==2.5.5 Jinja2==2.5.5
Pygments==1.4 Pygments==1.4
Sphinx==1.0.7 Sphinx==1.0.7

View File

@ -12,11 +12,11 @@ class PermissionsExampleView(View):
return [ return [
{ {
'name': 'Throttling Example', 'name': 'Throttling Example',
'url': reverse('throttled-resource', request) 'url': reverse('throttled-resource', request=request)
}, },
{ {
'name': 'Logged in example', 'name': 'Logged in example',
'url': reverse('loggedin-resource', request) 'url': reverse('loggedin-resource', request=request)
}, },
] ]

View File

@ -65,7 +65,7 @@ class PygmentsRoot(View):
Return a list of all currently existing snippets. 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)] unique_ids = [os.path.split(f)[1] for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)]
return [reverse('pygments-instance', request, args=[unique_id]) for unique_id in unique_ids] return [reverse('pygments-instance', request=request, args=[unique_id]) for unique_id in unique_ids]
def post(self, request): def post(self, request):
""" """
@ -85,7 +85,7 @@ class PygmentsRoot(View):
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES) remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', request, args=[unique_id])}) return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', request=request, args=[unique_id])})
class PygmentsInstance(View): class PygmentsInstance(View):

View File

@ -64,6 +64,9 @@ setup(
package_data=get_package_data('djangorestframework'), package_data=get_package_data('djangorestframework'),
test_suite='djangorestframework.runtests.runcoverage.main', test_suite='djangorestframework.runtests.runcoverage.main',
install_requires=['URLObject>=0.6.0'], install_requires=['URLObject>=0.6.0'],
extras_require={
'markdown': ["Markdown>=2.0"]
},
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'Environment :: Web Environment', 'Environment :: Web Environment',