mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-06 05:20:12 +03:00
Merge remote-tracking branch 'upstream/master' into paginate_kwarg_setting
This commit is contained in:
commit
843daa37bf
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -3,18 +3,14 @@
|
|||
*~
|
||||
.*
|
||||
|
||||
site/
|
||||
htmlcov/
|
||||
coverage/
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
/site/
|
||||
/htmlcov/
|
||||
/coverage/
|
||||
/build/
|
||||
/dist/
|
||||
/*.egg-info/
|
||||
/env/
|
||||
MANIFEST
|
||||
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
local/
|
||||
|
||||
!.gitignore
|
||||
!.travis.yml
|
||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -25,18 +25,6 @@ env:
|
|||
- TOX_ENV=py33-django18alpha
|
||||
- TOX_ENV=py32-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:
|
||||
- pip install tox
|
||||
|
|
|
@ -178,7 +178,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`,
|
|||
|
||||
#### 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):
|
||||
"""
|
||||
|
|
|
@ -41,6 +41,19 @@ You can determine your currently installed version using `pip freeze`:
|
|||
## 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
|
||||
|
||||
**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.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.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.5+Release%22
|
||||
|
||||
<!-- 3.0.1 -->
|
||||
[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
|
||||
[gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388
|
||||
[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
|
||||
|
|
|
@ -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
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
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 = {
|
||||
'PAGINATE_BY': 10
|
||||
|
|
|
@ -19,6 +19,8 @@ django-oauth2-provider>=0.2.4
|
|||
|
||||
# wheel for PyPI installs
|
||||
wheel==0.24.0
|
||||
# twine for secured PyPI uploads
|
||||
twine==1.4.0
|
||||
|
||||
# MkDocs for documentation previews/deploys
|
||||
mkdocs==0.11.1
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '3.0.4'
|
||||
__version__ = '3.0.5'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2015 Tom Christie'
|
||||
|
|
|
@ -167,7 +167,7 @@ class TokenAuthentication(BaseAuthentication):
|
|||
|
||||
def authenticate_credentials(self, key):
|
||||
try:
|
||||
token = self.model.objects.get(key=key)
|
||||
token = self.model.objects.select_related('user').get(key=key)
|
||||
except self.model.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token')
|
||||
|
||||
|
|
|
@ -18,8 +18,7 @@ def api_view(http_method_names=None):
|
|||
Decorator that converts a function-based view into an APIView subclass.
|
||||
Takes a list of allowed methods for the view as an argument.
|
||||
"""
|
||||
if http_method_names is None:
|
||||
http_method_names = ['GET']
|
||||
http_method_names = ['GET'] if (http_method_names is None) else http_method_names
|
||||
|
||||
def decorator(func):
|
||||
|
||||
|
@ -109,10 +108,12 @@ def permission_classes(permission_classes):
|
|||
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.
|
||||
"""
|
||||
methods = ['get'] if (methods is None) else methods
|
||||
|
||||
def decorator(func):
|
||||
func.bind_to_methods = methods
|
||||
func.detail = True
|
||||
|
@ -121,10 +122,12 @@ def detail_route(methods=['get'], **kwargs):
|
|||
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.
|
||||
"""
|
||||
methods = ['get'] if (methods is None) else methods
|
||||
|
||||
def decorator(func):
|
||||
func.bind_to_methods = methods
|
||||
func.detail = False
|
||||
|
|
|
@ -104,7 +104,7 @@ class SearchFilter(BaseFilterBackend):
|
|||
for search_term in self.get_search_terms(request):
|
||||
or_queries = [models.Q(**{orm_lookup: search_term})
|
||||
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
|
||||
|
||||
|
|
|
@ -12,12 +12,13 @@ from __future__ import unicode_literals
|
|||
from django.conf import settings
|
||||
from django.http import QueryDict
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.utils import six
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
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 exceptions
|
||||
from rest_framework.settings import api_settings
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
|
||||
|
@ -362,7 +363,7 @@ class Request(object):
|
|||
elif hasattr(self._request, 'read'):
|
||||
self._stream = self._request
|
||||
else:
|
||||
self._stream = BytesIO(self.raw_post_data)
|
||||
self._stream = six.BytesIO(self.raw_post_data)
|
||||
|
||||
def _perform_form_overloading(self):
|
||||
"""
|
||||
|
@ -404,7 +405,7 @@ class Request(object):
|
|||
self._CONTENTTYPE_PARAM in self._data
|
||||
):
|
||||
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)
|
||||
|
||||
def _parse(self):
|
||||
|
@ -485,8 +486,16 @@ class Request(object):
|
|||
else:
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
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)
|
||||
words = word_split_re.split(force_text(text))
|
||||
for i, word in enumerate(words):
|
||||
|
|
|
@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle):
|
|||
|
||||
def get_cache_key(self, request, view):
|
||||
if request.user.is_authenticated():
|
||||
ident = request.user.id
|
||||
ident = request.user.pk
|
||||
else:
|
||||
ident = self.get_ident(request)
|
||||
|
||||
|
@ -239,7 +239,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
|
|||
with the '.throttle_scope` property of the view.
|
||||
"""
|
||||
if request.user.is_authenticated():
|
||||
ident = request.user.id
|
||||
ident = request.user.pk
|
||||
else:
|
||||
ident = self.get_ident(request)
|
||||
|
||||
|
|
7
setup.py
7
setup.py
|
@ -48,8 +48,11 @@ if sys.argv[-1] == 'publish':
|
|||
if os.system("pip freeze | grep wheel"):
|
||||
print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
|
||||
sys.exit()
|
||||
os.system("python setup.py sdist upload")
|
||||
os.system("python setup.py bdist_wheel upload")
|
||||
if os.system("pip freeze | grep twine"):
|
||||
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(" git tag -a %s -m 'version %s'" % (version, version))
|
||||
print(" git push --tags")
|
||||
|
|
|
@ -202,6 +202,15 @@ class TokenAuthTests(TestCase):
|
|||
response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
|
||||
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):
|
||||
"""Ensure POSTing form over token auth without correct credentials fails"""
|
||||
response = self.csrf_client.post('/token/', {'example': 'example'})
|
||||
|
|
|
@ -429,6 +429,56 @@ class SearchFilterTests(TestCase):
|
|||
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):
|
||||
title = models.CharField(max_length=20)
|
||||
text = models.CharField(max_length=100)
|
||||
|
|
|
@ -12,7 +12,9 @@ factory = APIRequestFactory()
|
|||
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(
|
||||
'',
|
||||
|
|
|
@ -28,8 +28,13 @@ import re
|
|||
DUMMYSTATUS = status.HTTP_200_OK
|
||||
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 = [
|
||||
|
|
|
@ -249,9 +249,29 @@ class TestUserSetter(TestCase):
|
|||
login(self.request, 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):
|
||||
|
||||
def test_auth_can_be_set(self):
|
||||
request = Request(factory.get('/'))
|
||||
request.auth = 'DUMMY'
|
||||
|
|
|
@ -38,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer):
|
|||
DUMMYSTATUS = status.HTTP_200_OK
|
||||
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):
|
||||
|
|
|
@ -188,7 +188,9 @@ class ScopedRateThrottleTests(TestCase):
|
|||
class XYScopedRateThrottle(ScopedRateThrottle):
|
||||
TIMER_SECONDS = 0
|
||||
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
|
||||
timer = lambda self: self.TIMER_SECONDS
|
||||
|
||||
def timer(self):
|
||||
return self.TIMER_SECONDS
|
||||
|
||||
class XView(APIView):
|
||||
throttle_classes = (XYScopedRateThrottle,)
|
||||
|
@ -290,7 +292,9 @@ class XffTestingBase(TestCase):
|
|||
class Throttle(ScopedRateThrottle):
|
||||
THROTTLE_RATES = {'test_limit': '1/day'}
|
||||
TIMER_SECONDS = 0
|
||||
timer = lambda self: self.TIMER_SECONDS
|
||||
|
||||
def timer(self):
|
||||
return self.TIMER_SECONDS
|
||||
|
||||
class View(APIView):
|
||||
throttle_classes = (Throttle,)
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -3,7 +3,7 @@ envlist =
|
|||
py27-{flake8,docs},
|
||||
{py26,py27}-django14,
|
||||
{py26,py27,py32,py33,py34}-django{15,16},
|
||||
{py27,py32,py33,py34}-django{17,18alpha,master}
|
||||
{py27,py32,py33,py34}-django{17,18alpha}
|
||||
|
||||
[testenv]
|
||||
commands = ./runtests.py --fast
|
||||
|
@ -15,7 +15,6 @@ deps =
|
|||
django16: Django==1.6.3 # Should track minimum supported
|
||||
django17: Django==1.7.2 # Should track maximum supported
|
||||
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}: oauth2==1.5.211
|
||||
{py26,py27}-django{14,15,16}: django-oauth-plus==2.2.1
|
||||
|
|
Loading…
Reference in New Issue
Block a user