Merge remote-tracking branch 'upstream/master' into paginate_kwarg_setting

This commit is contained in:
Philip Neustrom 2015-02-12 18:28:19 -08:00
commit 843daa37bf
23 changed files with 176 additions and 54 deletions

18
.gitignore vendored
View File

@ -3,18 +3,14 @@
*~ *~
.* .*
site/ /site/
htmlcov/ /htmlcov/
coverage/ /coverage/
build/ /build/
dist/ /dist/
*.egg-info/ /*.egg-info/
/env/
MANIFEST MANIFEST
bin/
include/
lib/
local/
!.gitignore !.gitignore
!.travis.yml !.travis.yml

View File

@ -25,18 +25,6 @@ env:
- TOX_ENV=py33-django18alpha - TOX_ENV=py33-django18alpha
- TOX_ENV=py32-django18alpha - TOX_ENV=py32-django18alpha
- TOX_ENV=py27-django18alpha - TOX_ENV=py27-django18alpha
- TOX_ENV=py34-djangomaster
- TOX_ENV=py33-djangomaster
- TOX_ENV=py32-djangomaster
- TOX_ENV=py27-djangomaster
matrix:
fast_finish: true
allow_failures:
- env: TOX_ENV=py34-djangomaster
- env: TOX_ENV=py33-djangomaster
- env: TOX_ENV=py32-djangomaster
- env: TOX_ENV=py27-djangomaster
install: install:
- pip install tox - pip install tox

View File

@ -178,7 +178,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`,
#### Example #### Example
Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example: Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example:
class AccountViewSet(viewsets.ModelViewSet): class AccountViewSet(viewsets.ModelViewSet):
""" """

View File

@ -41,6 +41,19 @@ You can determine your currently installed version using `pip freeze`:
## 3.0.x series ## 3.0.x series
### 3.0.5
**Date**: [10th February 2015][3.0.5-milestone].
* Fix a bug where `_closable_objects` breaks pickling. ([#1850][gh1850], [#2492][gh2492])
* Allow non-standard `User` models with `Throttling`. ([#2524][gh2524])
* Support custom `User.db_table` in TokenAuthentication migration. ([#2479][gh2479])
* Fix misleading `AttributeError` tracebacks on `Request` objects. ([#2530][gh2530], [#2108][gh2108])
* `ManyRelatedField.get_value` clearing field on partial update. ([#2475][gh2475])
* Removed '.model' shortcut from code. ([#2486][gh2486])
* Fix `detail_route` and `list_route` mutable argument. ([#2518][gh2518])
* Prefetching the user object when getting the token in `TokenAuthentication`. ([#2519][gh2519])
### 3.0.4 ### 3.0.4
**Date**: [28th January 2015][3.0.4-milestone]. **Date**: [28th January 2015][3.0.4-milestone].
@ -721,6 +734,7 @@ For older release notes, [please see the GitHub repo](old-release-notes).
[3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22
[3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22 [3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22
[3.0.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.4+Release%22 [3.0.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.4+Release%22
[3.0.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.5+Release%22
<!-- 3.0.1 --> <!-- 3.0.1 -->
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
@ -808,3 +822,14 @@ For older release notes, [please see the GitHub repo](old-release-notes).
[gh2399]: https://github.com/tomchristie/django-rest-framework/issues/2399 [gh2399]: https://github.com/tomchristie/django-rest-framework/issues/2399
[gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388 [gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388
[gh2360]: https://github.com/tomchristie/django-rest-framework/issues/2360 [gh2360]: https://github.com/tomchristie/django-rest-framework/issues/2360
<!-- 3.0.5 -->
[gh1850]: https://github.com/tomchristie/django-rest-framework/issues/1850
[gh2108]: https://github.com/tomchristie/django-rest-framework/issues/2108
[gh2475]: https://github.com/tomchristie/django-rest-framework/issues/2475
[gh2479]: https://github.com/tomchristie/django-rest-framework/issues/2479
[gh2486]: https://github.com/tomchristie/django-rest-framework/issues/2486
[gh2492]: https://github.com/tomchristie/django-rest-framework/issues/2492
[gh2518]: https://github.com/tomchristie/django-rest-framework/issues/2518
[gh2519]: https://github.com/tomchristie/django-rest-framework/issues/2519
[gh2524]: https://github.com/tomchristie/django-rest-framework/issues/2524
[gh2530]: https://github.com/tomchristie/django-rest-framework/issues/2530

View File

@ -96,7 +96,7 @@ Notice that we're no longer explicitly tying our requests or responses to a give
## Adding optional format suffixes to our URLs ## Adding optional format suffixes to our URLs
To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url]. To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4/.json][json-url].
Start by adding a `format` keyword argument to both of the views, like so. Start by adding a `format` keyword argument to both of the views, like so.

View File

@ -138,7 +138,7 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file
The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages. The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages.
We can change the default list style to use pagination, by modifying our `settings.py` file slightly. Add the following setting: We can change the default list style to use pagination, by modifying our `tutorial/settings.py` file slightly. Add the following setting:
REST_FRAMEWORK = { REST_FRAMEWORK = {
'PAGINATE_BY': 10 'PAGINATE_BY': 10

View File

@ -19,6 +19,8 @@ django-oauth2-provider>=0.2.4
# wheel for PyPI installs # wheel for PyPI installs
wheel==0.24.0 wheel==0.24.0
# twine for secured PyPI uploads
twine==1.4.0
# MkDocs for documentation previews/deploys # MkDocs for documentation previews/deploys
mkdocs==0.11.1 mkdocs==0.11.1

View File

@ -8,7 +8,7 @@ ______ _____ _____ _____ __
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '3.0.4' __version__ = '3.0.5'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2015 Tom Christie' __copyright__ = 'Copyright 2011-2015 Tom Christie'

View File

@ -167,7 +167,7 @@ class TokenAuthentication(BaseAuthentication):
def authenticate_credentials(self, key): def authenticate_credentials(self, key):
try: try:
token = self.model.objects.get(key=key) token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist: except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token') raise exceptions.AuthenticationFailed('Invalid token')

View File

@ -18,8 +18,7 @@ def api_view(http_method_names=None):
Decorator that converts a function-based view into an APIView subclass. Decorator that converts a function-based view into an APIView subclass.
Takes a list of allowed methods for the view as an argument. Takes a list of allowed methods for the view as an argument.
""" """
if http_method_names is None: http_method_names = ['GET'] if (http_method_names is None) else http_method_names
http_method_names = ['GET']
def decorator(func): def decorator(func):
@ -109,10 +108,12 @@ def permission_classes(permission_classes):
return decorator return decorator
def detail_route(methods=['get'], **kwargs): def detail_route(methods=None, **kwargs):
""" """
Used to mark a method on a ViewSet that should be routed for detail requests. Used to mark a method on a ViewSet that should be routed for detail requests.
""" """
methods = ['get'] if (methods is None) else methods
def decorator(func): def decorator(func):
func.bind_to_methods = methods func.bind_to_methods = methods
func.detail = True func.detail = True
@ -121,10 +122,12 @@ def detail_route(methods=['get'], **kwargs):
return decorator return decorator
def list_route(methods=['get'], **kwargs): def list_route(methods=None, **kwargs):
""" """
Used to mark a method on a ViewSet that should be routed for list requests. Used to mark a method on a ViewSet that should be routed for list requests.
""" """
methods = ['get'] if (methods is None) else methods
def decorator(func): def decorator(func):
func.bind_to_methods = methods func.bind_to_methods = methods
func.detail = False func.detail = False

View File

@ -104,7 +104,7 @@ class SearchFilter(BaseFilterBackend):
for search_term in self.get_search_terms(request): for search_term in self.get_search_terms(request):
or_queries = [models.Q(**{orm_lookup: search_term}) or_queries = [models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups] for orm_lookup in orm_lookups]
queryset = queryset.filter(reduce(operator.or_, or_queries)) queryset = queryset.filter(reduce(operator.or_, or_queries)).distinct()
return queryset return queryset

View File

@ -12,12 +12,13 @@ from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.http import QueryDict from django.http import QueryDict
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.utils import six
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.datastructures import MergeDict as DjangoMergeDict from django.utils.datastructures import MergeDict as DjangoMergeDict
from django.utils.six import BytesIO
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import sys
import warnings import warnings
@ -362,7 +363,7 @@ class Request(object):
elif hasattr(self._request, 'read'): elif hasattr(self._request, 'read'):
self._stream = self._request self._stream = self._request
else: else:
self._stream = BytesIO(self.raw_post_data) self._stream = six.BytesIO(self.raw_post_data)
def _perform_form_overloading(self): def _perform_form_overloading(self):
""" """
@ -404,7 +405,7 @@ class Request(object):
self._CONTENTTYPE_PARAM in self._data self._CONTENTTYPE_PARAM in self._data
): ):
self._content_type = self._data[self._CONTENTTYPE_PARAM] self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
self._data, self._files, self._full_data = (Empty, Empty, Empty) self._data, self._files, self._full_data = (Empty, Empty, Empty)
def _parse(self): def _parse(self):
@ -485,8 +486,16 @@ class Request(object):
else: else:
self.auth = None self.auth = None
def __getattr__(self, attr): def __getattribute__(self, attr):
""" """
Proxy other attributes to the underlying HttpRequest object. If an attribute does not exist on this instance, then we also attempt
to proxy it to the underlying HttpRequest object.
""" """
return getattr(self._request, attr) try:
return super(Request, self).__getattribute__(attr)
except AttributeError:
info = sys.exc_info()
try:
return getattr(self._request, attr)
except AttributeError:
six.reraise(info[0], info[1], info[2].tb_next)

View File

@ -154,7 +154,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
If autoescape is True, the link text and URLs will get autoescaped. If autoescape is True, the link text and URLs will get autoescaped.
""" """
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x def trim_url(x, limit=trim_url_limit):
return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
safe_input = isinstance(text, SafeData) safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_text(text)) words = word_split_re.split(force_text(text))
for i, word in enumerate(words): for i, word in enumerate(words):

View File

@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle):
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
if request.user.is_authenticated(): if request.user.is_authenticated():
ident = request.user.id ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)
@ -239,7 +239,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
with the '.throttle_scope` property of the view. with the '.throttle_scope` property of the view.
""" """
if request.user.is_authenticated(): if request.user.is_authenticated():
ident = request.user.id ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)

View File

@ -48,8 +48,11 @@ if sys.argv[-1] == 'publish':
if os.system("pip freeze | grep wheel"): if os.system("pip freeze | grep wheel"):
print("wheel not installed.\nUse `pip install wheel`.\nExiting.") print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
sys.exit() sys.exit()
os.system("python setup.py sdist upload") if os.system("pip freeze | grep twine"):
os.system("python setup.py bdist_wheel upload") print("twine not installed.\nUse `pip install twine`.\nExiting.")
sys.exit()
os.system("python setup.py sdist bdist_wheel")
os.system("twine upload dist/*")
print("You probably want to also tag the version now:") print("You probably want to also tag the version now:")
print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git tag -a %s -m 'version %s'" % (version, version))
print(" git push --tags") print(" git push --tags")

View File

@ -202,6 +202,15 @@ class TokenAuthTests(TestCase):
response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post_json_makes_one_db_query(self):
"""Ensure that authenticating a user using a token performs only one DB query"""
auth = "Token " + self.key
def func_to_test():
return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
self.assertNumQueries(1, func_to_test)
def test_post_form_failing_token_auth(self): def test_post_form_failing_token_auth(self):
"""Ensure POSTing form over token auth without correct credentials fails""" """Ensure POSTing form over token auth without correct credentials fails"""
response = self.csrf_client.post('/token/', {'example': 'example'}) response = self.csrf_client.post('/token/', {'example': 'example'})

View File

@ -429,6 +429,56 @@ class SearchFilterTests(TestCase):
reload_module(filters) reload_module(filters)
class AttributeModel(models.Model):
label = models.CharField(max_length=32)
class SearchFilterModelM2M(models.Model):
title = models.CharField(max_length=20)
text = models.CharField(max_length=100)
attributes = models.ManyToManyField(AttributeModel)
class SearchFilterM2MSerializer(serializers.ModelSerializer):
class Meta:
model = SearchFilterModelM2M
class SearchFilterM2MTests(TestCase):
def setUp(self):
# Sequence of title/text/attributes is:
#
# z abc [1, 2, 3]
# zz bcd [1, 2, 3]
# zzz cde [1, 2, 3]
# ...
for idx in range(3):
label = 'w' * (idx + 1)
AttributeModel(label=label)
for idx in range(10):
title = 'z' * (idx + 1)
text = (
chr(idx + ord('a')) +
chr(idx + ord('b')) +
chr(idx + ord('c'))
)
SearchFilterModelM2M(title=title, text=text).save()
SearchFilterModelM2M.objects.get(title='zz').attributes.add(1, 2, 3)
def test_m2m_search(self):
class SearchListView(generics.ListAPIView):
queryset = SearchFilterModelM2M.objects.all()
serializer_class = SearchFilterM2MSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('=title', 'text', 'attributes__label')
view = SearchListView.as_view()
request = factory.get('/', {'search': 'zz'})
response = view(request)
self.assertEqual(len(response.data), 1)
class OrderingFilterModel(models.Model): class OrderingFilterModel(models.Model):
title = models.CharField(max_length=20) title = models.CharField(max_length=20)
text = models.CharField(max_length=100) text = models.CharField(max_length=100)

View File

@ -12,7 +12,9 @@ factory = APIRequestFactory()
request = factory.get('/') # Just to ensure we have a request in the serializer context request = factory.get('/') # Just to ensure we have a request in the serializer context
dummy_view = lambda request, pk: None def dummy_view(request, pk):
pass
urlpatterns = patterns( urlpatterns = patterns(
'', '',

View File

@ -28,8 +28,13 @@ import re
DUMMYSTATUS = status.HTTP_200_OK DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent' DUMMYCONTENT = 'dummycontent'
RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') def RENDERER_A_SERIALIZER(x):
return ('Renderer A: %s' % x).encode('ascii')
def RENDERER_B_SERIALIZER(x):
return ('Renderer B: %s' % x).encode('ascii')
expected_results = [ expected_results = [

View File

@ -249,9 +249,29 @@ class TestUserSetter(TestCase):
login(self.request, self.user) login(self.request, self.user)
self.assertEqual(self.wrapped_request.user, self.user) self.assertEqual(self.wrapped_request.user, self.user)
def test_calling_user_fails_when_attribute_error_is_raised(self):
"""
This proves that when an AttributeError is raised inside of the request.user
property, that we can handle this and report the true, underlying error.
"""
class AuthRaisesAttributeError(object):
def authenticate(self, request):
import rest_framework
rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST
self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),))
SessionMiddleware().process_request(self.request)
login(self.request, self.user)
try:
self.request.user
except AttributeError as error:
self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'")
else:
assert False, 'AttributeError not raised'
class TestAuthSetter(TestCase): class TestAuthSetter(TestCase):
def test_auth_can_be_set(self): def test_auth_can_be_set(self):
request = Request(factory.get('/')) request = Request(factory.get('/'))
request.auth = 'DUMMY' request.auth = 'DUMMY'

View File

@ -38,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer):
DUMMYSTATUS = status.HTTP_200_OK DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent' DUMMYCONTENT = 'dummycontent'
RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') def RENDERER_A_SERIALIZER(x):
return ('Renderer A: %s' % x).encode('ascii')
def RENDERER_B_SERIALIZER(x):
return ('Renderer B: %s' % x).encode('ascii')
class RendererA(BaseRenderer): class RendererA(BaseRenderer):

View File

@ -188,7 +188,9 @@ class ScopedRateThrottleTests(TestCase):
class XYScopedRateThrottle(ScopedRateThrottle): class XYScopedRateThrottle(ScopedRateThrottle):
TIMER_SECONDS = 0 TIMER_SECONDS = 0
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'} THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
timer = lambda self: self.TIMER_SECONDS
def timer(self):
return self.TIMER_SECONDS
class XView(APIView): class XView(APIView):
throttle_classes = (XYScopedRateThrottle,) throttle_classes = (XYScopedRateThrottle,)
@ -290,7 +292,9 @@ class XffTestingBase(TestCase):
class Throttle(ScopedRateThrottle): class Throttle(ScopedRateThrottle):
THROTTLE_RATES = {'test_limit': '1/day'} THROTTLE_RATES = {'test_limit': '1/day'}
TIMER_SECONDS = 0 TIMER_SECONDS = 0
timer = lambda self: self.TIMER_SECONDS
def timer(self):
return self.TIMER_SECONDS
class View(APIView): class View(APIView):
throttle_classes = (Throttle,) throttle_classes = (Throttle,)

View File

@ -3,7 +3,7 @@ envlist =
py27-{flake8,docs}, py27-{flake8,docs},
{py26,py27}-django14, {py26,py27}-django14,
{py26,py27,py32,py33,py34}-django{15,16}, {py26,py27,py32,py33,py34}-django{15,16},
{py27,py32,py33,py34}-django{17,18alpha,master} {py27,py32,py33,py34}-django{17,18alpha}
[testenv] [testenv]
commands = ./runtests.py --fast commands = ./runtests.py --fast
@ -15,7 +15,6 @@ deps =
django16: Django==1.6.3 # Should track minimum supported django16: Django==1.6.3 # Should track minimum supported
django17: Django==1.7.2 # Should track maximum supported django17: Django==1.7.2 # Should track maximum supported
django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/
djangomaster: https://github.com/django/django/zipball/master
{py26,py27}-django{14,15,16,17}: django-guardian==1.2.3 {py26,py27}-django{14,15,16,17}: django-guardian==1.2.3
{py26,py27}-django{14,15,16}: oauth2==1.5.211 {py26,py27}-django{14,15,16}: oauth2==1.5.211
{py26,py27}-django{14,15,16}: django-oauth-plus==2.2.1 {py26,py27}-django{14,15,16}: django-oauth-plus==2.2.1