Merge master

This commit is contained in:
Tom Christie 2015-02-09 20:43:50 +00:00
commit fbb21caaaa
29 changed files with 125 additions and 66 deletions

19
.gitignore vendored
View File

@ -3,19 +3,14 @@
*~ *~
.* .*
site/ /site/
htmlcov/ /htmlcov/
coverage/ /coverage/
build/ /build/
dist/ /dist/
*.egg-info/ /*.egg-info/
/env/
MANIFEST MANIFEST
bin/
include/
lib/
local/
env/
!.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

@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7) * Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-alpha)
# Installation # Installation

View File

@ -591,6 +591,10 @@ The [drf-compound-fields][drf-compound-fields] package provides "compound" seria
The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes. The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes.
## djangrestframework-recursive
the [djangorestframework-recursive][djangorestframework-recursive] package provides a `RecursiveField` for serializing and deserializing recursive structures
## django-rest-framework-gis ## django-rest-framework-gis
The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer. The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer.
@ -607,6 +611,7 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide
[iso8601]: http://www.w3.org/TR/NOTE-datetime [iso8601]: http://www.w3.org/TR/NOTE-datetime
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org [drf-compound-fields]: http://drf-compound-fields.readthedocs.org
[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields
[djangorestframework-recursive]: https://github.com/heywbj/django-rest-framework-recursive
[django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis [django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis
[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
[django-hstore]: https://github.com/djangonauts/django-hstore [django-hstore]: https://github.com/djangonauts/django-hstore

View File

@ -304,7 +304,7 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an
The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names]. The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].
[cite]: http://guides.rubyonrails.org/routing.html [cite]: http://guides.rubyonrails.org/routing.html
[route-decorators]: viewsets.html#marking-extra-actions-for-routing [route-decorators]: viewsets.md#marking-extra-actions-for-routing
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[wq.db]: http://wq.io/wq.db [wq.db]: http://wq.io/wq.db
[wq.db-router]: http://wq.io/docs/app.py [wq.db-router]: http://wq.io/docs/app.py

View File

@ -201,7 +201,7 @@ Note that you can use any of the standard attributes or method overrides provide
def get_queryset(self): def get_queryset(self):
return self.request.user.accounts.all() return self.request.user.accounts.all()
Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you you will have to specify the `base_name` kwarg as part of your [router registration][routers]. Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you will have to specify the `base_name` kwarg as part of your [router registration][routers].
Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes. Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes.

View File

@ -826,7 +826,7 @@ The `style` keyword argument can be used to pass through additional information
For example, to use a `textarea` control instead of the default `input` control, you would use the following… For example, to use a `textarea` control instead of the default `input` control, you would use the following…
additional_notes = serializers.CharField( additional_notes = serializers.CharField(
style={'base_template': 'text_area.html'} style={'base_template': 'textarea.html'}
) )
Similarly, to use a radio button control instead of the default `select` control, you would use the following… Similarly, to use a radio button control instead of the default `select` control, you would use the following…

View File

@ -237,7 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server. * [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
* [gaiarestframework][gaiarestframework] - Utils for django-rest-framewok * [gaiarestframework][gaiarestframework] - Utils for django-rest-framewok
* [drf-extensions][drf-extensions] - A collection of custom extensions * [drf-extensions][drf-extensions] - A collection of custom extensions
* [ember-data-django-rest-adapter][ember-data-django-rest-adapter] - An ember-data adapter * [ember-django-adapter][ember-django-adapter] - An adapter for working with Ember.js
## Other Resources ## Other Resources
@ -309,7 +309,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-rest-framework-proxy]: https://github.com/eofs/django-rest-framework-proxy [django-rest-framework-proxy]: https://github.com/eofs/django-rest-framework-proxy
[gaiarestframework]: https://github.com/AppsFuel/gaiarestframework [gaiarestframework]: https://github.com/AppsFuel/gaiarestframework
[drf-extensions]: https://github.com/chibisov/drf-extensions [drf-extensions]: https://github.com/chibisov/drf-extensions
[ember-data-django-rest-adapter]: https://github.com/toranb/ember-data-django-rest-adapter [ember-django-adapter]: https://github.com/dustinfarris/ember-django-adapter
[beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786 [beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786
[getting-started-with-django-rest-framework-and-angularjs]: http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html [getting-started-with-django-rest-framework-and-angularjs]: http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html
[end-to-end-web-app-with-django-rest-framework-angularjs]: http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework [end-to-end-web-app-with-django-rest-framework-angularjs]: http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework

View File

@ -97,7 +97,7 @@ The first thing we need to get started on our Web API is to provide a way of ser
class SnippetSerializer(serializers.Serializer): class SnippetSerializer(serializers.Serializer):
pk = serializers.IntegerField(read_only=True) pk = serializers.IntegerField(read_only=True)
title = serializers.CharField(required=False, allow_blank=True, max_length=100) title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'type': 'textarea'}) code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False) linenos = serializers.BooleanField(required=False)
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python') language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')

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

@ -14,6 +14,8 @@ django-filter>=0.9.2
# 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

@ -168,7 +168,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

@ -168,7 +168,9 @@ def _reverse_ordering(ordering_tuple):
Given an order_by tuple such as `('-created', 'uuid')` reverse the Given an order_by tuple such as `('-created', 'uuid')` reverse the
ordering and return a new tuple, eg. `('created', '-uuid')`. ordering and return a new tuple, eg. `('created', '-uuid')`.
""" """
invert = lambda x: x[1:] if (x.startswith('-')) else '-' + x def invert(x):
return x[1:] if (x.startswith('-')) else '-' + x
return tuple([invert(item) for item in ordering_tuple]) return tuple([invert(item) for item in ordering_tuple])

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
@ -366,7 +367,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):
""" """
@ -408,7 +409,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):
@ -489,8 +490,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

@ -86,8 +86,9 @@ class Response(SimpleTemplateResponse):
state = super(Response, self).__getstate__() state = super(Response, self).__getstate__()
for key in ( for key in (
'accepted_renderer', 'renderer_context', 'resolver_match', 'accepted_renderer', 'renderer_context', 'resolver_match',
'client', 'request', 'wsgi_request', '_closable_objects' 'client', 'request', 'wsgi_request'
): ):
if key in state: if key in state:
del state[key] del state[key]
state['_closable_objects'] = []
return state return state

View File

@ -177,7 +177,7 @@ class BaseSerializer(Field):
) )
assert hasattr(self, 'initial_data'), ( assert hasattr(self, 'initial_data'), (
'Cannot call `.is_valid()` as no `data=` keyword argument was' 'Cannot call `.is_valid()` as no `data=` keyword argument was '
'passed when instantiating the serializer instance.' 'passed when instantiating the serializer instance.'
) )
@ -635,11 +635,11 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data):
If we don't do this explicitly they'd get a less helpful error when If we don't do this explicitly they'd get a less helpful error when
calling `.save()` on the serializer. calling `.save()` on the serializer.
We don't *automatically* support these sorts of nested writes brecause We don't *automatically* support these sorts of nested writes because
there are too many ambiguities to define a default behavior. there are too many ambiguities to define a default behavior.
Eg. Suppose we have a `UserSerializer` with a nested profile. How should Eg. Suppose we have a `UserSerializer` with a nested profile. How should
we handle the case of an update, where the `profile` realtionship does we handle the case of an update, where the `profile` relationship does
not exist? Any of the following might be valid: not exist? Any of the following might be valid:
* Raise an application error. * Raise an application error.

View File

@ -143,7 +143,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

@ -168,6 +168,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

@ -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 = [ urlpatterns = [
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'), url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),

View File

@ -21,8 +21,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

@ -32,8 +32,13 @@ class RequestInvalidVersionView(APIView):
factory = APIRequestFactory() factory = APIRequestFactory()
dummy_view = lambda request: None
dummy_pk_view = lambda request, pk: None def dummy_view(request):
pass
def dummy_pk_view(request, pk):
pass
class TestRequestVersion: class TestRequestVersion:

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
django-guardian==1.2.4 django-guardian==1.2.4
pytest-django==2.8.0 pytest-django==2.8.0
django-filter==0.9.2 django-filter==0.9.2