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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:
return instance[attr] try:
return instance[attr]
except (KeyError, TypeError):
raise exc
return instance return instance

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
<div class="controls"> class="clearfix control-group {% if form.password.errors %}error{% endif %}">
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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