mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-29 13:04:03 +03:00
Merge master
This commit is contained in:
commit
de301f3b66
55
.travis.yml
55
.travis.yml
|
@ -1,43 +1,28 @@
|
||||||
language: python
|
language: python
|
||||||
|
|
||||||
python:
|
python: 2.7
|
||||||
- "2.6"
|
|
||||||
- "2.7"
|
|
||||||
- "3.2"
|
|
||||||
- "3.3"
|
|
||||||
- "3.4"
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
|
- TOX_ENV=flake8
|
||||||
- DJANGO="django==1.6.5"
|
- TOX_ENV=py3.4-django1.7
|
||||||
- DJANGO="django==1.5.8"
|
- TOX_ENV=py3.3-django1.7
|
||||||
- DJANGO="django==1.4.13"
|
- TOX_ENV=py3.2-django1.7
|
||||||
|
- TOX_ENV=py2.7-django1.7
|
||||||
|
- TOX_ENV=py3.4-django1.6
|
||||||
|
- TOX_ENV=py3.3-django1.6
|
||||||
|
- TOX_ENV=py3.2-django1.6
|
||||||
|
- TOX_ENV=py2.7-django1.6
|
||||||
|
- TOX_ENV=py2.6-django1.6
|
||||||
|
- TOX_ENV=py3.4-django1.5
|
||||||
|
- TOX_ENV=py3.3-django1.5
|
||||||
|
- TOX_ENV=py3.2-django1.5
|
||||||
|
- TOX_ENV=py2.7-django1.5
|
||||||
|
- TOX_ENV=py2.6-django1.5
|
||||||
|
- TOX_ENV=py2.7-django1.4
|
||||||
|
- TOX_ENV=py2.6-django1.4
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install $DJANGO
|
- "pip install tox --download-cache $HOME/.pip-cache"
|
||||||
- pip install defusedxml==0.3
|
|
||||||
- pip install Pillow==2.3.0
|
|
||||||
- pip install django-guardian==1.2.3
|
|
||||||
- pip install pytest-django==2.6.1
|
|
||||||
- pip install flake8==2.2.2
|
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
|
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi"
|
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
|
|
||||||
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
|
|
||||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
|
|
||||||
- "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7c2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
|
||||||
- export PYTHONPATH=.
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./runtests.py
|
- tox -e $TOX_ENV
|
||||||
|
|
||||||
matrix:
|
|
||||||
exclude:
|
|
||||||
- python: "2.6"
|
|
||||||
env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
|
|
||||||
- python: "3.2"
|
|
||||||
env: DJANGO="django==1.4.13"
|
|
||||||
- python: "3.3"
|
|
||||||
env: DJANGO="django==1.4.13"
|
|
||||||
- python: "3.4"
|
|
||||||
env: DJANGO="django==1.4.13"
|
|
||||||
|
|
|
@ -62,10 +62,10 @@ To run the tests, clone the repository, and then:
|
||||||
virtualenv env
|
virtualenv env
|
||||||
env/bin/activate
|
env/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -r optionals.txt
|
pip install -r requirements-test.txt
|
||||||
|
|
||||||
# Run the tests
|
# Run the tests
|
||||||
py.test
|
./runtests.py
|
||||||
|
|
||||||
You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
recursive-include rest_framework/static *.js *.css *.png
|
recursive-include rest_framework/static *.js *.css *.png
|
||||||
recursive-include rest_framework/templates *.html
|
recursive-include rest_framework/templates *.html
|
||||||
|
recursive-exclude * __pycache__
|
||||||
|
recursive-exclude * *.py[co]
|
||||||
|
|
|
@ -50,7 +50,7 @@ Startup up a new project like so...
|
||||||
|
|
||||||
pip install django
|
pip install django
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
django-admin startproject example .
|
django-admin.py startproject example .
|
||||||
./manage.py syncdb
|
./manage.py syncdb
|
||||||
|
|
||||||
Now edit the `example/urls.py` module in your project:
|
Now edit the `example/urls.py` module in your project:
|
||||||
|
|
|
@ -193,7 +193,7 @@ filters using `Manufacturer` name. For example:
|
||||||
class ProductFilter(django_filters.FilterSet):
|
class ProductFilter(django_filters.FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = ['category', 'in_stock', 'manufacturer__name`]
|
fields = ['category', 'in_stock', 'manufacturer__name']
|
||||||
|
|
||||||
This enables us to make queries like:
|
This enables us to make queries like:
|
||||||
|
|
||||||
|
@ -211,7 +211,7 @@ This is nice, but it exposes the Django's double underscore convention as part o
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = ['category', 'in_stock', 'manufacturer`]
|
fields = ['category', 'in_stock', 'manufacturer']
|
||||||
|
|
||||||
And now you can execute:
|
And now you can execute:
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ The optional authtoken application now includes support for *both* Django 1.7 sc
|
||||||
|
|
||||||
## Deprecation of `.model` view attribute
|
## Deprecation of `.model` view attribute
|
||||||
|
|
||||||
The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. It's usage results in more implicit, less obvious behavior.
|
The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. Its usage results in more implicit, less obvious behavior.
|
||||||
|
|
||||||
The documentation has previously stated that usage of the more explicit style is prefered, and we're now taking that one step further and deprecating the usage of the `.model` shortcut.
|
The documentation has previously stated that usage of the more explicit style is prefered, and we're now taking that one step further and deprecating the usage of the `.model` shortcut.
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ There are also a number of other features and bugfixes as [listed in the release
|
||||||
|
|
||||||
Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting.
|
Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting.
|
||||||
|
|
||||||
Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Trottle-Wait-Seconds` header which will be fully deprecated in 3.0.
|
Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Throttle-Wait-Seconds` header which will be fully deprecated in 3.0.
|
||||||
|
|
||||||
## Deprecations
|
## Deprecations
|
||||||
|
|
||||||
|
@ -163,10 +163,10 @@ The next planned release will be 3.0, featuring an improved and simplified seria
|
||||||
Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible!
|
Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible!
|
||||||
|
|
||||||
[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases
|
[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases
|
||||||
[2-4-release-notes]: ./topics/release-notes/#240
|
[2-4-release-notes]: release-notes#240
|
||||||
[view-name-and-description-settings]: ../api-guide/settings/#view-names-and-descriptions
|
[view-name-and-description-settings]: ../api-guide/settings/#view-names-and-descriptions
|
||||||
[client-ip-identification]: ../api-guide/throttling/#how-clients-are-identified
|
[client-ip-identification]: ../api-guide/throttling/#how-clients-are-identified
|
||||||
[2-3-announcement]: ./topics/2.3-announcement
|
[2-3-announcement]: 2.3-announcement
|
||||||
[github-labels]: https://github.com/tomchristie/django-rest-framework/issues
|
[github-labels]: https://github.com/tomchristie/django-rest-framework/issues
|
||||||
[github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones
|
[github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones
|
||||||
[kickstarter-sponsors]: ./topics/kickstarter-announcement/#sponsors
|
[kickstarter-sponsors]: kickstarter-announcement#sponsors
|
||||||
|
|
|
@ -40,6 +40,18 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.4.x series
|
## 2.4.x series
|
||||||
|
|
||||||
|
### 2.4.2
|
||||||
|
|
||||||
|
**Date**: 3rd September 2014
|
||||||
|
|
||||||
|
* Bugfix: Fix broken pagination for 2.4.x series.
|
||||||
|
|
||||||
|
### 2.4.1
|
||||||
|
|
||||||
|
**Date**: 1st September 2014
|
||||||
|
|
||||||
|
* Bugfix: Fix broken login template for browsable API.
|
||||||
|
|
||||||
### 2.4.0
|
### 2.4.0
|
||||||
|
|
||||||
**Date**: 29th August 2014
|
**Date**: 29th August 2014
|
||||||
|
|
|
@ -8,6 +8,7 @@ flake8==2.2.2
|
||||||
markdown>=2.1.0
|
markdown>=2.1.0
|
||||||
PyYAML>=3.10
|
PyYAML>=3.10
|
||||||
defusedxml>=0.3
|
defusedxml>=0.3
|
||||||
|
django-guardian==1.2.4
|
||||||
django-filter>=0.5.4
|
django-filter>=0.5.4
|
||||||
django-oauth-plus>=2.2.1
|
django-oauth-plus>=2.2.1
|
||||||
oauth2>=1.5.211
|
oauth2>=1.5.211
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Django>=1.3
|
Django>=1.4.2
|
||||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '2.4.0'
|
__version__ = '2.4.2'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||||
|
|
|
@ -344,7 +344,7 @@ class OAuth2Authentication(BaseAuthentication):
|
||||||
user = token.user
|
user = token.user
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
msg = 'User inactive or deleted: %s' % user.username
|
msg = 'User inactive or deleted: %s' % user.get_username()
|
||||||
raise exceptions.AuthenticationFailed(msg)
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
return (user, token)
|
return (user, token)
|
||||||
|
|
|
@ -51,8 +51,11 @@ def get_attribute(instance, attrs):
|
||||||
for attr in attrs:
|
for attr in attrs:
|
||||||
try:
|
try:
|
||||||
instance = getattr(instance, attr)
|
instance = getattr(instance, attr)
|
||||||
except AttributeError:
|
except AttributeError as exc:
|
||||||
|
try:
|
||||||
return instance[attr]
|
return instance[attr]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise exc
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,6 @@ class DjangoFilterBackend(BaseFilterBackend):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = queryset.model
|
model = queryset.model
|
||||||
fields = filter_fields
|
fields = filter_fields
|
||||||
order_by = True
|
|
||||||
return AutoFilterSet
|
return AutoFilterSet
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -43,8 +43,9 @@ class DefaultObjectSerializer(serializers.Field):
|
||||||
as the default.
|
as the default.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, source=None, context=None):
|
def __init__(self, source=None, many=None, context=None):
|
||||||
# Note: Swallow context kwarg - only required for eg. ModelSerializer.
|
# Note: Swallow context and many kwargs - only required for
|
||||||
|
# eg. ModelSerializer.
|
||||||
super(DefaultObjectSerializer, self).__init__(source=source)
|
super(DefaultObjectSerializer, self).__init__(source=source)
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,6 +62,7 @@ class BasePaginationSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
super(BasePaginationSerializer, self).__init__(*args, **kwargs)
|
super(BasePaginationSerializer, self).__init__(*args, **kwargs)
|
||||||
results_field = self.results_field
|
results_field = self.results_field
|
||||||
|
|
||||||
try:
|
try:
|
||||||
object_serializer = self.Meta.object_serializer_class
|
object_serializer = self.Meta.object_serializer_class
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -70,7 +72,7 @@ class BasePaginationSerializer(serializers.Serializer):
|
||||||
child=object_serializer(),
|
child=object_serializer(),
|
||||||
source='object_list'
|
source='object_list'
|
||||||
)
|
)
|
||||||
self.fields[results_field].bind(results_field, self, self) # TODO: Support automatic binding
|
self.fields[results_field].bind(results_field, self, self)
|
||||||
|
|
||||||
|
|
||||||
class PaginationSerializer(BasePaginationSerializer):
|
class PaginationSerializer(BasePaginationSerializer):
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.http import QueryDict
|
||||||
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
||||||
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
|
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from rest_framework.compat import etree, yaml, force_text
|
from rest_framework.compat import etree, yaml, force_text, urlparse
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework import renderers
|
from rest_framework import renderers
|
||||||
import json
|
import json
|
||||||
|
@ -290,6 +290,22 @@ class FileUploadParser(BaseParser):
|
||||||
try:
|
try:
|
||||||
meta = parser_context['request'].META
|
meta = parser_context['request'].META
|
||||||
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
|
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
|
||||||
return force_text(disposition[1]['filename'])
|
filename_parm = disposition[1]
|
||||||
|
if 'filename*' in filename_parm:
|
||||||
|
return self.get_encoded_filename(filename_parm)
|
||||||
|
return force_text(filename_parm['filename'])
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_encoded_filename(self, filename_parm):
|
||||||
|
"""
|
||||||
|
Handle encoded filenames per RFC6266. See also:
|
||||||
|
http://tools.ietf.org/html/rfc2231#section-4
|
||||||
|
"""
|
||||||
|
encoded_filename = force_text(filename_parm['filename*'])
|
||||||
|
try:
|
||||||
|
charset, lang, filename = encoded_filename.split('\'', 2)
|
||||||
|
filename = urlparse.unquote(filename)
|
||||||
|
except (ValueError, LookupError):
|
||||||
|
filename = force_text(filename_parm['filename'])
|
||||||
|
return filename
|
||||||
|
|
|
@ -284,10 +284,10 @@ class DefaultRouter(SimpleRouter):
|
||||||
class APIRoot(views.APIView):
|
class APIRoot(views.APIView):
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, *args, **kwargs):
|
||||||
ret = {}
|
ret = {}
|
||||||
for key, url_name in api_root_dict.items():
|
for key, url_name in api_root_dict.items():
|
||||||
ret[key] = reverse(url_name, request=request, format=format)
|
ret[key] = reverse(url_name, request=request, format=kwargs.get('format', None))
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
|
|
||||||
return APIRoot.as_view()
|
return APIRoot.as_view()
|
||||||
|
|
|
@ -317,6 +317,19 @@ class ModelSerializerOptions(object):
|
||||||
self.depth = getattr(meta, 'depth', 0)
|
self.depth = getattr(meta, 'depth', 0)
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_class(mapping, obj):
|
||||||
|
"""
|
||||||
|
Takes a dictionary with classes as keys, and an object.
|
||||||
|
Traverses the object's inheritance hierarchy in method
|
||||||
|
resolution order, and returns the first matching value
|
||||||
|
from the dictionary or None.
|
||||||
|
"""
|
||||||
|
return next(
|
||||||
|
(mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModelSerializer(Serializer):
|
class ModelSerializer(Serializer):
|
||||||
field_mapping = {
|
field_mapping = {
|
||||||
models.AutoField: IntegerField,
|
models.AutoField: IntegerField,
|
||||||
|
@ -580,13 +593,20 @@ class ModelSerializer(Serializer):
|
||||||
if decimal_places is not None:
|
if decimal_places is not None:
|
||||||
kwargs['decimal_places'] = decimal_places
|
kwargs['decimal_places'] = decimal_places
|
||||||
|
|
||||||
|
if isinstance(model_field, models.BooleanField):
|
||||||
|
# models.BooleanField has `blank=True`, but *is* actually
|
||||||
|
# required *unless* a default is provided.
|
||||||
|
# Also note that <1.6 `default=False`, >=1.6 `default=None`.
|
||||||
|
kwargs.pop('required', None)
|
||||||
|
|
||||||
if validator_kwarg:
|
if validator_kwarg:
|
||||||
kwargs['validators'] = validator_kwarg
|
kwargs['validators'] = validator_kwarg
|
||||||
|
|
||||||
try:
|
cls = lookup_class(self.field_mapping, model_field)
|
||||||
return self.field_mapping[model_field.__class__](**kwargs)
|
if cls is None:
|
||||||
except KeyError:
|
cls = ModelField
|
||||||
return ModelField(model_field=model_field, **kwargs)
|
kwargs['model_field'] = model_field
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
|
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
<body class="{% block bodyclass %}{% endblock %} container">
|
<body class="{% block bodyclass %}{% endblock %} container">
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
@ -43,17 +44,9 @@
|
||||||
<ul class="nav pull-right">
|
<ul class="nav pull-right">
|
||||||
{% block userlinks %}
|
{% block userlinks %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<li class="dropdown">
|
{% optional_logout request user %}
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
|
||||||
{{ user }}
|
|
||||||
<b class="caret"></b>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>{% optional_logout request %}</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>{% optional_login request %}</li>
|
{% optional_login request %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -261,4 +254,5 @@
|
||||||
<script src="{% static "rest_framework/js/default.js" %}"></script>
|
<script src="{% static "rest_framework/js/default.js" %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
{% endblock %}
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -17,21 +17,44 @@
|
||||||
|
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div>
|
<div>
|
||||||
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
|
<form action="{% url 'rest_framework:login' %}" role="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div id="div_id_username" class="clearfix control-group">
|
<div id="div_id_username"
|
||||||
|
class="clearfix control-group {% if form.username.errors %}error{% endif %}">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<Label class="span4">Username:</label>
|
<Label class="span4">Username:</label>
|
||||||
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
|
<input style="height: 25px" type="text" name="username" maxlength="100"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off" class="span12 textinput textInput"
|
||||||
|
id="id_username" required
|
||||||
|
{% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
|
||||||
|
{% if form.username.errors %}
|
||||||
|
<p class="text-error">
|
||||||
|
{{ form.username.errors|striptags }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="div_id_password" class="clearfix control-group">
|
<div id="div_id_password"
|
||||||
|
class="clearfix control-group {% if form.password.errors %}error{% endif %}">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<Label class="span4">Password:</label>
|
<Label class="span4">Password:</label>
|
||||||
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
<input style="height: 25px" type="password" name="password" maxlength="100"
|
||||||
|
autocapitalize="off" autocorrect="off" class="span12 textinput textInput"
|
||||||
|
id="id_password" required>
|
||||||
|
{% if form.password.errors %}
|
||||||
|
<p class="text-error">
|
||||||
|
{{ form.password.errors|striptags }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="next" value="{{ next }}" />
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
<div class="well well-small text-error" style="border: none">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
<div class="form-actions-no-box">
|
<div class="form-actions-no-box">
|
||||||
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
|
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,22 +41,31 @@ def optional_login(request):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
|
snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path))
|
||||||
return snippet
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def optional_logout(request):
|
def optional_logout(request, user):
|
||||||
"""
|
"""
|
||||||
Include a logout snippet if REST framework's logout view is in the URLconf.
|
Include a logout snippet if REST framework's logout view is in the URLconf.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logout_url = reverse('rest_framework:logout')
|
logout_url = reverse('rest_framework:logout')
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return '<li class="navbar-text">{user}</li>'.format(user=user)
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
|
snippet = """<li class="dropdown">
|
||||||
return snippet
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
|
{user}
|
||||||
|
<b class="caret"></b>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href='{href}?next={next}'>Log out</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>"""
|
||||||
|
|
||||||
|
return snippet.format(user=user, href=logout_url, next=escape(request.path))
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
|
|
@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
||||||
Encode the data returning a two tuple of (bytes, content_type)
|
Encode the data returning a two tuple of (bytes, content_type)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not data:
|
if data is None:
|
||||||
return ('', content_type)
|
return ('', content_type)
|
||||||
|
|
||||||
assert format is None or content_type is None, (
|
assert format is None or content_type is None, (
|
||||||
|
|
|
@ -6,7 +6,7 @@ your API requires authentication:
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
...
|
...
|
||||||
url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
)
|
)
|
||||||
|
|
||||||
The urls must be namespaced as 'rest_framework', and you should make sure
|
The urls must be namespaced as 'rest_framework', and you should make sure
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
Utility functions to return a formatted name and description for a given view.
|
Utility functions to return a formatted name and description for a given view.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import re
|
||||||
|
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from rest_framework.compat import apply_markdown
|
|
||||||
import re
|
from rest_framework.compat import apply_markdown, force_text
|
||||||
|
|
||||||
|
|
||||||
def remove_trailing_string(content, trailing):
|
def remove_trailing_string(content, trailing):
|
||||||
|
@ -28,6 +29,7 @@ def dedent(content):
|
||||||
as it fails to dedent multiline docstrings that include
|
as it fails to dedent multiline docstrings that include
|
||||||
unindented text on the initial line.
|
unindented text on the initial line.
|
||||||
"""
|
"""
|
||||||
|
content = force_text(content)
|
||||||
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
||||||
for line in content.splitlines()[1:] if line.lstrip()]
|
for line in content.splitlines()[1:] if line.lstrip()]
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,9 @@ class APIView(View):
|
||||||
"""
|
"""
|
||||||
view = super(APIView, cls).as_view(**initkwargs)
|
view = super(APIView, cls).as_view(**initkwargs)
|
||||||
view.cls = cls
|
view.cls = cls
|
||||||
return view
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
|
# all other authentication is CSRF exempt.
|
||||||
|
return csrf_exempt(view)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allowed_methods(self):
|
def allowed_methods(self):
|
||||||
|
@ -376,9 +378,9 @@ class APIView(View):
|
||||||
response.exception = True
|
response.exception = True
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
# Note: Views are made CSRF exempt from within `as_view` as to prevent
|
||||||
# all other authentication is CSRF exempt.
|
# accidental removal of this exemption in cases where `dispatch` needs to
|
||||||
@csrf_exempt
|
# be overridden.
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
`.dispatch()` is pretty much the same as Django's regular dispatch,
|
`.dispatch()` is pretty much the same as Django's regular dispatch,
|
||||||
|
|
0
tests/browsable_api/__init__.py
Normal file
0
tests/browsable_api/__init__.py
Normal file
10
tests/browsable_api/auth_urls.py
Normal file
10
tests/browsable_api/auth_urls.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.conf.urls import patterns, url, include
|
||||||
|
|
||||||
|
from .views import MockView
|
||||||
|
|
||||||
|
urlpatterns = patterns(
|
||||||
|
'',
|
||||||
|
(r'^$', MockView.as_view()),
|
||||||
|
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
|
)
|
9
tests/browsable_api/no_auth_urls.py
Normal file
9
tests/browsable_api/no_auth_urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.conf.urls import patterns
|
||||||
|
|
||||||
|
from .views import MockView
|
||||||
|
|
||||||
|
urlpatterns = patterns(
|
||||||
|
'',
|
||||||
|
(r'^$', MockView.as_view()),
|
||||||
|
)
|
65
tests/browsable_api/test_browsable_api.py
Normal file
65
tests/browsable_api/test_browsable_api.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
|
||||||
|
class DropdownWithAuthTests(TestCase):
|
||||||
|
"""Tests correct dropdown behaviour with Auth views enabled."""
|
||||||
|
|
||||||
|
urls = 'tests.browsable_api.auth_urls'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient(enforce_csrf_checks=True)
|
||||||
|
self.username = 'john'
|
||||||
|
self.email = 'lennon@thebeatles.com'
|
||||||
|
self.password = 'password'
|
||||||
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
def test_name_shown_when_logged_in(self):
|
||||||
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertContains(response, 'john')
|
||||||
|
|
||||||
|
def test_logout_shown_when_logged_in(self):
|
||||||
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertContains(response, '>Log out<')
|
||||||
|
|
||||||
|
def test_login_shown_when_logged_out(self):
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertContains(response, '>Log in<')
|
||||||
|
|
||||||
|
|
||||||
|
class NoDropdownWithoutAuthTests(TestCase):
|
||||||
|
"""Tests correct dropdown behaviour with Auth views NOT enabled."""
|
||||||
|
|
||||||
|
urls = 'tests.browsable_api.no_auth_urls'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient(enforce_csrf_checks=True)
|
||||||
|
self.username = 'john'
|
||||||
|
self.email = 'lennon@thebeatles.com'
|
||||||
|
self.password = 'password'
|
||||||
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
def test_name_shown_when_logged_in(self):
|
||||||
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertContains(response, 'john')
|
||||||
|
|
||||||
|
def test_dropdown_not_shown_when_logged_in(self):
|
||||||
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertNotContains(response, '<li class="dropdown">')
|
||||||
|
|
||||||
|
def test_dropdown_not_shown_when_logged_out(self):
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertNotContains(response, '<li class="dropdown">')
|
15
tests/browsable_api/views.py
Normal file
15
tests/browsable_api/views.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import authentication
|
||||||
|
from rest_framework import renderers
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class MockView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (authentication.SessionAuthentication,)
|
||||||
|
renderer_classes = (renderers.BrowsableAPIRenderer,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response({'a': 1, 'b': 2, 'c': 3})
|
|
@ -5,6 +5,7 @@ def pytest_configure():
|
||||||
DEBUG_PROPAGATE_EXCEPTIONS=True,
|
DEBUG_PROPAGATE_EXCEPTIONS=True,
|
||||||
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3',
|
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': ':memory:'}},
|
'NAME': ':memory:'}},
|
||||||
|
SITE_ID=1,
|
||||||
SECRET_KEY='not very secret in tests',
|
SECRET_KEY='not very secret in tests',
|
||||||
USE_I18N=True,
|
USE_I18N=True,
|
||||||
USE_L10N=True,
|
USE_L10N=True,
|
||||||
|
|
|
@ -57,7 +57,8 @@ urlpatterns = patterns(
|
||||||
authentication_classes=[OAuthAuthentication],
|
authentication_classes=[OAuthAuthentication],
|
||||||
permission_classes=[permissions.TokenHasReadWriteScope]
|
permission_classes=[permissions.TokenHasReadWriteScope]
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,6 +135,15 @@ class SessionAuthTests(TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.csrf_client.logout()
|
self.csrf_client.logout()
|
||||||
|
|
||||||
|
def test_login_view_renders_on_get(self):
|
||||||
|
"""
|
||||||
|
Ensure the login template renders for a basic GET.
|
||||||
|
|
||||||
|
cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810)
|
||||||
|
"""
|
||||||
|
response = self.csrf_client.get('/auth/login/')
|
||||||
|
self.assertContains(response, '<Label class="span4">Username:</label>')
|
||||||
|
|
||||||
def test_post_form_session_auth_failing_csrf(self):
|
def test_post_form_session_auth_failing_csrf(self):
|
||||||
"""
|
"""
|
||||||
Ensure POSTing form over session authentication without CSRF token fails.
|
Ensure POSTing form over session authentication without CSRF token fails.
|
||||||
|
|
|
@ -98,6 +98,30 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
pass
|
pass
|
||||||
self.assertEqual(MockView().get_view_description(), '')
|
self.assertEqual(MockView().get_view_description(), '')
|
||||||
|
|
||||||
|
def test_view_description_can_be_promise(self):
|
||||||
|
"""
|
||||||
|
Ensure a view may have a docstring that is actually a lazily evaluated
|
||||||
|
class that can be converted to a string.
|
||||||
|
|
||||||
|
See: https://github.com/tomchristie/django-rest-framework/issues/1708
|
||||||
|
"""
|
||||||
|
# use a mock object instead of gettext_lazy to ensure that we can't end
|
||||||
|
# up with a test case string in our l10n catalog
|
||||||
|
class MockLazyStr(object):
|
||||||
|
def __init__(self, string):
|
||||||
|
self.s = string
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.s
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.s
|
||||||
|
|
||||||
|
class MockView(APIView):
|
||||||
|
__doc__ = MockLazyStr("a gettext string")
|
||||||
|
|
||||||
|
self.assertEqual(MockView().get_view_description(), 'a gettext string')
|
||||||
|
|
||||||
def test_markdown(self):
|
def test_markdown(self):
|
||||||
"""
|
"""
|
||||||
Ensure markdown to HTML works as expected.
|
Ensure markdown to HTML works as expected.
|
||||||
|
|
|
@ -1004,6 +1004,26 @@
|
||||||
# self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
|
# self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
|
||||||
|
|
||||||
|
|
||||||
|
# class SerializerMethodFieldTest(TestCase):
|
||||||
|
# """
|
||||||
|
# Tests for the SerializerMethodField field_to_native() behavior
|
||||||
|
# """
|
||||||
|
# class SerializerTest(serializers.Serializer):
|
||||||
|
# def get_my_test(self, obj):
|
||||||
|
# return obj.my_test[0:5]
|
||||||
|
|
||||||
|
# class ModelCharField(TestCase):
|
||||||
|
# """
|
||||||
|
# Tests for CharField
|
||||||
|
# """
|
||||||
|
# def test_none_serializing(self):
|
||||||
|
# class CharFieldSerializer(serializers.Serializer):
|
||||||
|
# char = serializers.CharField(allow_none=True, required=False)
|
||||||
|
# serializer = CharFieldSerializer(data={'char': None})
|
||||||
|
# self.assertTrue(serializer.is_valid())
|
||||||
|
# self.assertIsNone(serializer.object['char'])
|
||||||
|
|
||||||
|
|
||||||
# class SerializerMethodFieldTest(TestCase):
|
# class SerializerMethodFieldTest(TestCase):
|
||||||
# """
|
# """
|
||||||
# Tests for the SerializerMethodField field_to_native() behavior
|
# Tests for the SerializerMethodField field_to_native() behavior
|
||||||
|
|
|
@ -422,19 +422,70 @@ class SearchFilterTests(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrdringFilterModel(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)
|
||||||
|
|
||||||
|
|
||||||
class OrderingFilterRelatedModel(models.Model):
|
class OrderingFilterRelatedModel(models.Model):
|
||||||
related_object = models.ForeignKey(OrdringFilterModel,
|
related_object = models.ForeignKey(OrderingFilterModel,
|
||||||
related_name="relateds")
|
related_name="relateds")
|
||||||
|
|
||||||
|
|
||||||
class OrderingFilterSerializer(serializers.ModelSerializer):
|
class OrderingFilterSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrdringFilterModel
|
model = OrderingFilterModel
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoFilterOrderingModel(models.Model):
|
||||||
|
date = models.DateField()
|
||||||
|
text = models.CharField(max_length=10)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-date']
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = DjangoFilterOrderingModel
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoFilterOrderingTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
data = [{
|
||||||
|
'date': datetime.date(2012, 10, 8),
|
||||||
|
'text': 'abc'
|
||||||
|
}, {
|
||||||
|
'date': datetime.date(2013, 10, 8),
|
||||||
|
'text': 'bcd'
|
||||||
|
}, {
|
||||||
|
'date': datetime.date(2014, 10, 8),
|
||||||
|
'text': 'cde'
|
||||||
|
}]
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
DjangoFilterOrderingModel.objects.create(**d)
|
||||||
|
|
||||||
|
def test_default_ordering(self):
|
||||||
|
class DjangoFilterOrderingView(generics.ListAPIView):
|
||||||
|
serializer_class = DjangoFilterOrderingSerializer
|
||||||
|
queryset = DjangoFilterOrderingModel.objects.all()
|
||||||
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
filter_fields = ['text']
|
||||||
|
ordering = ('-date',)
|
||||||
|
|
||||||
|
view = DjangoFilterOrderingView.as_view()
|
||||||
|
request = factory.get('/')
|
||||||
|
response = view(request)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 3, 'date': datetime.date(2014, 10, 8), 'text': 'cde'},
|
||||||
|
{'id': 2, 'date': datetime.date(2013, 10, 8), 'text': 'bcd'},
|
||||||
|
{'id': 1, 'date': datetime.date(2012, 10, 8), 'text': 'abc'}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrderingFilterTests(TestCase):
|
class OrderingFilterTests(TestCase):
|
||||||
|
@ -455,11 +506,11 @@ class OrderingFilterTests(TestCase):
|
||||||
chr(idx + ord('b')) +
|
chr(idx + ord('b')) +
|
||||||
chr(idx + ord('c'))
|
chr(idx + ord('c'))
|
||||||
)
|
)
|
||||||
OrdringFilterModel(title=title, text=text).save()
|
OrderingFilterModel(title=title, text=text).save()
|
||||||
|
|
||||||
def test_ordering(self):
|
def test_ordering(self):
|
||||||
class OrderingListView(generics.ListAPIView):
|
class OrderingListView(generics.ListAPIView):
|
||||||
queryset = OrdringFilterModel.objects.all()
|
queryset = OrderingFilterModel.objects.all()
|
||||||
serializer_class = OrderingFilterSerializer
|
serializer_class = OrderingFilterSerializer
|
||||||
filter_backends = (filters.OrderingFilter,)
|
filter_backends = (filters.OrderingFilter,)
|
||||||
ordering = ('title',)
|
ordering = ('title',)
|
||||||
|
@ -479,7 +530,7 @@ class OrderingFilterTests(TestCase):
|
||||||
|
|
||||||
def test_reverse_ordering(self):
|
def test_reverse_ordering(self):
|
||||||
class OrderingListView(generics.ListAPIView):
|
class OrderingListView(generics.ListAPIView):
|
||||||
queryset = OrdringFilterModel.objects.all()
|
queryset = OrderingFilterModel.objects.all()
|
||||||
serializer_class = OrderingFilterSerializer
|
serializer_class = OrderingFilterSerializer
|
||||||
filter_backends = (filters.OrderingFilter,)
|
filter_backends = (filters.OrderingFilter,)
|
||||||
ordering = ('title',)
|
ordering = ('title',)
|
||||||
|
@ -499,7 +550,7 @@ class OrderingFilterTests(TestCase):
|
||||||
|
|
||||||
def test_incorrectfield_ordering(self):
|
def test_incorrectfield_ordering(self):
|
||||||
class OrderingListView(generics.ListAPIView):
|
class OrderingListView(generics.ListAPIView):
|
||||||
queryset = OrdringFilterModel.objects.all()
|
queryset = OrderingFilterModel.objects.all()
|
||||||
serializer_class = OrderingFilterSerializer
|
serializer_class = OrderingFilterSerializer
|
||||||
filter_backends = (filters.OrderingFilter,)
|
filter_backends = (filters.OrderingFilter,)
|
||||||
ordering = ('title',)
|
ordering = ('title',)
|
||||||
|
@ -519,7 +570,7 @@ class OrderingFilterTests(TestCase):
|
||||||
|
|
||||||
def test_default_ordering(self):
|
def test_default_ordering(self):
|
||||||
class OrderingListView(generics.ListAPIView):
|
class OrderingListView(generics.ListAPIView):
|
||||||
queryset = OrdringFilterModel.objects.all()
|
queryset = OrderingFilterModel.objects.all()
|
||||||
serializer_class = OrderingFilterSerializer
|
serializer_class = OrderingFilterSerializer
|
||||||
filter_backends = (filters.OrderingFilter,)
|
filter_backends = (filters.OrderingFilter,)
|
||||||
ordering = ('title',)
|
ordering = ('title',)
|
||||||
|
@ -539,7 +590,7 @@ class OrderingFilterTests(TestCase):
|
||||||
|
|
||||||
def test_default_ordering_using_string(self):
|
def test_default_ordering_using_string(self):
|
||||||
class OrderingListView(generics.ListAPIView):
|
class OrderingListView(generics.ListAPIView):
|
||||||
queryset = OrdringFilterModel.objects.all()
|
queryset = OrderingFilterModel.objects.all()
|
||||||
serializer_class = OrderingFilterSerializer
|
serializer_class = OrderingFilterSerializer
|
||||||
filter_backends = (filters.OrderingFilter,)
|
filter_backends = (filters.OrderingFilter,)
|
||||||
ordering = 'title'
|
ordering = 'title'
|
||||||
|
@ -560,7 +611,7 @@ class OrderingFilterTests(TestCase):
|
||||||
def test_ordering_by_aggregate_field(self):
|
def test_ordering_by_aggregate_field(self):
|
||||||
# create some related models to aggregate order by
|
# create some related models to aggregate order by
|
||||||
num_objs = [2, 5, 3]
|
num_objs = [2, 5, 3]
|
||||||
for obj, num_relateds in zip(OrdringFilterModel.objects.all(),
|
for obj, num_relateds in zip(OrderingFilterModel.objects.all(),
|
||||||
num_objs):
|
num_objs):
|
||||||
for _ in range(num_relateds):
|
for _ in range(num_relateds):
|
||||||
new_related = OrderingFilterRelatedModel(
|
new_related = OrderingFilterRelatedModel(
|
||||||
|
@ -573,7 +624,7 @@ class OrderingFilterTests(TestCase):
|
||||||
filter_backends = (filters.OrderingFilter,)
|
filter_backends = (filters.OrderingFilter,)
|
||||||
ordering = 'title'
|
ordering = 'title'
|
||||||
ordering_fields = '__all__'
|
ordering_fields = '__all__'
|
||||||
queryset = OrdringFilterModel.objects.all().annotate(
|
queryset = OrderingFilterModel.objects.all().annotate(
|
||||||
models.Count("relateds"))
|
models.Count("relateds"))
|
||||||
|
|
||||||
view = OrderingListView.as_view()
|
view = OrderingListView.as_view()
|
||||||
|
@ -591,7 +642,7 @@ class OrderingFilterTests(TestCase):
|
||||||
def test_ordering_with_nonstandard_ordering_param(self):
|
def test_ordering_with_nonstandard_ordering_param(self):
|
||||||
with temporary_setting('ORDERING_PARAM', 'order', filters):
|
with temporary_setting('ORDERING_PARAM', 'order', filters):
|
||||||
class OrderingListView(generics.ListAPIView):
|
class OrderingListView(generics.ListAPIView):
|
||||||
queryset = OrdringFilterModel.objects.all()
|
queryset = OrderingFilterModel.objects.all()
|
||||||
serializer_class = OrderingFilterSerializer
|
serializer_class = OrderingFilterSerializer
|
||||||
filter_backends = (filters.OrderingFilter,)
|
filter_backends = (filters.OrderingFilter,)
|
||||||
ordering = ('title',)
|
ordering = ('title',)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -176,6 +177,9 @@ class TestRootView(TestCase):
|
||||||
self.assertEqual(created.text, 'foobar')
|
self.assertEqual(created.text, 'foobar')
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_QUERYS_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
|
||||||
|
|
||||||
|
|
||||||
class TestInstanceView(TestCase):
|
class TestInstanceView(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -219,7 +223,7 @@ class TestInstanceView(TestCase):
|
||||||
"""
|
"""
|
||||||
data = {'text': 'foobar'}
|
data = {'text': 'foobar'}
|
||||||
request = factory.put('/1', data, format='json')
|
request = factory.put('/1', data, format='json')
|
||||||
with self.assertNumQueries(3):
|
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
||||||
response = self.view(request, pk='1').render()
|
response = self.view(request, pk='1').render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
|
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
|
||||||
|
@ -233,7 +237,7 @@ class TestInstanceView(TestCase):
|
||||||
data = {'text': 'foobar'}
|
data = {'text': 'foobar'}
|
||||||
request = factory.patch('/1', data, format='json')
|
request = factory.patch('/1', data, format='json')
|
||||||
|
|
||||||
with self.assertNumQueries(3):
|
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
||||||
response = self.view(request, pk=1).render()
|
response = self.view(request, pk=1).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
|
@ -351,7 +355,7 @@ class TestInstanceView(TestCase):
|
||||||
"""
|
"""
|
||||||
data = {'id': 999, 'text': 'foobar'}
|
data = {'id': 999, 'text': 'foobar'}
|
||||||
request = factory.put('/1', data, format='json')
|
request = factory.put('/1', data, format='json')
|
||||||
with self.assertNumQueries(3):
|
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
||||||
response = self.view(request, pk=1).render()
|
response = self.view(request, pk=1).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
|
|
|
@ -15,7 +15,7 @@ from rest_framework import serializers
|
||||||
class RegularFieldsModel(models.Model):
|
class RegularFieldsModel(models.Model):
|
||||||
auto_field = models.AutoField(primary_key=True)
|
auto_field = models.AutoField(primary_key=True)
|
||||||
big_integer_field = models.BigIntegerField()
|
big_integer_field = models.BigIntegerField()
|
||||||
boolean_field = models.BooleanField()
|
boolean_field = models.BooleanField(default=False)
|
||||||
char_field = models.CharField(max_length=100)
|
char_field = models.CharField(max_length=100)
|
||||||
comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
|
comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
|
||||||
date_field = models.DateField()
|
date_field = models.DateField()
|
||||||
|
@ -60,22 +60,22 @@ TestSerializer():
|
||||||
|
|
||||||
# Model for testing relational field mapping
|
# Model for testing relational field mapping
|
||||||
|
|
||||||
class ForeignKeyTarget(models.Model):
|
class ForeignKeyTargetModel(models.Model):
|
||||||
char_field = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
class ManyToManyTarget(models.Model):
|
class ManyToManyTargetModel(models.Model):
|
||||||
char_field = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
class OneToOneTarget(models.Model):
|
class OneToOneTargetModel(models.Model):
|
||||||
char_field = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
class RelationalModel(models.Model):
|
class RelationalModel(models.Model):
|
||||||
foreign_key = models.ForeignKey(ForeignKeyTarget)
|
foreign_key = models.ForeignKey(ForeignKeyTargetModel)
|
||||||
many_to_many = models.ManyToManyField(ManyToManyTarget)
|
many_to_many = models.ManyToManyField(ManyToManyTargetModel)
|
||||||
one_to_one = models.OneToOneField(OneToOneTarget)
|
one_to_one = models.OneToOneField(OneToOneTargetModel)
|
||||||
|
|
||||||
|
|
||||||
RELATIONAL_FLAT_REPR = """
|
RELATIONAL_FLAT_REPR = """
|
||||||
|
@ -105,9 +105,9 @@ TestSerializer():
|
||||||
HYPERLINKED_FLAT_REPR = """
|
HYPERLINKED_FLAT_REPR = """
|
||||||
TestSerializer():
|
TestSerializer():
|
||||||
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
|
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
|
||||||
foreign_key = HyperlinkedRelatedField(label='foreign key', queryset=<django.db.models.manager.Manager object>, view_name='foreignkeytarget-detail')
|
foreign_key = HyperlinkedRelatedField(label='foreign key', queryset=<django.db.models.manager.Manager object>, view_name='foreignkeytargetmodel-detail')
|
||||||
one_to_one = HyperlinkedRelatedField(label='one to one', queryset=<django.db.models.manager.Manager object>, view_name='onetoonetarget-detail')
|
one_to_one = HyperlinkedRelatedField(label='one to one', queryset=<django.db.models.manager.Manager object>, view_name='onetoonetargetmodel-detail')
|
||||||
many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=<django.db.models.manager.Manager object>, view_name='manytomanytarget-detail')
|
many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=<django.db.models.manager.Manager object>, view_name='manytomanytargetmodel-detail')
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ -127,6 +127,8 @@ TestSerializer():
|
||||||
|
|
||||||
|
|
||||||
class TestSerializerMappings(TestCase):
|
class TestSerializerMappings(TestCase):
|
||||||
|
maxDiff = 10000
|
||||||
|
|
||||||
def test_regular_fields(self):
|
def test_regular_fields(self):
|
||||||
class TestSerializer(serializers.ModelSerializer):
|
class TestSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -428,6 +428,15 @@ class CustomPaginationSerializer(pagination.BasePaginationSerializer):
|
||||||
results_field = 'objects'
|
results_field = 'objects'
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFooSerializer(serializers.Serializer):
|
||||||
|
foo = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFooPaginationSerializer(pagination.PaginationSerializer):
|
||||||
|
class Meta:
|
||||||
|
object_serializer_class = CustomFooSerializer
|
||||||
|
|
||||||
|
|
||||||
class TestCustomPaginationSerializer(TestCase):
|
class TestCustomPaginationSerializer(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
objects = ['john', 'paul', 'george', 'ringo']
|
objects = ['john', 'paul', 'george', 'ringo']
|
||||||
|
@ -450,6 +459,16 @@ class TestCustomPaginationSerializer(TestCase):
|
||||||
}
|
}
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_custom_pagination_serializer_with_custom_object_serializer(self):
|
||||||
|
objects = [
|
||||||
|
{'foo': 'bar'},
|
||||||
|
{'foo': 'spam'}
|
||||||
|
]
|
||||||
|
paginator = Paginator(objects, 1)
|
||||||
|
page = paginator.page(1)
|
||||||
|
serializer = CustomFooPaginationSerializer(page)
|
||||||
|
serializer.data
|
||||||
|
|
||||||
|
|
||||||
class NonIntegerPage(object):
|
class NonIntegerPage(object):
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from rest_framework.compat import StringIO
|
from rest_framework.compat import StringIO
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -113,3 +115,25 @@ class TestFileUploadParser(TestCase):
|
||||||
parser = FileUploadParser()
|
parser = FileUploadParser()
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'file.txt')
|
self.assertEqual(filename, 'file.txt')
|
||||||
|
|
||||||
|
def test_get_encoded_filename(self):
|
||||||
|
parser = FileUploadParser()
|
||||||
|
|
||||||
|
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
|
||||||
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
|
self.assertEqual(filename, 'ÀĥƦ.txt')
|
||||||
|
|
||||||
|
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
|
||||||
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
|
self.assertEqual(filename, 'ÀĥƦ.txt')
|
||||||
|
|
||||||
|
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
|
||||||
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
|
self.assertEqual(filename, 'ÀĥƦ.txt')
|
||||||
|
|
||||||
|
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt')
|
||||||
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
|
self.assertEqual(filename, 'fallback.txt')
|
||||||
|
|
||||||
|
def __replace_content_disposition(self, disposition):
|
||||||
|
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.core.validators import MaxValueValidator
|
from django.core.validators import MaxValueValidator
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import generics, serializers, status
|
from rest_framework import generics, serializers, status
|
||||||
|
@ -132,3 +133,42 @@ class TestMaxValueValidatorValidation(TestCase):
|
||||||
response = view(request, pk=obj.pk).render()
|
response = view(request, pk=obj.pk).render()
|
||||||
self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}')
|
self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}')
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class TestChoiceFieldChoicesValidate(TestCase):
|
||||||
|
CHOICES = [
|
||||||
|
(0, 'Small'),
|
||||||
|
(1, 'Medium'),
|
||||||
|
(2, 'Large'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CHOICES_NESTED = [
|
||||||
|
('Category', (
|
||||||
|
(1, 'First'),
|
||||||
|
(2, 'Second'),
|
||||||
|
(3, 'Third'),
|
||||||
|
)),
|
||||||
|
(4, 'Fourth'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_choices(self):
|
||||||
|
"""
|
||||||
|
Make sure a value for choices works as expected.
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(choices=self.CHOICES)
|
||||||
|
value = self.CHOICES[0][0]
|
||||||
|
try:
|
||||||
|
f.to_native(value)
|
||||||
|
except ValidationError:
|
||||||
|
self.fail("Value %s does not validate" % str(value))
|
||||||
|
|
||||||
|
# def test_nested_choices(self):
|
||||||
|
# """
|
||||||
|
# Make sure a nested value for choices works as expected.
|
||||||
|
# """
|
||||||
|
# f = serializers.ChoiceField(choices=self.CHOICES_NESTED)
|
||||||
|
# value = self.CHOICES_NESTED[0][1][0][0]
|
||||||
|
# try:
|
||||||
|
# f.to_native(value)
|
||||||
|
# except ValidationError:
|
||||||
|
# self.fail("Value %s does not validate" % str(value))
|
||||||
|
|
8
tox.ini
8
tox.ini
|
@ -18,7 +18,7 @@ commands = ./runtests.py --lintonly
|
||||||
|
|
||||||
[testenv:py3.4-django1.7]
|
[testenv:py3.4-django1.7]
|
||||||
basepython = python3.4
|
basepython = python3.4
|
||||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
deps = Django==1.7
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
Pillow==2.3.0
|
||||||
|
@ -26,7 +26,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
||||||
|
|
||||||
[testenv:py3.3-django1.7]
|
[testenv:py3.3-django1.7]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
deps = Django==1.7
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
Pillow==2.3.0
|
||||||
|
@ -34,7 +34,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
||||||
|
|
||||||
[testenv:py3.2-django1.7]
|
[testenv:py3.2-django1.7]
|
||||||
basepython = python3.2
|
basepython = python3.2
|
||||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
deps = Django==1.7
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
Pillow==2.3.0
|
||||||
|
@ -42,7 +42,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
||||||
|
|
||||||
[testenv:py2.7-django1.7]
|
[testenv:py2.7-django1.7]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
deps = Django==1.7
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
# django-oauth-plus==2.2.1
|
# django-oauth-plus==2.2.1
|
||||||
|
|
Loading…
Reference in New Issue
Block a user