Merge master

This commit is contained in:
Tom Christie 2014-09-11 13:20:44 +01:00
commit de301f3b66
40 changed files with 496 additions and 132 deletions

View File

@ -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

View File

@ -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:

View File

@ -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]

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
Django>=1.3
Django>=1.4.2

View File

@ -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'

View File

@ -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)

View File

@ -51,8 +51,11 @@ def get_attribute(instance, attrs):
for attr in attrs:
try:
instance = getattr(instance, attr)
except AttributeError:
except AttributeError as exc:
try:
return instance[attr]
except (KeyError, TypeError):
raise exc
return instance

View File

@ -56,7 +56,6 @@ class DjangoFilterBackend(BaseFilterBackend):
class Meta:
model = queryset.model
fields = filter_fields
order_by = True
return AutoFilterSet
return None

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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):

View File

@ -25,6 +25,7 @@
{% 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>
@ -261,4 +254,5 @@
<script src="{% static "rest_framework/js/default.js" %}"></script>
{% endblock %}
</body>
{% endblock %}
</html>

View File

@ -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 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>

View File

@ -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

View File

@ -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, (

View File

@ -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

View File

@ -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()]

View File

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

View File

View 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')),
)

View 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()),
)

View 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">')

View 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})

View File

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

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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',)

View File

@ -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'})

View File

@ -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:

View File

@ -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):

View File

@ -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

View File

@ -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))

View File

@ -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