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/
|
/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
|
||||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -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")
|
||||||
|
|
|
@ -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'})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
'',
|
'',
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,)
|
||||||
|
|
3
tox.ini
3
tox.ini
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user