mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-02 20:54:42 +03:00
Merge master
This commit is contained in:
commit
fbb21caaaa
19
.gitignore
vendored
19
.gitignore
vendored
|
@ -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
|
||||||
|
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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…
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.'))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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'})
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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,)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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
|
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user