mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 18:13:57 +03:00
Merge branch 'master' of https://github.com/tomchristie/django-rest-framework
This commit is contained in:
commit
7557777c66
6
AUTHORS
6
AUTHORS
|
@ -34,6 +34,12 @@ Paul Oswald <poswald>
|
|||
Sean C. Farley <scfarley>
|
||||
Daniel Izquierdo <izquierdo>
|
||||
Can Yavuz <tschan>
|
||||
Shawn Lewis <shawnlewis>
|
||||
Adam Ness <greylurk>
|
||||
<yetist>
|
||||
Max Arnold <max-arnold>
|
||||
Ralph Broenink <ralphje>
|
||||
Simon Pantzare <pilt>
|
||||
|
||||
THANKS TO:
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
Release Notes
|
||||
=============
|
||||
|
||||
0.4.0-dev
|
||||
---------
|
||||
|
||||
* Markdown < 2.0 is no longer supported.
|
||||
|
||||
0.3.3
|
||||
-----
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframewor
|
|||
Requirements:
|
||||
|
||||
* 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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '0.3.3'
|
||||
__version__ = '0.4.0-dev'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@ Alternative frameworks
|
|||
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-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/>`_
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Documentation requires Django & Sphinx, and their dependencies...
|
||||
|
||||
Django==1.2.4
|
||||
Django>=1.2.4
|
||||
Jinja2==2.5.5
|
||||
Pygments==1.4
|
||||
Sphinx==1.0.7
|
||||
|
|
|
@ -12,11 +12,11 @@ class PermissionsExampleView(View):
|
|||
return [
|
||||
{
|
||||
'name': 'Throttling Example',
|
||||
'url': reverse('throttled-resource', request)
|
||||
'url': reverse('throttled-resource', request=request)
|
||||
},
|
||||
{
|
||||
'name': 'Logged in example',
|
||||
'url': reverse('loggedin-resource', request)
|
||||
'url': reverse('loggedin-resource', request=request)
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ class PygmentsRoot(View):
|
|||
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', 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):
|
||||
"""
|
||||
|
@ -85,7 +85,7 @@ class PygmentsRoot(View):
|
|||
|
||||
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):
|
||||
|
|
3
setup.py
3
setup.py
|
@ -64,6 +64,9 @@ setup(
|
|||
package_data=get_package_data('djangorestframework'),
|
||||
test_suite='djangorestframework.runtests.runcoverage.main',
|
||||
install_requires=['URLObject>=0.6.0'],
|
||||
extras_require={
|
||||
'markdown': ["Markdown>=2.0"]
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
|
|
Loading…
Reference in New Issue
Block a user