mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 19:14:01 +03:00
Merge master
This commit is contained in:
commit
de301f3b66
55
.travis.yml
55
.travis.yml
|
@ -1,43 +1,28 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
python: 2.7
|
||||
|
||||
env:
|
||||
- DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
|
||||
- DJANGO="django==1.6.5"
|
||||
- DJANGO="django==1.5.8"
|
||||
- DJANGO="django==1.4.13"
|
||||
- TOX_ENV=flake8
|
||||
- TOX_ENV=py3.4-django1.7
|
||||
- TOX_ENV=py3.3-django1.7
|
||||
- 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:
|
||||
- pip install $DJANGO
|
||||
- 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=.
|
||||
- "pip install tox --download-cache $HOME/.pip-cache"
|
||||
|
||||
script:
|
||||
- ./runtests.py
|
||||
|
||||
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"
|
||||
- tox -e $TOX_ENV
|
||||
|
|
|
@ -62,10 +62,10 @@ To run the tests, clone the repository, and then:
|
|||
virtualenv env
|
||||
env/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pip install -r optionals.txt
|
||||
pip install -r requirements-test.txt
|
||||
|
||||
# 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:
|
||||
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
recursive-include rest_framework/static *.js *.css *.png
|
||||
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 djangorestframework
|
||||
django-admin startproject example .
|
||||
django-admin.py startproject example .
|
||||
./manage.py syncdb
|
||||
|
||||
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 Meta:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'manufacturer__name`]
|
||||
fields = ['category', 'in_stock', 'manufacturer__name']
|
||||
|
||||
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:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'manufacturer`]
|
||||
fields = ['category', 'in_stock', 'manufacturer']
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -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!
|
||||
|
||||
[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
|
||||
[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-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.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
|
||||
|
||||
**Date**: 29th August 2014
|
||||
|
|
|
@ -8,6 +8,7 @@ flake8==2.2.2
|
|||
markdown>=2.1.0
|
||||
PyYAML>=3.10
|
||||
defusedxml>=0.3
|
||||
django-guardian==1.2.4
|
||||
django-filter>=0.5.4
|
||||
django-oauth-plus>=2.2.1
|
||||
oauth2>=1.5.211
|
||||
|
|
|
@ -1 +1 @@
|
|||
Django>=1.3
|
||||
Django>=1.4.2
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '2.4.0'
|
||||
__version__ = '2.4.2'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||
|
|
|
@ -344,7 +344,7 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
user = token.user
|
||||
|
||||
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)
|
||||
|
||||
return (user, token)
|
||||
|
|
|
@ -51,8 +51,11 @@ def get_attribute(instance, attrs):
|
|||
for attr in attrs:
|
||||
try:
|
||||
instance = getattr(instance, attr)
|
||||
except AttributeError:
|
||||
return instance[attr]
|
||||
except AttributeError as exc:
|
||||
try:
|
||||
return instance[attr]
|
||||
except (KeyError, TypeError):
|
||||
raise exc
|
||||
return instance
|
||||
|
||||
|
||||
|
|
|
@ -56,7 +56,6 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
class Meta:
|
||||
model = queryset.model
|
||||
fields = filter_fields
|
||||
order_by = True
|
||||
return AutoFilterSet
|
||||
|
||||
return None
|
||||
|
|
|
@ -43,8 +43,9 @@ class DefaultObjectSerializer(serializers.Field):
|
|||
as the default.
|
||||
"""
|
||||
|
||||
def __init__(self, source=None, context=None):
|
||||
# Note: Swallow context kwarg - only required for eg. ModelSerializer.
|
||||
def __init__(self, source=None, many=None, context=None):
|
||||
# Note: Swallow context and many kwargs - only required for
|
||||
# eg. ModelSerializer.
|
||||
super(DefaultObjectSerializer, self).__init__(source=source)
|
||||
|
||||
|
||||
|
@ -61,6 +62,7 @@ class BasePaginationSerializer(serializers.Serializer):
|
|||
"""
|
||||
super(BasePaginationSerializer, self).__init__(*args, **kwargs)
|
||||
results_field = self.results_field
|
||||
|
||||
try:
|
||||
object_serializer = self.Meta.object_serializer_class
|
||||
except AttributeError:
|
||||
|
@ -70,7 +72,7 @@ class BasePaginationSerializer(serializers.Serializer):
|
|||
child=object_serializer(),
|
||||
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):
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.http import QueryDict
|
|||
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
||||
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
|
||||
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 import renderers
|
||||
import json
|
||||
|
@ -290,6 +290,22 @@ class FileUploadParser(BaseParser):
|
|||
try:
|
||||
meta = parser_context['request'].META
|
||||
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):
|
||||
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):
|
||||
_ignore_model_permissions = True
|
||||
|
||||
def get(self, request, format=None):
|
||||
def get(self, request, *args, **kwargs):
|
||||
ret = {}
|
||||
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 APIRoot.as_view()
|
||||
|
|
|
@ -317,6 +317,19 @@ class ModelSerializerOptions(object):
|
|||
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):
|
||||
field_mapping = {
|
||||
models.AutoField: IntegerField,
|
||||
|
@ -580,13 +593,20 @@ class ModelSerializer(Serializer):
|
|||
if decimal_places is not None:
|
||||
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:
|
||||
kwargs['validators'] = validator_kwarg
|
||||
|
||||
try:
|
||||
return self.field_mapping[model_field.__class__](**kwargs)
|
||||
except KeyError:
|
||||
return ModelField(model_field=model_field, **kwargs)
|
||||
cls = lookup_class(self.field_mapping, model_field)
|
||||
if cls is None:
|
||||
cls = ModelField
|
||||
kwargs['model_field'] = model_field
|
||||
return cls(**kwargs)
|
||||
|
||||
|
||||
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
|
||||
|
||||
{% block meta %}
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
{% endblock %}
|
||||
|
||||
|
||||
<title>{% block title %}Django REST framework{% endblock %}</title>
|
||||
|
||||
|
||||
{% block style %}
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||
|
@ -21,10 +21,11 @@
|
|||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
{% block body %}
|
||||
<body class="{% block bodyclass %}{% endblock %} container">
|
||||
|
||||
<div class="wrapper">
|
||||
|
@ -43,17 +44,9 @@
|
|||
<ul class="nav pull-right">
|
||||
{% block userlinks %}
|
||||
{% if user.is_authenticated %}
|
||||
<li class="dropdown">
|
||||
<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>
|
||||
{% optional_logout request user %}
|
||||
{% else %}
|
||||
<li>{% optional_login request %}</li>
|
||||
{% optional_login request %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
|
@ -84,7 +77,7 @@
|
|||
<div class="btn-group format-selection">
|
||||
<a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}'
|
||||
rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
|
||||
|
||||
|
||||
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown"
|
||||
title="Specify a format for the GET request">
|
||||
<span class="caret"></span>
|
||||
|
@ -143,7 +136,7 @@
|
|||
</div>
|
||||
|
||||
{% if display_edit_forms %}
|
||||
|
||||
|
||||
{% if post_form or raw_data_post_form %}
|
||||
<div {% if post_form %}class="tabbable"{% endif %}>
|
||||
{% if post_form %}
|
||||
|
@ -189,7 +182,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if put_form or raw_data_put_form or raw_data_patch_form %}
|
||||
<div {% if put_form %}class="tabbable"{% endif %}>
|
||||
{% if put_form %}
|
||||
|
@ -245,7 +238,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<!-- END Content -->
|
||||
|
||||
|
||||
<footer>
|
||||
{% block footer %}
|
||||
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p>
|
||||
|
@ -261,4 +254,5 @@
|
|||
<script src="{% static "rest_framework/js/default.js" %}"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
{% endblock %}
|
||||
</html>
|
||||
|
|
|
@ -17,21 +17,44 @@
|
|||
|
||||
<div class="row-fluid">
|
||||
<div>
|
||||
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
|
||||
<form action="{% url 'rest_framework:login' %}" role="form" method="post">
|
||||
{% 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">
|
||||
<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 id="div_id_password" class="clearfix control-group">
|
||||
<div class="controls">
|
||||
<div id="div_id_password"
|
||||
class="clearfix control-group {% if form.password.errors %}error{% endif %}">
|
||||
<div class="controls">
|
||||
<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>
|
||||
<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">
|
||||
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
|
||||
</div>
|
||||
|
|
|
@ -41,22 +41,31 @@ def optional_login(request):
|
|||
except NoReverseMatch:
|
||||
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
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
try:
|
||||
logout_url = reverse('rest_framework:logout')
|
||||
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))
|
||||
return snippet
|
||||
snippet = """<li class="dropdown">
|
||||
<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
|
||||
|
|
|
@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
Encode the data returning a two tuple of (bytes, content_type)
|
||||
"""
|
||||
|
||||
if not data:
|
||||
if data is None:
|
||||
return ('', content_type)
|
||||
|
||||
assert format is None or content_type is None, (
|
||||
|
|
|
@ -6,7 +6,7 @@ your API requires authentication:
|
|||
|
||||
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
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
Utility functions to return a formatted name and description for a given view.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
|
||||
from django.utils.html import escape
|
||||
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):
|
||||
|
@ -28,6 +29,7 @@ def dedent(content):
|
|||
as it fails to dedent multiline docstrings that include
|
||||
unindented text on the initial line.
|
||||
"""
|
||||
content = force_text(content)
|
||||
whitespace_counts = [len(line) - len(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.cls = cls
|
||||
return view
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
# all other authentication is CSRF exempt.
|
||||
return csrf_exempt(view)
|
||||
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
|
@ -376,9 +378,9 @@ class APIView(View):
|
|||
response.exception = True
|
||||
return response
|
||||
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
# all other authentication is CSRF exempt.
|
||||
@csrf_exempt
|
||||
# Note: Views are made CSRF exempt from within `as_view` as to prevent
|
||||
# accidental removal of this exemption in cases where `dispatch` needs to
|
||||
# be overridden.
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
`.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,
|
||||
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:'}},
|
||||
SITE_ID=1,
|
||||
SECRET_KEY='not very secret in tests',
|
||||
USE_I18N=True,
|
||||
USE_L10N=True,
|
||||
|
|
|
@ -57,7 +57,8 @@ urlpatterns = patterns(
|
|||
authentication_classes=[OAuthAuthentication],
|
||||
permission_classes=[permissions.TokenHasReadWriteScope]
|
||||
)
|
||||
)
|
||||
),
|
||||
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||
)
|
||||
|
||||
|
||||
|
@ -134,6 +135,15 @@ class SessionAuthTests(TestCase):
|
|||
def tearDown(self):
|
||||
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):
|
||||
"""
|
||||
Ensure POSTing form over session authentication without CSRF token fails.
|
||||
|
|
|
@ -98,6 +98,30 @@ class TestViewNamesAndDescriptions(TestCase):
|
|||
pass
|
||||
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):
|
||||
"""
|
||||
Ensure markdown to HTML works as expected.
|
||||
|
|
|
@ -1004,6 +1004,26 @@
|
|||
# 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):
|
||||
# """
|
||||
# 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)
|
||||
text = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class OrderingFilterRelatedModel(models.Model):
|
||||
related_object = models.ForeignKey(OrdringFilterModel,
|
||||
related_object = models.ForeignKey(OrderingFilterModel,
|
||||
related_name="relateds")
|
||||
|
||||
|
||||
class OrderingFilterSerializer(serializers.ModelSerializer):
|
||||
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):
|
||||
|
@ -455,11 +506,11 @@ class OrderingFilterTests(TestCase):
|
|||
chr(idx + ord('b')) +
|
||||
chr(idx + ord('c'))
|
||||
)
|
||||
OrdringFilterModel(title=title, text=text).save()
|
||||
OrderingFilterModel(title=title, text=text).save()
|
||||
|
||||
def test_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrdringFilterModel.objects.all()
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
|
@ -479,7 +530,7 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_reverse_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrdringFilterModel.objects.all()
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
|
@ -499,7 +550,7 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_incorrectfield_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrdringFilterModel.objects.all()
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
|
@ -519,7 +570,7 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_default_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrdringFilterModel.objects.all()
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
|
@ -539,7 +590,7 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
def test_default_ordering_using_string(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrdringFilterModel.objects.all()
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = 'title'
|
||||
|
@ -560,7 +611,7 @@ class OrderingFilterTests(TestCase):
|
|||
def test_ordering_by_aggregate_field(self):
|
||||
# create some related models to aggregate order by
|
||||
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):
|
||||
for _ in range(num_relateds):
|
||||
new_related = OrderingFilterRelatedModel(
|
||||
|
@ -573,7 +624,7 @@ class OrderingFilterTests(TestCase):
|
|||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = 'title'
|
||||
ordering_fields = '__all__'
|
||||
queryset = OrdringFilterModel.objects.all().annotate(
|
||||
queryset = OrderingFilterModel.objects.all().annotate(
|
||||
models.Count("relateds"))
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
|
@ -591,7 +642,7 @@ class OrderingFilterTests(TestCase):
|
|||
def test_ordering_with_nonstandard_ordering_param(self):
|
||||
with temporary_setting('ORDERING_PARAM', 'order', filters):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrdringFilterModel.objects.all()
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
import django
|
||||
from django.db import models
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.test import TestCase
|
||||
|
@ -176,6 +177,9 @@ class TestRootView(TestCase):
|
|||
self.assertEqual(created.text, 'foobar')
|
||||
|
||||
|
||||
EXPECTED_QUERYS_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
|
||||
|
||||
|
||||
class TestInstanceView(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
|
@ -219,7 +223,7 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
data = {'text': 'foobar'}
|
||||
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()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
|
||||
|
@ -233,7 +237,7 @@ class TestInstanceView(TestCase):
|
|||
data = {'text': 'foobar'}
|
||||
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()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||
|
@ -351,7 +355,7 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
data = {'id': 999, 'text': 'foobar'}
|
||||
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()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||
|
|
|
@ -15,7 +15,7 @@ from rest_framework import serializers
|
|||
class RegularFieldsModel(models.Model):
|
||||
auto_field = models.AutoField(primary_key=True)
|
||||
big_integer_field = models.BigIntegerField()
|
||||
boolean_field = models.BooleanField()
|
||||
boolean_field = models.BooleanField(default=False)
|
||||
char_field = models.CharField(max_length=100)
|
||||
comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
|
||||
date_field = models.DateField()
|
||||
|
@ -60,22 +60,22 @@ TestSerializer():
|
|||
|
||||
# Model for testing relational field mapping
|
||||
|
||||
class ForeignKeyTarget(models.Model):
|
||||
char_field = models.CharField(max_length=100)
|
||||
class ForeignKeyTargetModel(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ManyToManyTarget(models.Model):
|
||||
char_field = models.CharField(max_length=100)
|
||||
class ManyToManyTargetModel(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class OneToOneTarget(models.Model):
|
||||
char_field = models.CharField(max_length=100)
|
||||
class OneToOneTargetModel(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class RelationalModel(models.Model):
|
||||
foreign_key = models.ForeignKey(ForeignKeyTarget)
|
||||
many_to_many = models.ManyToManyField(ManyToManyTarget)
|
||||
one_to_one = models.OneToOneField(OneToOneTarget)
|
||||
foreign_key = models.ForeignKey(ForeignKeyTargetModel)
|
||||
many_to_many = models.ManyToManyField(ManyToManyTargetModel)
|
||||
one_to_one = models.OneToOneField(OneToOneTargetModel)
|
||||
|
||||
|
||||
RELATIONAL_FLAT_REPR = """
|
||||
|
@ -105,9 +105,9 @@ TestSerializer():
|
|||
HYPERLINKED_FLAT_REPR = """
|
||||
TestSerializer():
|
||||
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
|
||||
foreign_key = HyperlinkedRelatedField(label='foreign key', queryset=<django.db.models.manager.Manager object>, view_name='foreignkeytarget-detail')
|
||||
one_to_one = HyperlinkedRelatedField(label='one to one', queryset=<django.db.models.manager.Manager object>, view_name='onetoonetarget-detail')
|
||||
many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=<django.db.models.manager.Manager object>, view_name='manytomanytarget-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='onetoonetargetmodel-detail')
|
||||
many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=<django.db.models.manager.Manager object>, view_name='manytomanytargetmodel-detail')
|
||||
""".strip()
|
||||
|
||||
|
||||
|
@ -127,6 +127,8 @@ TestSerializer():
|
|||
|
||||
|
||||
class TestSerializerMappings(TestCase):
|
||||
maxDiff = 10000
|
||||
|
||||
def test_regular_fields(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
@ -428,6 +428,15 @@ class CustomPaginationSerializer(pagination.BasePaginationSerializer):
|
|||
results_field = 'objects'
|
||||
|
||||
|
||||
class CustomFooSerializer(serializers.Serializer):
|
||||
foo = serializers.CharField()
|
||||
|
||||
|
||||
class CustomFooPaginationSerializer(pagination.PaginationSerializer):
|
||||
class Meta:
|
||||
object_serializer_class = CustomFooSerializer
|
||||
|
||||
|
||||
class TestCustomPaginationSerializer(TestCase):
|
||||
def setUp(self):
|
||||
objects = ['john', 'paul', 'george', 'ringo']
|
||||
|
@ -450,6 +459,16 @@ class TestCustomPaginationSerializer(TestCase):
|
|||
}
|
||||
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):
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from rest_framework.compat import StringIO
|
||||
from django import forms
|
||||
|
@ -113,3 +115,25 @@ class TestFileUploadParser(TestCase):
|
|||
parser = FileUploadParser()
|
||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||
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 django.core.validators import MaxValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import generics, serializers, status
|
||||
|
@ -132,3 +133,42 @@ class TestMaxValueValidatorValidation(TestCase):
|
|||
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.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]
|
||||
basepython = python3.4
|
||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
||||
deps = Django==1.7
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
@ -26,7 +26,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
|||
|
||||
[testenv:py3.3-django1.7]
|
||||
basepython = python3.3
|
||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
||||
deps = Django==1.7
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
@ -34,7 +34,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
|||
|
||||
[testenv:py3.2-django1.7]
|
||||
basepython = python3.2
|
||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
||||
deps = Django==1.7
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
@ -42,7 +42,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
|||
|
||||
[testenv:py2.7-django1.7]
|
||||
basepython = python2.7
|
||||
deps = https://www.djangoproject.com/download/1.7c2/tarball/
|
||||
deps = Django==1.7
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
# django-oauth-plus==2.2.1
|
||||
|
|
Loading…
Reference in New Issue
Block a user