Commit 83480087 by Rense VanderHoek

Merge pull request #1 from tomchristie/master

Update from tom
parents 91416632 5aa204e9
...@@ -21,10 +21,10 @@ env: ...@@ -21,10 +21,10 @@ env:
- TOX_ENV=py26-django15 - TOX_ENV=py26-django15
- TOX_ENV=py27-django14 - TOX_ENV=py27-django14
- TOX_ENV=py26-django14 - TOX_ENV=py26-django14
- TOX_ENV=py34-django18alpha - TOX_ENV=py34-django18beta
- TOX_ENV=py33-django18alpha - TOX_ENV=py33-django18beta
- TOX_ENV=py32-django18alpha - TOX_ENV=py32-django18beta
- TOX_ENV=py27-django18alpha - TOX_ENV=py27-django18beta
install: install:
- pip install tox - pip install tox
......
...@@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox]. ...@@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-alpha) * Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta)
# Installation # Installation
......
...@@ -353,6 +353,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a ...@@ -353,6 +353,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system. [Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system.
## django-rest-auth
[Django-rest-auth][django-rest-auth] library provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for user management.
[cite]: http://jacobian.org/writing/rest-worst-practices/ [cite]: http://jacobian.org/writing/rest-worst-practices/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 [http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
...@@ -392,3 +396,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a ...@@ -392,3 +396,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[mohawk]: http://mohawk.readthedocs.org/en/latest/ [mohawk]: http://mohawk.readthedocs.org/en/latest/
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05 [mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
[djoser]: https://github.com/sunscrapers/djoser [djoser]: https://github.com/sunscrapers/djoser
[django-rest-auth]: https://github.com/Tivix/django-rest-auth
...@@ -47,7 +47,7 @@ Any example validation error might look like this: ...@@ -47,7 +47,7 @@ Any example validation error might look like this:
You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API.
The function must take a single argument, which is the exception to be handled, and should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. The function must take a pair of arguments, this first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response.
For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so:
...@@ -72,6 +72,8 @@ In order to alter the style of the response, you could write the following custo ...@@ -72,6 +72,8 @@ In order to alter the style of the response, you could write the following custo
return response return response
The context argument is not used by the default handler, but can be useful if the exception handler needs further information such as the view currently being handled, which can be accessed as `context['view']`.
The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example: The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example:
REST_FRAMEWORK = { REST_FRAMEWORK = {
......
...@@ -14,7 +14,7 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. ...@@ -14,7 +14,7 @@ Extends [Django's existing `RequestFactory` class][requestfactory].
## Creating test requests ## Creating test requests
The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that the standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available.
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
...@@ -115,7 +115,7 @@ Extends [Django's existing `Client` class][client]. ...@@ -115,7 +115,7 @@ Extends [Django's existing `Client` class][client].
## Making requests ## Making requests
The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: The `APIClient` class supports the same request interface as Django's standard `Client` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
from rest_framework.test import APIClient from rest_framework.test import APIClient
...@@ -269,6 +269,6 @@ For example, to add support for using `format='html'` in test requests, you migh ...@@ -269,6 +269,6 @@ For example, to add support for using `format='html'` in test requests, you migh
} }
[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper [cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper
[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client [client]: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client
[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory [requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory
[configuration]: #configuration [configuration]: #configuration
...@@ -50,7 +50,7 @@ Some reasons you might want to use REST framework: ...@@ -50,7 +50,7 @@ Some reasons you might want to use REST framework:
REST framework requires the following: REST framework requires the following:
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7) * Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta)
The following packages are optional: The following packages are optional:
......
...@@ -188,6 +188,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ...@@ -188,6 +188,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [hawkrest][hawkrest] - Provides Hawk HTTP Authorization. * [hawkrest][hawkrest] - Provides Hawk HTTP Authorization.
* [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism. * [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism.
* [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. * [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation.
* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc.
### Permissions ### Permissions
...@@ -324,3 +325,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ...@@ -324,3 +325,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-rest-framework-and-angularjs-video]: http://www.youtube.com/watch?v=q8frbgtj020 [django-rest-framework-and-angularjs-video]: http://www.youtube.com/watch?v=q8frbgtj020
[web-api-performance-profiling-django-rest-framework]: http://dabapps.com/blog/api-performance-profiling-django-rest-framework/ [web-api-performance-profiling-django-rest-framework]: http://dabapps.com/blog/api-performance-profiling-django-rest-framework/
[api-development-with-django-and-django-rest-framework]: https://bnotions.com/api-development-with-django-and-django-rest-framework/ [api-development-with-django-and-django-rest-framework]: https://bnotions.com/api-development-with-django-and-django-rest-framework/
[django-rest-auth]: https://github.com/Tivix/django-rest-auth/
...@@ -71,7 +71,14 @@ def get_attribute(instance, attrs): ...@@ -71,7 +71,14 @@ def get_attribute(instance, attrs):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
if is_simple_callable(instance): if is_simple_callable(instance):
instance = instance() try:
instance = instance()
except (AttributeError, KeyError) as exc:
# If we raised an Attribute or KeyError here it'd get treated
# as an omitted field in `Field.get_attribute()`. Instead we
# raise a ValueError to ensure the exception is not masked.
raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc))
return instance return instance
...@@ -107,6 +114,8 @@ class CreateOnlyDefault: ...@@ -107,6 +114,8 @@ class CreateOnlyDefault:
def set_context(self, serializer_field): def set_context(self, serializer_field):
self.is_update = serializer_field.parent.instance is not None self.is_update = serializer_field.parent.instance is not None
if callable(self.default) and hasattr(self.default, 'set_context'):
self.default.set_context(serializer_field)
def __call__(self): def __call__(self):
if self.is_update: if self.is_update:
...@@ -1184,7 +1193,7 @@ class ListField(Field): ...@@ -1184,7 +1193,7 @@ class ListField(Field):
class DictField(Field): class DictField(Field):
child = _UnvalidatedField() child = _UnvalidatedField()
initial = [] initial = {}
default_error_messages = { default_error_messages = {
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".') 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".')
} }
......
...@@ -259,7 +259,7 @@ class PageNumberPagination(BasePagination): ...@@ -259,7 +259,7 @@ class PageNumberPagination(BasePagination):
) )
raise NotFound(msg) raise NotFound(msg)
if paginator.count > 1: if paginator.count > 1 and self.template is not None:
# The browsable API should display pagination controls. # The browsable API should display pagination controls.
self.display_page_controls = True self.display_page_controls = True
...@@ -347,7 +347,7 @@ class LimitOffsetPagination(BasePagination): ...@@ -347,7 +347,7 @@ class LimitOffsetPagination(BasePagination):
self.offset = self.get_offset(request) self.offset = self.get_offset(request)
self.count = _get_count(queryset) self.count = _get_count(queryset)
self.request = request self.request = request
if self.count > self.limit: if self.count > self.limit and self.template is not None:
self.display_page_controls = True self.display_page_controls = True
return queryset[self.offset:self.offset + self.limit] return queryset[self.offset:self.offset + self.limit]
...@@ -518,7 +518,7 @@ class CursorPagination(BasePagination): ...@@ -518,7 +518,7 @@ class CursorPagination(BasePagination):
# Display page controls in the browsable API if there is more # Display page controls in the browsable API if there is more
# than one page. # than one page.
if self.has_previous or self.has_next: if (self.has_previous or self.has_next) and self.template is not None:
self.display_page_controls = True self.display_page_controls = True
return self.page return self.page
......
...@@ -165,34 +165,30 @@ class SimpleRouter(BaseRouter): ...@@ -165,34 +165,30 @@ class SimpleRouter(BaseRouter):
else: else:
list_routes.append((httpmethods, methodname)) list_routes.append((httpmethods, methodname))
def _get_dynamic_routes(route, dynamic_routes):
ret = []
for httpmethods, methodname in dynamic_routes:
method_kwargs = getattr(viewset, methodname).kwargs
initkwargs = route.initkwargs.copy()
initkwargs.update(method_kwargs)
url_path = initkwargs.pop("url_path", None) or methodname
ret.append(Route(
url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
return ret
ret = [] ret = []
for route in self.routes: for route in self.routes:
if isinstance(route, DynamicDetailRoute): if isinstance(route, DynamicDetailRoute):
# Dynamic detail routes (@detail_route decorator) # Dynamic detail routes (@detail_route decorator)
for httpmethods, methodname in detail_routes: ret += _get_dynamic_routes(route, detail_routes)
method_kwargs = getattr(viewset, methodname).kwargs
url_path = method_kwargs.pop("url_path", None) or methodname
initkwargs = route.initkwargs.copy()
initkwargs.update(method_kwargs)
ret.append(Route(
url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
elif isinstance(route, DynamicListRoute): elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator) # Dynamic list routes (@list_route decorator)
for httpmethods, methodname in list_routes: ret += _get_dynamic_routes(route, list_routes)
method_kwargs = getattr(viewset, methodname).kwargs
url_path = method_kwargs.pop("url_path", None) or methodname
initkwargs = route.initkwargs.copy()
initkwargs.update(method_kwargs)
ret.append(Route(
url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
else: else:
# Standard route # Standard route
ret.append(route) ret.append(route)
......
...@@ -13,6 +13,7 @@ response content is handled by parsers and renderers. ...@@ -13,6 +13,7 @@ response content is handled by parsers and renderers.
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField
from django.db.models import query
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.compat import postgres_fields, unicode_to_repr
from rest_framework.utils import model_meta from rest_framework.utils import model_meta
...@@ -562,7 +563,7 @@ class ListSerializer(BaseSerializer): ...@@ -562,7 +563,7 @@ class ListSerializer(BaseSerializer):
""" """
# Dealing with nested relationships, data can be a Manager, # Dealing with nested relationships, data can be a Manager,
# so, first get a queryset from the Manager if needed # so, first get a queryset from the Manager if needed
iterable = data.all() if isinstance(data, models.Manager) else data iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data
return [ return [
self.child.to_representation(item) for item in iterable self.child.to_representation(item) for item in iterable
] ]
......
{% load i18n %}
{% trans "No items to select." as no_items %}
<div class="form-group"> <div class="form-group">
{% if field.label %} {% if field.label %}
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label> <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
{% endif %} {% endif %}
<div class="col-sm-10"> <div class="col-sm-10">
<select multiple class="form-control" name="{{ field.name }}"> <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
{% empty %}
<option>{{ no_items }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% if field.errors %} {% if field.errors %}
......
{% load i18n %}
{% trans "No items to select." as no_items %}
<div class="form-group {% if field.errors %}has-error{% endif %}"> <div class="form-group {% if field.errors %}has-error{% endif %}">
{% if field.label %} {% if field.label %}
<label class="sr-only">{{ field.label }}</label> <label class="sr-only">{{ field.label }}</label>
{% endif %} {% endif %}
<select multiple class="form-control" name="{{ field.name }}"> <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
{% empty %}
<option>{{ no_items }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
{% load i18n %}
{% trans "No items to select." as no_items %}
<div class="form-group {% if field.errors %}has-error{% endif %}"> <div class="form-group {% if field.errors %}has-error{% endif %}">
{% if field.label %} {% if field.label %}
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
{% endif %} {% endif %}
<select multiple class="form-control" name="{{ field.name }}"> <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
{% empty %}
<option>{{ no_items }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% if field.errors %} {% if field.errors %}
......
...@@ -209,7 +209,8 @@ class APIClient(APIRequestFactory, DjangoClient): ...@@ -209,7 +209,8 @@ class APIClient(APIRequestFactory, DjangoClient):
self.handler._force_user = None self.handler._force_user = None
self.handler._force_token = None self.handler._force_token = None
return super(APIClient, self).logout() if self.session:
super(APIClient, self).logout()
class APITransactionTestCase(testcases.TransactionTestCase): class APITransactionTestCase(testcases.TransactionTestCase):
......
...@@ -13,7 +13,7 @@ from rest_framework.exceptions import ValidationError ...@@ -13,7 +13,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.utils.representation import smart_repr from rest_framework.utils.representation import smart_repr
class UniqueValidator: class UniqueValidator(object):
""" """
Validator that corresponds to `unique=True` on a model field. Validator that corresponds to `unique=True` on a model field.
...@@ -67,7 +67,7 @@ class UniqueValidator: ...@@ -67,7 +67,7 @@ class UniqueValidator:
)) ))
class UniqueTogetherValidator: class UniqueTogetherValidator(object):
""" """
Validator that corresponds to `unique_together = (...)` on a model class. Validator that corresponds to `unique_together = (...)` on a model class.
...@@ -138,7 +138,12 @@ class UniqueTogetherValidator: ...@@ -138,7 +138,12 @@ class UniqueTogetherValidator:
queryset = self.queryset queryset = self.queryset
queryset = self.filter_queryset(attrs, queryset) queryset = self.filter_queryset(attrs, queryset)
queryset = self.exclude_current_instance(attrs, queryset) queryset = self.exclude_current_instance(attrs, queryset)
if queryset.exists():
# Ignore validation if any field is None
checked_values = [
value for field, value in attrs.items() if field in self.fields
]
if None not in checked_values and queryset.exists():
field_names = ', '.join(self.fields) field_names = ', '.join(self.fields)
raise ValidationError(self.message.format(field_names=field_names)) raise ValidationError(self.message.format(field_names=field_names))
...@@ -150,7 +155,7 @@ class UniqueTogetherValidator: ...@@ -150,7 +155,7 @@ class UniqueTogetherValidator:
)) ))
class BaseUniqueForValidator: class BaseUniqueForValidator(object):
message = None message = None
missing_message = _('This field is required.') missing_message = _('This field is required.')
......
...@@ -93,6 +93,31 @@ class TestSource: ...@@ -93,6 +93,31 @@ class TestSource:
"same as the field name. Remove the `source` keyword argument." "same as the field name. Remove the `source` keyword argument."
) )
def test_callable_source(self):
class ExampleSerializer(serializers.Serializer):
example_field = serializers.CharField(source='example_callable')
class ExampleInstance(object):
def example_callable(self):
return 'example callable value'
serializer = ExampleSerializer(ExampleInstance())
assert serializer.data['example_field'] == 'example callable value'
def test_callable_source_raises(self):
class ExampleSerializer(serializers.Serializer):
example_field = serializers.CharField(source='example_callable', read_only=True)
class ExampleInstance(object):
def example_callable(self):
raise AttributeError('method call failed')
with pytest.raises(ValueError) as exc_info:
serializer = ExampleSerializer(ExampleInstance())
serializer.data.items()
assert 'method call failed' in str(exc_info.value)
class TestReadOnly: class TestReadOnly:
def setup(self): def setup(self):
...@@ -292,6 +317,25 @@ class TestCreateOnlyDefault: ...@@ -292,6 +317,25 @@ class TestCreateOnlyDefault:
'text': 'example', 'text': 'example',
} }
def test_create_only_default_callable_sets_context(self):
"""
CreateOnlyDefault instances with a callable default should set_context
on the callable if possible
"""
class TestCallableDefault:
def set_context(self, serializer_field):
self.field = serializer_field
def __call__(self):
return "success" if hasattr(self, 'field') else "failure"
class TestSerializer(serializers.Serializer):
context_set = serializers.CharField(default=serializers.CreateOnlyDefault(TestCallableDefault()))
serializer = TestSerializer(data={})
assert serializer.is_valid()
assert serializer.validated_data['context_set'] == 'success'
# Tests for field input and output values. # Tests for field input and output values.
# ---------------------------------------- # ----------------------------------------
......
...@@ -99,11 +99,5 @@ class TestFileUploadParser(TestCase): ...@@ -99,11 +99,5 @@ class TestFileUploadParser(TestCase):
filename = parser.get_filename(self.stream, None, self.parser_context) filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt') 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)
# Malformed. Either None or 'fallback.txt' will be acceptable.
# See also https://code.djangoproject.com/ticket/24209
self.assertIn(filename, ('fallback.txt', None))
def __replace_content_disposition(self, disposition): def __replace_content_disposition(self, disposition):
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
...@@ -302,12 +302,16 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet): ...@@ -302,12 +302,16 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
return Response({'method': 'link2'}) return Response({'method': 'link2'})
class SubDynamicListAndDetailViewSet(DynamicListAndDetailViewSet):
pass
class TestDynamicListAndDetailRouter(TestCase): class TestDynamicListAndDetailRouter(TestCase):
def setUp(self): def setUp(self):
self.router = SimpleRouter() self.router = SimpleRouter()
def test_list_and_detail_route_decorators(self): def _test_list_and_detail_route_decorators(self, viewset):
routes = self.router.get_routes(DynamicListAndDetailViewSet) routes = self.router.get_routes(viewset)
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path') MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
...@@ -336,3 +340,9 @@ class TestDynamicListAndDetailRouter(TestCase): ...@@ -336,3 +340,9 @@ class TestDynamicListAndDetailRouter(TestCase):
else: else:
method_map = 'get' method_map = 'get'
self.assertEqual(route.mapping[method_map], method_name) self.assertEqual(route.mapping[method_map], method_name)
def test_list_and_detail_route_decorators(self):
self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
def test_inherited_list_and_detail_route_decorators(self):
self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet)
...@@ -83,11 +83,37 @@ class UniquenessTogetherModel(models.Model): ...@@ -83,11 +83,37 @@ class UniquenessTogetherModel(models.Model):
unique_together = ('race_name', 'position') unique_together = ('race_name', 'position')
class NullUniquenessTogetherModel(models.Model):
"""
Used to ensure that null values are not included when checking
unique_together constraints.
Ignoring items which have a null in any of the validated fields is the same
behavior that database backends will use when they have the
unique_together constraint added.
Example case: a null position could indicate a non-finisher in the race,
there could be many non-finishers in a race, but all non-NULL
values *should* be unique against the given `race_name`.
"""
date_of_birth = models.DateField(null=True) # Not part of the uniqueness constraint
race_name = models.CharField(max_length=100)
position = models.IntegerField(null=True)
class Meta:
unique_together = ('race_name', 'position')
class UniquenessTogetherSerializer(serializers.ModelSerializer): class UniquenessTogetherSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = UniquenessTogetherModel model = UniquenessTogetherModel
class NullUniquenessTogetherSerializer(serializers.ModelSerializer):
class Meta:
model = NullUniquenessTogetherModel
class TestUniquenessTogetherValidation(TestCase): class TestUniquenessTogetherValidation(TestCase):
def setUp(self): def setUp(self):
self.instance = UniquenessTogetherModel.objects.create( self.instance = UniquenessTogetherModel.objects.create(
...@@ -182,6 +208,34 @@ class TestUniquenessTogetherValidation(TestCase): ...@@ -182,6 +208,34 @@ class TestUniquenessTogetherValidation(TestCase):
""") """)
assert repr(serializer) == expected assert repr(serializer) == expected
def test_ignore_validation_for_null_fields(self):
# None values that are on fields which are part of the uniqueness
# constraint cause the instance to ignore uniqueness validation.
NullUniquenessTogetherModel.objects.create(
date_of_birth=datetime.date(2000, 1, 1),
race_name='Paris Marathon',
position=None
)
data = {
'date': datetime.date(2000, 1, 1),
'race_name': 'Paris Marathon',
'position': None
}
serializer = NullUniquenessTogetherSerializer(data=data)
assert serializer.is_valid()
def test_do_not_ignore_validation_for_null_fields(self):
# None values that are not on fields part of the uniqueness constraint
# do not cause the instance to skip validation.
NullUniquenessTogetherModel.objects.create(
date_of_birth=datetime.date(2000, 1, 1),
race_name='Paris Marathon',
position=1
)
data = {'date': None, 'race_name': 'Paris Marathon', 'position': 1}
serializer = NullUniquenessTogetherSerializer(data=data)
assert not serializer.is_valid()
# Tests for `UniqueForDateValidator` # Tests for `UniqueForDateValidator`
# ---------------------------------- # ----------------------------------
......
...@@ -3,7 +3,7 @@ envlist = ...@@ -3,7 +3,7 @@ envlist =
py27-{flake8,docs}, py27-{flake8,docs},
{py26,py27}-django14, {py26,py27}-django14,
{py26,py27,py32,py33,py34}-django{15,16}, {py26,py27,py32,py33,py34}-django{15,16},
{py27,py32,py33,py34}-django{17,18alpha} {py27,py32,py33,py34}-django{17,18beta}
[testenv] [testenv]
commands = ./runtests.py --fast commands = ./runtests.py --fast
...@@ -14,7 +14,7 @@ deps = ...@@ -14,7 +14,7 @@ deps =
django15: Django==1.5.6 # Should track minimum supported django15: Django==1.5.6 # Should track minimum supported
django16: Django==1.6.3 # Should track minimum supported django16: Django==1.6.3 # Should track minimum supported
django17: Django==1.7.2 # Should track maximum supported django17: Django==1.7.2 # Should track maximum supported
django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ django18beta: https://www.djangoproject.com/download/1.8b1/tarball/
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt -rrequirements/requirements-optionals.txt
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment