Merge branch 'master' into requests-client

This commit is contained in:
Tom Christie 2016-08-12 15:27:59 +01:00
commit 5abac93c01
57 changed files with 792 additions and 183 deletions

View File

@ -18,8 +18,7 @@ REST framework commercially we strongly encourage you to invest in its
continued development by **[signing up for a paid plan][funding]**. continued development by **[signing up for a paid plan][funding]**.
The initial aim is to provide a single full-time position on REST framework. The initial aim is to provide a single full-time position on REST framework.
Right now we're over 58% of the way towards achieving that. *Every single sign-up makes a significant impact towards making that possible.*
*Every single sign-up makes a significant impact.*
<p align="center"> <p align="center">
<a href="http://jobs.rover.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rover-readme.png"/></a> <a href="http://jobs.rover.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rover-readme.png"/></a>

View File

@ -49,7 +49,9 @@ Defaults to `False`
### `default` ### `default`
If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all. If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behaviour is to not populate the attribute at all.
The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned.
May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. This works the same way as for [validators](validators.md#using-set_context). May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. This works the same way as for [validators](validators.md#using-set_context).
@ -486,7 +488,7 @@ This field is used by default with `ModelSerializer` when including field names
**Signature**: `ReadOnlyField()` **Signature**: `ReadOnlyField()`
For example, is `has_expired` was a property on the `Account` model, then the following serializer would automatically generate it as a `ReadOnlyField`: For example, if `has_expired` was a property on the `Account` model, then the following serializer would automatically generate it as a `ReadOnlyField`:
class AccountSerializer(serializers.ModelSerializer): class AccountSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@ -241,7 +241,6 @@ For more details on using filter sets see the [django-filter documentation][djan
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting. * By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].) * When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax. * `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3.
--- ---

View File

@ -132,7 +132,7 @@ This permission is suitable if you want to your API to allow read permissions to
## DjangoModelPermissions ## DjangoModelPermissions
This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that has a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned.
* `POST` requests require the user to have the `add` permission on the model. * `POST` requests require the user to have the `add` permission on the model.
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model.

View File

@ -184,6 +184,8 @@ If the `.wait()` method is implemented and the request is throttled, then a `Ret
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests. The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
import random
class RandomRateThrottle(throttling.BaseThrottle): class RandomRateThrottle(throttling.BaseThrottle):
def allow_request(self, request, view): def allow_request(self, request, view):
return random.randint(1, 10) == 1 return random.randint(1, 10) == 1

View File

@ -64,6 +64,8 @@ It takes a single required argument, and an optional `messages` argument:
This validator should be applied to *serializer fields*, like so: This validator should be applied to *serializer fields*, like so:
from rest_framework.validators import UniqueValidator
slug = SlugField( slug = SlugField(
max_length=100, max_length=100,
validators=[UniqueValidator(queryset=BlogPost.objects.all())] validators=[UniqueValidator(queryset=BlogPost.objects.all())]
@ -80,6 +82,8 @@ It has two required arguments, and a single optional `messages` argument:
The validator should be applied to *serializer classes*, like so: The validator should be applied to *serializer classes*, like so:
from rest_framework.validators import UniqueTogetherValidator
class ExampleSerializer(serializers.Serializer): class ExampleSerializer(serializers.Serializer):
# ... # ...
class Meta: class Meta:
@ -114,6 +118,8 @@ These validators can be used to enforce the `unique_for_date`, `unique_for_month
The validator should be applied to *serializer classes*, like so: The validator should be applied to *serializer classes*, like so:
from rest_framework.validators import UniqueForYearValidator
class ExampleSerializer(serializers.Serializer): class ExampleSerializer(serializers.Serializer):
# ... # ...
class Meta: class Meta:
@ -183,7 +189,7 @@ It takes a single argument, which is the default value or callable that should b
created_at = serializers.DateTimeField( created_at = serializers.DateTimeField(
read_only=True, read_only=True,
default=CreateOnlyDefault(timezone.now) default=serializers.CreateOnlyDefault(timezone.now)
) )
--- ---

View File

@ -71,8 +71,8 @@ You can also set the versioning scheme on an individual view. Typically you won'
The following settings keys are also used to control versioning: The following settings keys are also used to control versioning:
* `DEFAULT_VERSION`. The value that should be used for `request.version` when no versioning information is present. Defaults to `None`. * `DEFAULT_VERSION`. The value that should be used for `request.version` when no versioning information is present. Defaults to `None`.
* `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set. Note that the value used for the `DEFAULT_VERSION` setting is always considered to be part of the `ALLOWED_VERSIONS` set. Defaults to `None`. * `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version is not in this set. Note that the value used for the `DEFAULT_VERSION` setting is always considered to be part of the `ALLOWED_VERSIONS` set (unless it is `None`). Defaults to `None`.
* `VERSION_PARAM`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`. * `VERSION_PARAM`. The string that should be used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`.
You can also set your versioning class plus those three values on a per-view or a per-viewset basis by defining your own versioning scheme and using the `default_version`, `allowed_versions` and `version_param` class variables. For example, if you want to use `URLPathVersioning`: You can also set your versioning class plus those three values on a per-view or a per-viewset basis by defining your own versioning scheme and using the `default_version`, `allowed_versions` and `version_param` class variables. For example, if you want to use `URLPathVersioning`:

View File

@ -68,8 +68,7 @@ REST framework commercially we strongly encourage you to invest in its
continued development by **[signing up for a paid plan][funding]**. continued development by **[signing up for a paid plan][funding]**.
The initial aim is to provide a single full-time position on REST framework. The initial aim is to provide a single full-time position on REST framework.
Right now we're over 58% of the way towards achieving that. *Every single sign-up makes a significant impact towards making that possible.*
*Every single sign-up makes a significant impact.*
<ul class="premium-promo promo"> <ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li> <li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
@ -87,7 +86,7 @@ Right now we're over 58% of the way towards achieving that.
REST framework requires the following: REST framework requires the following:
* Python (2.7, 3.2, 3.3, 3.4, 3.5) * Python (2.7, 3.2, 3.3, 3.4, 3.5)
* Django (1.7+, 1.8, 1.9) * Django (1.8, 1.9, 1.10)
The following packages are optional: The following packages are optional:

View File

@ -40,6 +40,49 @@ You can determine your currently installed version using `pip freeze`:
## 3.4.x series ## 3.4.x series
### 3.4.4
**Date**: [12th August 2016][3.4.4-milestone]
* Ensure views are fully initialized when generating schemas. ([#4373][gh4373], [#4382][gh4382], [#4383][gh4383], [#4279][gh4279], [#4278][gh4278])
* Add form field descriptions to schemas. ([#4387][gh4387])
* Fix category generation for schema endpoints. ([#4391][gh4391], [#4394][gh4394], [#4390][gh4390], [#4386][gh4386], [#4376][gh4376], [#4329][gh4329])
* Don't strip empty query params when paginating. ([#4392][gh4392], [#4393][gh4393], [#4260][gh4260])
* Do not re-run query for empty results with LimitOffsetPagination. ([#4201][gh4201], [#4388][gh4388])
* Stricter type validation for CharField. ([#4380][gh4380], [#3394][gh3394])
* RelatedField.choices should preserve non-string values. ([#4111][gh4111], [#4379][gh4379], [#3365][gh3365])
* Test case for rendering checkboxes in vertical form style. ([#4378][gh4378], [#3868][gh3868], [#3868][gh3868])
* Show error traceback HTML in browsable API ([#4042][gh4042], [#4172][gh4172])
* Fix handling of ALLOWED_VERSIONS and no DEFAULT_VERSION. [#4370][gh4370]
* Allow `max_digits=None` on DecimalField. ([#4377][gh4377], [#4372][gh4372])
* Limit queryset when rendering relational choices. ([#4375][gh4375], [#4122][gh4122], [#3329][gh3329], [#3330][gh3330], [#3877][gh3877])
* Resolve form display with ChoiceField, MultipleChoiceField and non-string choices. ([#4374][gh4374], [#4119][gh4119], [#4121][gh4121], [#4137][gh4137], [#4120][gh4120])
* Fix call to TemplateHTMLRenderer.resolve_context() fallback method. ([#4371][gh4371])
### 3.4.3
**Date**: [5th August 2016][3.4.3-milestone]
* Include fallaback for users of older TemplateHTMLRenderer internal API. ([#4361][gh4361])
### 3.4.2
**Date**: [5th August 2016][3.4.2-milestone]
* Include kwargs passed to 'as_view' when generating schemas. ([#4359][gh4359], [#4330][gh4330], [#4331][gh4331])
* Access `request.user.is_authenticated` as property not method, under Django 1.10+ ([#4358][gh4358], [#4354][gh4354])
* Filter HEAD out from schemas. ([#4357][gh4357])
* extra_kwargs takes precedence over uniqueness kwargs. ([#4198][gh4198], [#4199][gh4199], [#4349][gh4349])
* Correct descriptions when tabs are used in code indentation. ([#4345][gh4345], [#4347][gh4347])*
* Change template context generation in TemplateHTMLRenderer. ([#4236][gh4236])
* Serializer defaults should not be included in partial updates. ([#4346][gh4346], [#3565][gh3565])
* Consistent behavior & descriptive error from FileUploadParser when filename not included. ([#4340][gh4340], [#3610][gh3610], [#4292][gh4292], [#4296][gh4296])
* DecimalField quantizes incoming digitals. ([#4339][gh4339], [#4318][gh4318])
* Handle non-string input for IP fields. ([#4335][gh4335], [#4336][gh4336], [#4338][gh4338])
* Fix leading slash handling when Schema generation includes a root URL. ([#4332][gh4332])
* Test cases for DictField with allow_null options. ([#4348][gh4348])
* Update tests from Django 1.10 beta to Django 1.10. ([#4344][gh4344])
### 3.4.1 ### 3.4.1
**Date**: [28th July 2016][3.4.1-milestone] **Date**: [28th July 2016][3.4.1-milestone]
@ -514,6 +557,9 @@ For older release notes, [please see the version 2.x documentation][old-release-
[3.3.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.3+Release%22 [3.3.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.3+Release%22
[3.4.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.0+Release%22 [3.4.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.0+Release%22
[3.4.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.1+Release%22 [3.4.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.1+Release%22
[3.4.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.2+Release%22
[3.4.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.3+Release%22
[3.4.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.4+Release%22
<!-- 3.0.1 --> <!-- 3.0.1 -->
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
@ -943,3 +989,79 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh4272]: https://github.com/tomchristie/django-rest-framework/issues/4272 [gh4272]: https://github.com/tomchristie/django-rest-framework/issues/4272
[gh4273]: https://github.com/tomchristie/django-rest-framework/issues/4273 [gh4273]: https://github.com/tomchristie/django-rest-framework/issues/4273
[gh4288]: https://github.com/tomchristie/django-rest-framework/issues/4288 [gh4288]: https://github.com/tomchristie/django-rest-framework/issues/4288
<!-- 3.4.2 -->
[gh3565]: https://github.com/tomchristie/django-rest-framework/issues/3565
[gh3610]: https://github.com/tomchristie/django-rest-framework/issues/3610
[gh4198]: https://github.com/tomchristie/django-rest-framework/issues/4198
[gh4199]: https://github.com/tomchristie/django-rest-framework/issues/4199
[gh4236]: https://github.com/tomchristie/django-rest-framework/issues/4236
[gh4292]: https://github.com/tomchristie/django-rest-framework/issues/4292
[gh4296]: https://github.com/tomchristie/django-rest-framework/issues/4296
[gh4318]: https://github.com/tomchristie/django-rest-framework/issues/4318
[gh4330]: https://github.com/tomchristie/django-rest-framework/issues/4330
[gh4331]: https://github.com/tomchristie/django-rest-framework/issues/4331
[gh4332]: https://github.com/tomchristie/django-rest-framework/issues/4332
[gh4335]: https://github.com/tomchristie/django-rest-framework/issues/4335
[gh4336]: https://github.com/tomchristie/django-rest-framework/issues/4336
[gh4338]: https://github.com/tomchristie/django-rest-framework/issues/4338
[gh4339]: https://github.com/tomchristie/django-rest-framework/issues/4339
[gh4340]: https://github.com/tomchristie/django-rest-framework/issues/4340
[gh4344]: https://github.com/tomchristie/django-rest-framework/issues/4344
[gh4345]: https://github.com/tomchristie/django-rest-framework/issues/4345
[gh4346]: https://github.com/tomchristie/django-rest-framework/issues/4346
[gh4347]: https://github.com/tomchristie/django-rest-framework/issues/4347
[gh4348]: https://github.com/tomchristie/django-rest-framework/issues/4348
[gh4349]: https://github.com/tomchristie/django-rest-framework/issues/4349
[gh4354]: https://github.com/tomchristie/django-rest-framework/issues/4354
[gh4357]: https://github.com/tomchristie/django-rest-framework/issues/4357
[gh4358]: https://github.com/tomchristie/django-rest-framework/issues/4358
[gh4359]: https://github.com/tomchristie/django-rest-framework/issues/4359
<!-- 3.4.3 -->
[gh4361]: https://github.com/tomchristie/django-rest-framework/issues/4361
<!-- 3.4.4 -->
[gh2829]: https://github.com/tomchristie/django-rest-framework/issues/2829
[gh3329]: https://github.com/tomchristie/django-rest-framework/issues/3329
[gh3330]: https://github.com/tomchristie/django-rest-framework/issues/3330
[gh3365]: https://github.com/tomchristie/django-rest-framework/issues/3365
[gh3394]: https://github.com/tomchristie/django-rest-framework/issues/3394
[gh3868]: https://github.com/tomchristie/django-rest-framework/issues/3868
[gh3868]: https://github.com/tomchristie/django-rest-framework/issues/3868
[gh3877]: https://github.com/tomchristie/django-rest-framework/issues/3877
[gh4042]: https://github.com/tomchristie/django-rest-framework/issues/4042
[gh4111]: https://github.com/tomchristie/django-rest-framework/issues/4111
[gh4119]: https://github.com/tomchristie/django-rest-framework/issues/4119
[gh4120]: https://github.com/tomchristie/django-rest-framework/issues/4120
[gh4121]: https://github.com/tomchristie/django-rest-framework/issues/4121
[gh4122]: https://github.com/tomchristie/django-rest-framework/issues/4122
[gh4137]: https://github.com/tomchristie/django-rest-framework/issues/4137
[gh4172]: https://github.com/tomchristie/django-rest-framework/issues/4172
[gh4201]: https://github.com/tomchristie/django-rest-framework/issues/4201
[gh4260]: https://github.com/tomchristie/django-rest-framework/issues/4260
[gh4278]: https://github.com/tomchristie/django-rest-framework/issues/4278
[gh4279]: https://github.com/tomchristie/django-rest-framework/issues/4279
[gh4329]: https://github.com/tomchristie/django-rest-framework/issues/4329
[gh4370]: https://github.com/tomchristie/django-rest-framework/issues/4370
[gh4371]: https://github.com/tomchristie/django-rest-framework/issues/4371
[gh4372]: https://github.com/tomchristie/django-rest-framework/issues/4372
[gh4373]: https://github.com/tomchristie/django-rest-framework/issues/4373
[gh4374]: https://github.com/tomchristie/django-rest-framework/issues/4374
[gh4375]: https://github.com/tomchristie/django-rest-framework/issues/4375
[gh4376]: https://github.com/tomchristie/django-rest-framework/issues/4376
[gh4377]: https://github.com/tomchristie/django-rest-framework/issues/4377
[gh4378]: https://github.com/tomchristie/django-rest-framework/issues/4378
[gh4379]: https://github.com/tomchristie/django-rest-framework/issues/4379
[gh4380]: https://github.com/tomchristie/django-rest-framework/issues/4380
[gh4382]: https://github.com/tomchristie/django-rest-framework/issues/4382
[gh4383]: https://github.com/tomchristie/django-rest-framework/issues/4383
[gh4386]: https://github.com/tomchristie/django-rest-framework/issues/4386
[gh4387]: https://github.com/tomchristie/django-rest-framework/issues/4387
[gh4388]: https://github.com/tomchristie/django-rest-framework/issues/4388
[gh4390]: https://github.com/tomchristie/django-rest-framework/issues/4390
[gh4391]: https://github.com/tomchristie/django-rest-framework/issues/4391
[gh4392]: https://github.com/tomchristie/django-rest-framework/issues/4392
[gh4393]: https://github.com/tomchristie/django-rest-framework/issues/4393
[gh4394]: https://github.com/tomchristie/django-rest-framework/issues/4394

View File

@ -8,7 +8,7 @@ ______ _____ _____ _____ __
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '3.4.1' __version__ = '3.4.4'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2016 Tom Christie' __copyright__ = 'Copyright 2011-2016 Tom Christie'

View File

@ -122,6 +122,12 @@ def _resolve_model(obj):
raise ValueError("{0} is not a Django model".format(obj)) raise ValueError("{0} is not a Django model".format(obj))
def is_authenticated(user):
if django.VERSION < (1, 10):
return user.is_authenticated()
return user.is_authenticated
def get_related_model(field): def get_related_model(field):
if django.VERSION < (1, 9): if django.VERSION < (1, 9):
return _resolve_model(field.rel.to) return _resolve_model(field.rel.to)
@ -162,7 +168,7 @@ except ImportError:
crispy_forms = None crispy_forms = None
# coreapi is optional (Note that uritemplate is a dependancy of coreapi) # coreapi is optional (Note that uritemplate is a dependency of coreapi)
try: try:
import coreapi import coreapi
import uritemplate import uritemplate

View File

@ -395,8 +395,8 @@ class Field(object):
# determine if we should use null instead. # determine if we should use null instead.
return '' if getattr(self, 'allow_blank', False) else None return '' if getattr(self, 'allow_blank', False) else None
elif ret == '' and not self.required: elif ret == '' and not self.required:
# If the field is blank, and emptyness is valid then # If the field is blank, and emptiness is valid then
# determine if we should use emptyness instead. # determine if we should use emptiness instead.
return '' if getattr(self, 'allow_blank', False) else empty return '' if getattr(self, 'allow_blank', False) else empty
return ret return ret
return dictionary.get(self.field_name, empty) return dictionary.get(self.field_name, empty)
@ -435,7 +435,8 @@ class Field(object):
return `empty`, indicating that no value should be set in the return `empty`, indicating that no value should be set in the
validated data for this field. validated data for this field.
""" """
if self.default is empty: if self.default is empty or getattr(self.root, 'partial', False):
# No default, or this is a partial update.
raise SkipField() raise SkipField()
if callable(self.default): if callable(self.default):
if hasattr(self.default, 'set_context'): if hasattr(self.default, 'set_context'):
@ -671,6 +672,7 @@ class NullBooleanField(Field):
class CharField(Field): class CharField(Field):
default_error_messages = { default_error_messages = {
'invalid': _('Not a valid string.'),
'blank': _('This field may not be blank.'), 'blank': _('This field may not be blank.'),
'max_length': _('Ensure this field has no more than {max_length} characters.'), 'max_length': _('Ensure this field has no more than {max_length} characters.'),
'min_length': _('Ensure this field has at least {min_length} characters.') 'min_length': _('Ensure this field has at least {min_length} characters.')
@ -701,6 +703,11 @@ class CharField(Field):
return super(CharField, self).run_validation(data) return super(CharField, self).run_validation(data)
def to_internal_value(self, data): def to_internal_value(self, data):
# We're lenient with allowing basic numerics to be coerced into strings,
# but other types should fail. Eg. unclear if booleans should represent as `true` or `True`,
# and composites such as lists are likely user error.
if isinstance(data, bool) or not isinstance(data, six.string_types + six.integer_types + (float,)):
self.fail('invalid')
value = six.text_type(data) value = six.text_type(data)
return value.strip() if self.trim_whitespace else value return value.strip() if self.trim_whitespace else value
@ -804,7 +811,10 @@ class IPAddressField(CharField):
self.validators.extend(validators) self.validators.extend(validators)
def to_internal_value(self, data): def to_internal_value(self, data):
if data and ':' in data: if not isinstance(data, six.string_types):
self.fail('invalid', value=data)
if ':' in data:
try: try:
if self.protocol in ('both', 'ipv6'): if self.protocol in ('both', 'ipv6'):
return clean_ipv6_address(data, self.unpack_ipv4) return clean_ipv6_address(data, self.unpack_ipv4)
@ -952,7 +962,7 @@ class DecimalField(Field):
if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')): if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')):
self.fail('invalid') self.fail('invalid')
return self.validate_precision(value) return self.quantize(self.validate_precision(value))
def validate_precision(self, value): def validate_precision(self, value):
""" """
@ -1012,10 +1022,12 @@ class DecimalField(Field):
return value return value
context = decimal.getcontext().copy() context = decimal.getcontext().copy()
context.prec = self.max_digits if self.max_digits is not None:
context.prec = self.max_digits
return value.quantize( return value.quantize(
decimal.Decimal('.1') ** self.decimal_places, decimal.Decimal('.1') ** self.decimal_places,
context=context) context=context
)
# Date & time fields... # Date & time fields...
@ -1341,7 +1353,7 @@ class FilePathField(ChoiceField):
def __init__(self, path, match=None, recursive=False, allow_files=True, def __init__(self, path, match=None, recursive=False, allow_files=True,
allow_folders=False, required=None, **kwargs): allow_folders=False, required=None, **kwargs):
# Defer to Django's FilePathField implmentation to get the # Defer to Django's FilePathField implementation to get the
# valid set of choices. # valid set of choices.
field = DjangoFilePathField( field = DjangoFilePathField(
path, match=match, recursive=recursive, allow_files=allow_files, path, match=match, recursive=recursive, allow_files=allow_files,
@ -1644,7 +1656,7 @@ class SerializerMethodField(Field):
def bind(self, field_name, parent): def bind(self, field_name, parent):
# In order to enforce a consistent style, we error if a redundant # In order to enforce a consistent style, we error if a redundant
# 'method_name' argument has been used. For example: # 'method_name' argument has been used. For example:
# my_field = serializer.CharField(source='my_field') # my_field = serializer.SerializerMethodField(method_name='get_my_field')
default_method_name = 'get_{field_name}'.format(field_name=field_name) default_method_name = 'get_{field_name}'.format(field_name=field_name)
assert self.method_name != default_method_name, ( assert self.method_name != default_method_name, (
"It is redundant to specify `%s` on SerializerMethodField '%s' in " "It is redundant to specify `%s` on SerializerMethodField '%s' in "

View File

@ -90,7 +90,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
def get_accept_list(self, request): def get_accept_list(self, request):
""" """
Given the incoming request, return a tokenised list of media Given the incoming request, return a tokenized list of media
type strings. type strings.
""" """
header = request.META.get('HTTP_ACCEPT', '*/*') header = request.META.get('HTTP_ACCEPT', '*/*')

View File

@ -64,10 +64,10 @@ def _get_displayed_page_numbers(current, final):
This implementation gives one page to each side of the cursor, This implementation gives one page to each side of the cursor,
or two pages to the side when the cursor is at the edge, then or two pages to the side when the cursor is at the edge, then
ensures that any breaks between non-continous page numbers never ensures that any breaks between non-continuous page numbers never
remove only a single page. remove only a single page.
For an alernativative implementation which gives two pages to each side of For an alternative implementation which gives two pages to each side of
the cursor, eg. as in GitHub issue list pagination, see: the cursor, eg. as in GitHub issue list pagination, see:
https://gist.github.com/tomchristie/321140cebb1c4a558b15 https://gist.github.com/tomchristie/321140cebb1c4a558b15
@ -312,6 +312,9 @@ class LimitOffsetPagination(BasePagination):
self.request = request self.request = request
if self.count > self.limit and self.template is not None: if self.count > self.limit and self.template is not None:
self.display_page_controls = True self.display_page_controls = True
if self.count == 0 or self.offset > self.count:
return []
return list(queryset[self.offset:self.offset + self.limit]) return list(queryset[self.offset:self.offset + self.limit])
def get_paginated_response(self, data): def get_paginated_response(self, data):
@ -476,10 +479,10 @@ class CursorPagination(BasePagination):
# Determine the position of the final item following the page. # Determine the position of the final item following the page.
if len(results) > len(self.page): if len(results) > len(self.page):
has_following_postion = True has_following_position = True
following_position = self._get_position_from_instance(results[-1], self.ordering) following_position = self._get_position_from_instance(results[-1], self.ordering)
else: else:
has_following_postion = False has_following_position = False
following_position = None following_position = None
# If we have a reverse queryset, then the query ordering was in reverse # If we have a reverse queryset, then the query ordering was in reverse
@ -490,14 +493,14 @@ class CursorPagination(BasePagination):
if reverse: if reverse:
# Determine next and previous positions for reverse cursors. # Determine next and previous positions for reverse cursors.
self.has_next = (current_position is not None) or (offset > 0) self.has_next = (current_position is not None) or (offset > 0)
self.has_previous = has_following_postion self.has_previous = has_following_position
if self.has_next: if self.has_next:
self.next_position = current_position self.next_position = current_position
if self.has_previous: if self.has_previous:
self.previous_position = following_position self.previous_position = following_position
else: else:
# Determine next and previous positions for forward cursors. # Determine next and previous positions for forward cursors.
self.has_next = has_following_postion self.has_next = has_following_position
self.has_previous = (current_position is not None) or (offset > 0) self.has_previous = (current_position is not None) or (offset > 0)
if self.has_next: if self.has_next:
self.next_position = following_position self.next_position = following_position
@ -534,7 +537,7 @@ class CursorPagination(BasePagination):
# our marker. # our marker.
break break
# The item in this postion has the same position as the item # The item in this position has the same position as the item
# following it, we can't use it as a marker position, so increment # following it, we can't use it as a marker position, so increment
# the offset and keep seeking to the previous item. # the offset and keep seeking to the previous item.
compare = position compare = position
@ -582,7 +585,7 @@ class CursorPagination(BasePagination):
# our marker. # our marker.
break break
# The item in this postion has the same position as the item # The item in this position has the same position as the item
# following it, we can't use it as a marker position, so increment # following it, we can't use it as a marker position, so increment
# the offset and keep seeking to the previous item. # the offset and keep seeking to the previous item.
compare = position compare = position

View File

@ -118,6 +118,10 @@ class FileUploadParser(BaseParser):
Parser for file upload data. Parser for file upload data.
""" """
media_type = '*/*' media_type = '*/*'
errors = {
'unhandled': 'FileUpload parse error - none of upload handlers can handle the stream',
'no_filename': 'Missing filename. Request should include a Content-Disposition header with a filename parameter.',
}
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
""" """
@ -134,6 +138,9 @@ class FileUploadParser(BaseParser):
upload_handlers = request.upload_handlers upload_handlers = request.upload_handlers
filename = self.get_filename(stream, media_type, parser_context) filename = self.get_filename(stream, media_type, parser_context)
if not filename:
raise ParseError(self.errors['no_filename'])
# Note that this code is extracted from Django's handling of # Note that this code is extracted from Django's handling of
# file uploads in MultiPartParser. # file uploads in MultiPartParser.
content_type = meta.get('HTTP_CONTENT_TYPE', content_type = meta.get('HTTP_CONTENT_TYPE',
@ -146,7 +153,7 @@ class FileUploadParser(BaseParser):
# See if the handler will want to take care of the parsing. # See if the handler will want to take care of the parsing.
for handler in upload_handlers: for handler in upload_handlers:
result = handler.handle_raw_input(None, result = handler.handle_raw_input(stream,
meta, meta,
content_length, content_length,
None, None,
@ -178,10 +185,10 @@ class FileUploadParser(BaseParser):
for index, handler in enumerate(upload_handlers): for index, handler in enumerate(upload_handlers):
file_obj = handler.file_complete(counters[index]) file_obj = handler.file_complete(counters[index])
if file_obj: if file_obj is not None:
return DataAndFiles({}, {'file': file_obj}) return DataAndFiles({}, {'file': file_obj})
raise ParseError("FileUpload parse error - "
"none of upload handlers can handle the stream") raise ParseError(self.errors['unhandled'])
def get_filename(self, stream, media_type, parser_context): def get_filename(self, stream, media_type, parser_context):
""" """

View File

@ -5,6 +5,9 @@ from __future__ import unicode_literals
from django.http import Http404 from django.http import Http404
from rest_framework.compat import is_authenticated
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
@ -44,7 +47,7 @@ class IsAuthenticated(BasePermission):
""" """
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user and request.user.is_authenticated() return request.user and is_authenticated(request.user)
class IsAdminUser(BasePermission): class IsAdminUser(BasePermission):
@ -65,7 +68,7 @@ class IsAuthenticatedOrReadOnly(BasePermission):
return ( return (
request.method in SAFE_METHODS or request.method in SAFE_METHODS or
request.user and request.user and
request.user.is_authenticated() is_authenticated(request.user)
) )
@ -127,7 +130,7 @@ class DjangoModelPermissions(BasePermission):
return ( return (
request.user and request.user and
(request.user.is_authenticated() or not self.authenticated_users_only) and (is_authenticated(request.user) or not self.authenticated_users_only) and
request.user.has_perms(perms) request.user.has_perms(perms)
) )

View File

@ -156,29 +156,35 @@ class RelatedField(Field):
# Standard case, return the object instance. # Standard case, return the object instance.
return get_attribute(instance, self.source_attrs) return get_attribute(instance, self.source_attrs)
@property def get_choices(self, cutoff=None):
def choices(self):
queryset = self.get_queryset() queryset = self.get_queryset()
if queryset is None: if queryset is None:
# Ensure that field.choices returns something sensible # Ensure that field.choices returns something sensible
# even when accessed with a read-only field. # even when accessed with a read-only field.
return {} return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([ return OrderedDict([
( (
six.text_type(self.to_representation(item)), self.to_representation(item),
self.display_value(item) self.display_value(item)
) )
for item in queryset for item in queryset
]) ])
@property
def choices(self):
return self.get_choices()
@property @property
def grouped_choices(self): def grouped_choices(self):
return self.choices return self.choices
def iter_options(self): def iter_options(self):
return iter_options( return iter_options(
self.grouped_choices, self.get_choices(cutoff=self.html_cutoff),
cutoff=self.html_cutoff, cutoff=self.html_cutoff,
cutoff_text=self.html_cutoff_text cutoff_text=self.html_cutoff_text
) )
@ -487,9 +493,12 @@ class ManyRelatedField(Field):
for value in iterable for value in iterable
] ]
def get_choices(self, cutoff=None):
return self.child_relation.get_choices(cutoff)
@property @property
def choices(self): def choices(self):
return self.child_relation.choices return self.get_choices()
@property @property
def grouped_choices(self): def grouped_choices(self):
@ -497,7 +506,7 @@ class ManyRelatedField(Field):
def iter_options(self): def iter_options(self):
return iter_options( return iter_options(
self.grouped_choices, self.get_choices(cutoff=self.html_cutoff),
cutoff=self.html_cutoff, cutoff=self.html_cutoff,
cutoff_text=self.html_cutoff_text cutoff_text=self.html_cutoff_text
) )

View File

@ -166,13 +166,18 @@ class TemplateHTMLRenderer(BaseRenderer):
template_names = self.get_template_names(response, view) template_names = self.get_template_names(response, view)
template = self.resolve_template(template_names) template = self.resolve_template(template_names)
context = self.resolve_context(data, request, response) if hasattr(self, 'resolve_context'):
# Fallback for older versions.
context = self.resolve_context(data, request, response)
else:
context = self.get_template_context(data, renderer_context)
return template_render(template, context, request=request) return template_render(template, context, request=request)
def resolve_template(self, template_names): def resolve_template(self, template_names):
return loader.select_template(template_names) return loader.select_template(template_names)
def resolve_context(self, data, request, response): def get_template_context(self, data, renderer_context):
response = renderer_context['response']
if response.exception: if response.exception:
data['status_code'] = response.status_code data['status_code'] = response.status_code
return data return data
@ -228,7 +233,10 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
if response and response.exception: if response and response.exception:
request = renderer_context['request'] request = renderer_context['request']
template = self.get_exception_template(response) template = self.get_exception_template(response)
context = self.resolve_context(data, request, response) if hasattr(self, 'resolve_context'):
context = self.resolve_context(data, request, response)
else:
context = self.get_template_context(data, renderer_context)
return template_render(template, context, request=request) return template_render(template, context, request=request)
return data return data

View File

@ -4,6 +4,7 @@ from django.conf import settings
from django.contrib.admindocs.views import simplify_regex from django.contrib.admindocs.views import simplify_regex
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
from django.utils import six from django.utils import six
from django.utils.encoding import force_text
from rest_framework import exceptions, serializers from rest_framework import exceptions, serializers
from rest_framework.compat import coreapi, uritemplate, urlparse from rest_framework.compat import coreapi, uritemplate, urlparse
@ -30,24 +31,6 @@ def is_api_view(callback):
return (cls is not None) and issubclass(cls, APIView) return (cls is not None) and issubclass(cls, APIView)
def insert_into(target, keys, item):
"""
Insert `item` into the nested dictionary `target`.
For example:
target = {}
insert_into(target, ('users', 'list'), Link(...))
insert_into(target, ('users', 'detail'), Link(...))
assert target == {'users': {'list': Link(...), 'detail': Link(...)}}
"""
for key in keys[:1]:
if key not in target:
target[key] = {}
target = target[key]
target[keys[-1]] = item
class SchemaGenerator(object): class SchemaGenerator(object):
default_mapping = { default_mapping = {
'get': 'read', 'get': 'read',
@ -56,6 +39,10 @@ class SchemaGenerator(object):
'patch': 'partial_update', 'patch': 'partial_update',
'delete': 'destroy', 'delete': 'destroy',
} }
known_actions = (
'create', 'read', 'retrieve', 'list',
'update', 'partial_update', 'destroy'
)
def __init__(self, title=None, url=None, patterns=None, urlconf=None): def __init__(self, title=None, url=None, patterns=None, urlconf=None):
assert coreapi, '`coreapi` must be installed for schema support.' assert coreapi, '`coreapi` must be installed for schema support.'
@ -65,45 +52,58 @@ class SchemaGenerator(object):
urls = import_module(urlconf) urls = import_module(urlconf)
else: else:
urls = urlconf urls = urlconf
patterns = urls.urlpatterns self.patterns = urls.urlpatterns
elif patterns is None and urlconf is None: elif patterns is None and urlconf is None:
urls = import_module(settings.ROOT_URLCONF) urls = import_module(settings.ROOT_URLCONF)
patterns = urls.urlpatterns self.patterns = urls.urlpatterns
else:
self.patterns = patterns
if url and not url.endswith('/'): if url and not url.endswith('/'):
url += '/' url += '/'
self.title = title self.title = title
self.url = url self.url = url
self.endpoints = self.get_api_endpoints(patterns) self.endpoints = None
def get_schema(self, request=None): def get_schema(self, request=None):
if request is None: if self.endpoints is None:
endpoints = self.endpoints self.endpoints = self.get_api_endpoints(self.patterns)
else:
# Filter the list of endpoints to only include those that links = []
# the user has permission on. for path, method, category, action, callback in self.endpoints:
endpoints = [] view = callback.cls()
for key, link, callback in self.endpoints: for attr, val in getattr(callback, 'initkwargs', {}).items():
method = link.action.upper() setattr(view, attr, val)
view = callback.cls() view.args = ()
view.kwargs = {}
view.format_kwarg = None
if request is not None:
view.request = clone_request(request, method) view.request = clone_request(request, method)
view.format_kwarg = None
try: try:
view.check_permissions(view.request) view.check_permissions(view.request)
except exceptions.APIException: except exceptions.APIException:
pass continue
else: else:
endpoints.append((key, link, callback)) view.request = None
if not endpoints: link = self.get_link(path, method, callback, view)
links.append((category, action, link))
if not links:
return None return None
# Generate the schema content structure, from the endpoints. # Generate the schema content structure, eg:
# ('users', 'list'), Link -> {'users': {'list': Link()}} # {'users': {'list': Link()}}
content = {} content = {}
for key, link, callback in endpoints: for category, action, link in links:
insert_into(content, key, link) if category is None:
content[action] = link
elif category in content:
content[category][action] = link
else:
content[category] = {action: link}
# Return the schema document. # Return the schema document.
return coreapi.Document(title=self.title, content=content, url=self.url) return coreapi.Document(title=self.title, content=content, url=self.url)
@ -121,9 +121,9 @@ class SchemaGenerator(object):
callback = pattern.callback callback = pattern.callback
if self.should_include_endpoint(path, callback): if self.should_include_endpoint(path, callback):
for method in self.get_allowed_methods(callback): for method in self.get_allowed_methods(callback):
key = self.get_key(path, method, callback) action = self.get_action(path, method, callback)
link = self.get_link(path, method, callback) category = self.get_category(path, method, callback, action)
endpoint = (key, link, callback) endpoint = (path, method, category, action, callback)
api_endpoints.append(endpoint) api_endpoints.append(endpoint)
elif isinstance(pattern, RegexURLResolver): elif isinstance(pattern, RegexURLResolver):
@ -167,35 +167,53 @@ class SchemaGenerator(object):
return [ return [
method for method in method for method in
callback.cls().allowed_methods if method != 'OPTIONS' callback.cls().allowed_methods if method not in ('OPTIONS', 'HEAD')
] ]
def get_key(self, path, method, callback): def get_action(self, path, method, callback):
""" """
Return a tuple of strings, indicating the identity to use for a Return a descriptive action string for the endpoint, eg. 'list'.
given endpoint. eg. ('users', 'list').
""" """
category = None
for item in path.strip('/').split('/'):
if '{' in item:
break
category = item
actions = getattr(callback, 'actions', self.default_mapping) actions = getattr(callback, 'actions', self.default_mapping)
action = actions[method.lower()] return actions[method.lower()]
if category: def get_category(self, path, method, callback, action):
return (category, action) """
return (action,) Return a descriptive category string for the endpoint, eg. 'users'.
Examples of category/action pairs that should be generated for various
endpoints:
/users/ [users][list], [users][create]
/users/{pk}/ [users][read], [users][update], [users][destroy]
/users/enabled/ [users][enabled] (custom action)
/users/{pk}/star/ [users][star] (custom action)
/users/{pk}/groups/ [groups][list], [groups][create]
/users/{pk}/groups/{pk}/ [groups][read], [groups][update], [groups][destroy]
"""
path_components = path.strip('/').split('/')
path_components = [
component for component in path_components
if '{' not in component
]
if action in self.known_actions:
# Default action, eg "/users/", "/users/{pk}/"
idx = -1
else:
# Custom action, eg "/users/{pk}/activate/", "/users/active/"
idx = -2
try:
return path_components[idx]
except IndexError:
return None
# Methods for generating each individual `Link` instance... # Methods for generating each individual `Link` instance...
def get_link(self, path, method, callback): def get_link(self, path, method, callback, view):
""" """
Return a `coreapi.Link` instance for the given endpoint. Return a `coreapi.Link` instance for the given endpoint.
""" """
view = callback.cls()
fields = self.get_path_fields(path, method, callback, view) fields = self.get_path_fields(path, method, callback, view)
fields += self.get_serializer_fields(path, method, callback, view) fields += self.get_serializer_fields(path, method, callback, view)
fields += self.get_pagination_fields(path, method, callback, view) fields += self.get_pagination_fields(path, method, callback, view)
@ -206,6 +224,9 @@ class SchemaGenerator(object):
else: else:
encoding = None encoding = None
if self.url and path.startswith('/'):
path = path[1:]
return coreapi.Link( return coreapi.Link(
url=urlparse.urljoin(self.url, path), url=urlparse.urljoin(self.url, path),
action=method.lower(), action=method.lower(),
@ -255,25 +276,29 @@ class SchemaGenerator(object):
if method not in ('PUT', 'PATCH', 'POST'): if method not in ('PUT', 'PATCH', 'POST'):
return [] return []
if not hasattr(view, 'get_serializer_class'): if not hasattr(view, 'get_serializer'):
return [] return []
fields = [] serializer = view.get_serializer()
serializer_class = view.get_serializer_class()
serializer = serializer_class()
if isinstance(serializer, serializers.ListSerializer): if isinstance(serializer, serializers.ListSerializer):
return coreapi.Field(name='data', location='body', required=True) return [coreapi.Field(name='data', location='body', required=True)]
if not isinstance(serializer, serializers.Serializer): if not isinstance(serializer, serializers.Serializer):
return [] return []
fields = []
for field in serializer.fields.values(): for field in serializer.fields.values():
if field.read_only: if field.read_only:
continue continue
required = field.required and method != 'PATCH' required = field.required and method != 'PATCH'
field = coreapi.Field(name=field.source, location='form', required=required) description = force_text(field.help_text) if field.help_text else ''
field = coreapi.Field(
name=field.source,
location='form',
required=required,
description=description
)
fields.append(field) fields.append(field)
return fields return fields

View File

@ -1324,9 +1324,8 @@ class ModelSerializer(Serializer):
# Update `extra_kwargs` with any new options. # Update `extra_kwargs` with any new options.
for key, value in uniqueness_extra_kwargs.items(): for key, value in uniqueness_extra_kwargs.items():
if key in extra_kwargs: if key in extra_kwargs:
extra_kwargs[key].update(value) value.update(extra_kwargs[key])
else: extra_kwargs[key] = value
extra_kwargs[key] = value
return extra_kwargs, hidden_fields return extra_kwargs, hidden_fields
@ -1384,7 +1383,7 @@ class ModelSerializer(Serializer):
def get_unique_together_validators(self): def get_unique_together_validators(self):
""" """
Determine a default set of validators for any unique_together contraints. Determine a default set of validators for any unique_together constraints.
""" """
model_class_inheritance_tree = ( model_class_inheritance_tree = (
[self.Meta.model] + [self.Meta.model] +
@ -1415,7 +1414,7 @@ class ModelSerializer(Serializer):
def get_unique_for_date_validators(self): def get_unique_for_date_validators(self):
""" """
Determine a default set of validators for the following contraints: Determine a default set of validators for the following constraints:
* unique_for_date * unique_for_date
* unique_for_month * unique_for_month

View File

@ -1,3 +1,5 @@
{% load rest_framework %}
<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 %}"> <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">
@ -9,7 +11,7 @@
{% if style.inline %} {% if style.inline %}
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<label class="checkbox-inline"> <label class="checkbox-inline">
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_strings %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
{% endfor %} {% endfor %}
@ -17,7 +19,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_strings %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
</div> </div>

View File

@ -1,4 +1,6 @@
{% load i18n %} {% load i18n %}
{% load rest_framework %}
{% trans "None" as none_choice %} {% trans "None" as none_choice %}
<div class="form-group"> <div class="form-group">
@ -19,7 +21,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<label class="radio-inline"> <label class="radio-inline">
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %} /> <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key|as_string == field.value|as_string %}checked{% endif %} />
{{ text }} {{ text }}
</label> </label>
{% endfor %} {% endfor %}
@ -35,7 +37,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<div class="radio"> <div class="radio">
<label> <label>
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %} /> <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key|as_string == field.value|as_string %}checked{% endif %} />
{{ text }} {{ text }}
</label> </label>
</div> </div>

View File

@ -1,3 +1,5 @@
{% load rest_framework %}
<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 %}"> <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">
@ -16,7 +18,7 @@
{% elif select.end_option_group %} {% elif select.end_option_group %}
</optgroup> </optgroup>
{% else %} {% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option> <option value="{{ select.value }}" {% if select.value|as_string == field.value|as_string %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,4 +1,6 @@
{% load i18n %} {% load i18n %}
{% load rest_framework %}
{% trans "No items to select." as no_items %} {% trans "No items to select." as no_items %}
<div class="form-group"> <div class="form-group">
@ -16,7 +18,7 @@
{% elif select.end_option_group %} {% elif select.end_option_group %}
</optgroup> </optgroup>
{% else %} {% else %}
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option> <option value="{{ select.value }}" {% if select.value|as_string in field.value|as_list_of_strings %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
{% endif %} {% endif %}
{% empty %} {% empty %}
<option>{{ no_items }}</option> <option>{{ no_items }}</option>

View File

@ -1,3 +1,5 @@
{% load rest_framework %}
<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>
@ -6,7 +8,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_strings %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
</div> </div>

View File

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% load rest_framework %}
{% trans "None" as none_choice %} {% trans "None" as none_choice %}
<div class="form-group {% if field.errors %}has-error{% endif %}"> <div class="form-group {% if field.errors %}has-error{% endif %}">
@ -20,7 +21,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<div class="radio"> <div class="radio">
<label> <label>
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key|as_string == field.value|as_string %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
</div> </div>

View File

@ -1,3 +1,5 @@
{% load rest_framework %}
<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"> <label class="sr-only">
@ -15,7 +17,7 @@
{% elif select.end_option_group %} {% elif select.end_option_group %}
</optgroup> </optgroup>
{% else %} {% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option> <option value="{{ select.value }}" {% if select.value|as_string == field.value|as_string %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% load rest_framework %}
{% trans "No items to select." as no_items %} {% 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 %}">
@ -15,7 +16,7 @@
{% elif select.end_option_group %} {% elif select.end_option_group %}
</optgroup> </optgroup>
{% else %} {% else %}
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option> <option value="{{ select.value }}" {% if select.value|as_string in field.value|as_list_of_strings %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
{% endif %} {% endif %}
{% empty %} {% empty %}
<option>{{ no_items }}</option> <option>{{ no_items }}</option>

View File

@ -1,3 +1,5 @@
{% load rest_framework %}
<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>
@ -7,7 +9,7 @@
<div> <div>
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<label class="checkbox-inline"> <label class="checkbox-inline">
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_stringsg %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
{% endfor %} {% endfor %}
@ -16,7 +18,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_stringsg %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
</div> </div>

View File

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% load rest_framework %}
{% trans "None" as none_choice %} {% trans "None" as none_choice %}
<div class="form-group {% if field.errors %}has-error{% endif %}"> <div class="form-group {% if field.errors %}has-error{% endif %}">
@ -19,7 +20,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<label class="radio-inline"> <label class="radio-inline">
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key|as_string == field.value|as_string %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
{% endfor %} {% endfor %}
@ -37,7 +38,7 @@
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<div class="radio"> <div class="radio">
<label> <label>
<input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key|as_string == field.value|as_string %}checked{% endif %}>
{{ text }} {{ text }}
</label> </label>
</div> </div>

View File

@ -1,3 +1,5 @@
{% load rest_framework %}
<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 %}> <label {% if style.hide_label %}class="sr-only"{% endif %}>
@ -15,7 +17,7 @@
{% elif select.end_option_group %} {% elif select.end_option_group %}
</optgroup> </optgroup>
{% else %} {% else %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option> <option value="{{ select.value }}" {% if select.value|as_string == field.value|as_string %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% load rest_framework %}
{% trans "No items to select." as no_items %} {% 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 %}">
@ -15,7 +16,7 @@
{% elif select.end_option_group %} {% elif select.end_option_group %}
</optgroup> </optgroup>
{% else %} {% else %}
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option> <option value="{{ select.value }}" {% if select.value|as_string in field.value|as_list_of_strings %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
{% endif %} {% endif %}
{% empty %} {% empty %}
<option>{{ no_items }}</option> <option>{{ no_items }}</option>

View File

@ -89,6 +89,21 @@ def add_query_param(request, key, val):
return escape(replace_query_param(uri, key, val)) return escape(replace_query_param(uri, key, val))
@register.filter
def as_string(value):
if value is None:
return ''
return '%s' % value
@register.filter
def as_list_of_strings(value):
return [
'' if (item is None) else ('%s' % item)
for item in value
]
@register.filter @register.filter
def add_class(value, css_class): def add_class(value, css_class):
""" """
@ -189,7 +204,7 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
leading punctuation (opening parens) and it'll still do the right thing. leading punctuation (opening parens) and it'll still do the right thing.
If trim_url_limit is not None, the URLs in link text longer than this limit If trim_url_limit is not None, the URLs in link text longer than this limit
will truncated to trim_url_limit-3 characters and appended with an elipsis. will truncated to trim_url_limit-3 characters and appended with an ellipsis.
If nofollow is True, the URLs in link text will get a rel="nofollow" If nofollow is True, the URLs in link text will get a rel="nofollow"
attribute. attribute.

View File

@ -8,6 +8,7 @@ import time
from django.core.cache import cache as default_cache from django.core.cache import cache as default_cache
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from rest_framework.compat import is_authenticated
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -173,7 +174,7 @@ class AnonRateThrottle(SimpleRateThrottle):
scope = 'anon' scope = 'anon'
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
if request.user.is_authenticated(): if is_authenticated(request.user):
return None # Only throttle unauthenticated requests. return None # Only throttle unauthenticated requests.
return self.cache_format % { return self.cache_format % {
@ -193,7 +194,7 @@ class UserRateThrottle(SimpleRateThrottle):
scope = 'user' scope = 'user'
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
if request.user.is_authenticated(): if is_authenticated(request.user):
ident = request.user.pk ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)
@ -241,7 +242,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
Otherwise generate the unique cache key by concatenating the user id Otherwise generate the unique cache key by concatenating the user id
with the '.throttle_scope` property of the view. with the '.throttle_scope` property of the view.
""" """
if request.user.is_authenticated(): if is_authenticated(request.user):
ident = request.user.pk ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)

View File

@ -1,6 +1,6 @@
""" """
Helper functions for mapping model fields to a dictionary of default Helper functions for mapping model fields to a dictionary of default
keyword arguments that should be used for their equivelent serializer fields. keyword arguments that should be used for their equivalent serializer fields.
""" """
import inspect import inspect

View File

@ -32,13 +32,22 @@ def dedent(content):
unindented text on the initial line. unindented text on the initial line.
""" """
content = force_text(content) content = force_text(content)
whitespace_counts = [len(line) - len(line.lstrip(' ')) whitespace_counts = [
for line in content.splitlines()[1:] if line.lstrip()] len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()
]
tab_counts = [
len(line) - len(line.lstrip('\t'))
for line in content.splitlines()[1:] if line.lstrip()
]
# unindent the content if needed # unindent the content if needed
if whitespace_counts: if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts)) whitespace_pattern = '^' + (' ' * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
elif tab_counts:
whitespace_pattern = '^' + ('\t' * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
return content.strip() return content.strip()

View File

@ -14,7 +14,7 @@ def is_html_input(dictionary):
def parse_html_list(dictionary, prefix=''): def parse_html_list(dictionary, prefix=''):
""" """
Used to suport list values in HTML forms. Used to support list values in HTML forms.
Supports lists of primitives and/or dictionaries. Supports lists of primitives and/or dictionaries.
* List of primitives. * List of primitives.

View File

@ -78,7 +78,7 @@ class BoundField(object):
)) ))
def as_form_field(self): def as_form_field(self):
value = '' if (self.value is None or self.value is False) else force_text(self.value) value = '' if (self.value is None or self.value is False) else self.value
return self.__class__(self._field, value, self.errors, self._prefix) return self.__class__(self._field, value, self.errors, self._prefix)

View File

@ -7,7 +7,7 @@ def replace_query_param(url, key, val):
parameters of the URL, and return the new URL. parameters of the URL, and return the new URL.
""" """
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(url) (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
query_dict = urlparse.parse_qs(query) query_dict = urlparse.parse_qs(query, keep_blank_values=True)
query_dict[key] = [val] query_dict[key] = [val]
query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
@ -19,7 +19,7 @@ def remove_query_param(url, key):
parameters of the URL, and return the new URL. parameters of the URL, and return the new URL.
""" """
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(url) (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
query_dict = urlparse.parse_qs(query) query_dict = urlparse.parse_qs(query, keep_blank_values=True)
query_dict.pop(key, None) query_dict.pop(key, None)
query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) return urlparse.urlunsplit((scheme, netloc, path, query, fragment))

View File

@ -30,7 +30,8 @@ class BaseVersioning(object):
def is_allowed_version(self, version): def is_allowed_version(self, version):
if not self.allowed_versions: if not self.allowed_versions:
return True return True
return (version == self.default_version) or (version in self.allowed_versions) return ((version is not None and version == self.default_version) or
(version in self.allowed_versions))
class AcceptHeaderVersioning(BaseVersioning): class AcceptHeaderVersioning(BaseVersioning):
@ -94,7 +95,7 @@ class NamespaceVersioning(BaseVersioning):
The difference is in the backend - this implementation uses The difference is in the backend - this implementation uses
Django's URL namespaces to determine the version. Django's URL namespaces to determine the version.
An example URL conf that is namespaced into two seperate versions An example URL conf that is namespaced into two separate versions
# users/urls.py # users/urls.py
urlpatterns = [ urlpatterns = [
@ -147,7 +148,7 @@ class HostNameVersioning(BaseVersioning):
invalid_version_message = _('Invalid version in hostname.') invalid_version_message = _('Invalid version in hostname.')
def determine_version(self, request, *args, **kwargs): def determine_version(self, request, *args, **kwargs):
hostname, seperator, port = request.get_host().partition(':') hostname, separator, port = request.get_host().partition(':')
match = self.hostname_regex.match(hostname) match = self.hostname_regex.match(hostname)
if not match: if not match:
return self.default_version return self.default_version

View File

@ -3,13 +3,17 @@ Provides an APIView class that is the base of all views in REST framework.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import sys
from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.http import Http404 from django.http import Http404
from django.http.response import HttpResponseBase from django.http.response import HttpResponse, HttpResponseBase
from django.utils import six from django.utils import six
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views import debug
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View from django.views.generic import View
@ -91,7 +95,11 @@ def exception_handler(exc, context):
set_rollback() set_rollback()
return Response(data, status=status.HTTP_403_FORBIDDEN) return Response(data, status=status.HTTP_403_FORBIDDEN)
# Note: Unhandled exceptions will raise a 500 error. # throw django's error page if debug is True
if settings.DEBUG:
exception_reporter = debug.ExceptionReporter(context.get('request'), *sys.exc_info())
return HttpResponse(exception_reporter.get_traceback_html(), status=500)
return None return None

View File

@ -97,6 +97,7 @@ class ViewSetMixin(object):
# generation can pick out these bits of information from a # generation can pick out these bits of information from a
# resolved URL. # resolved URL.
view.cls = cls view.cls = cls
view.initkwargs = initkwargs
view.suffix = initkwargs.get('suffix', None) view.suffix = initkwargs.get('suffix', None)
view.actions = actions view.actions = actions
return csrf_exempt(view) return csrf_exempt(view)
@ -111,7 +112,7 @@ class ViewSetMixin(object):
if method == 'options': if method == 'options':
# This is a special case as we always provide handling for the # This is a special case as we always provide handling for the
# options method in the base `View` class. # options method in the base `View` class.
# Unlike the other explicitly defined actions, 'metadata' is implict. # Unlike the other explicitly defined actions, 'metadata' is implicit.
self.action = 'metadata' self.action = 'metadata'
else: else:
self.action = self.action_map.get(method) self.action = self.action_map.get(method)

View File

@ -440,7 +440,7 @@ class FailingAuthAccessedInRenderer(TestCase):
class NoAuthenticationClassesTests(TestCase): class NoAuthenticationClassesTests(TestCase):
def test_permission_message_with_no_authentication_classes(self): def test_permission_message_with_no_authentication_classes(self):
""" """
An unauthenticated request made against a view that containes no An unauthenticated request made against a view that contains no
`authentication_classes` but do contain `permissions_classes` the error `authentication_classes` but do contain `permissions_classes` the error
code returned should be 403 with the exception's message. code returned should be 403 with the exception's message.
""" """

View File

@ -55,6 +55,30 @@ class TestSimpleBoundField:
assert serializer['bool_field'].as_form_field().value == '' assert serializer['bool_field'].as_form_field().value == ''
assert serializer['null_field'].as_form_field().value == '' assert serializer['null_field'].as_form_field().value == ''
def test_rendering_boolean_field(self):
from rest_framework.renderers import HTMLFormRenderer
class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField(
style={'base_template': 'checkbox.html', 'template_pack': 'rest_framework/vertical'})
serializer = ExampleSerializer(data={'bool_field': True})
assert serializer.is_valid()
renderer = HTMLFormRenderer()
rendered = renderer.render_field(serializer['bool_field'], {})
expected_packed = (
'<divclass="form-group">'
'<divclass="checkbox">'
'<label>'
'<inputtype="checkbox"name="bool_field"value="true"checked>'
'Boolfield'
'</label>'
'</div>'
'</div>'
)
rendered_packed = ''.join(rendered.split())
assert rendered_packed == expected_packed
class TestNestedBoundField: class TestNestedBoundField:
def test_nested_empty_bound_field(self): def test_nested_empty_bound_field(self):

View File

@ -6,6 +6,7 @@ from django.test import TestCase
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from rest_framework.compat import apply_markdown from rest_framework.compat import apply_markdown
from rest_framework.utils.formatting import dedent
from rest_framework.views import APIView from rest_framework.views import APIView
@ -120,3 +121,7 @@ class TestViewNamesAndDescriptions(TestCase):
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
self.assertTrue(gte_21_match or lt_21_match) self.assertTrue(gte_21_match or lt_21_match)
def test_dedent_tabs():
assert dedent("\tfirst string\n\n\tsecond string") == 'first string\n\n\tsecond string'

View File

@ -535,6 +535,8 @@ class TestCharField(FieldValues):
'abc': 'abc' 'abc': 'abc'
} }
invalid_inputs = { invalid_inputs = {
(): ['Not a valid string.'],
True: ['Not a valid string.'],
'': ['This field may not be blank.'] '': ['This field may not be blank.']
} }
outputs = { outputs = {
@ -663,6 +665,7 @@ class TestIPAddressField(FieldValues):
'127.122.111.2231': ['Enter a valid IPv4 or IPv6 address.'], '127.122.111.2231': ['Enter a valid IPv4 or IPv6 address.'],
'2001:::9652': ['Enter a valid IPv4 or IPv6 address.'], '2001:::9652': ['Enter a valid IPv4 or IPv6 address.'],
'2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'], '2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'],
1000: ['Enter a valid IPv4 or IPv6 address.'],
} }
outputs = {} outputs = {}
field = serializers.IPAddressField() field = serializers.IPAddressField()
@ -875,6 +878,18 @@ class TestMinMaxDecimalField(FieldValues):
) )
class TestNoMaxDigitsDecimalField(FieldValues):
field = serializers.DecimalField(
max_value=100, min_value=0,
decimal_places=2, max_digits=None
)
valid_inputs = {
'10': Decimal('10.00')
}
invalid_inputs = {}
outputs = {}
class TestNoStringCoercionDecimalField(FieldValues): class TestNoStringCoercionDecimalField(FieldValues):
""" """
Output values for `DecimalField` with `coerce_to_string=False`. Output values for `DecimalField` with `coerce_to_string=False`.
@ -911,6 +926,26 @@ class TestLocalizedDecimalField(TestCase):
self.assertTrue(isinstance(field.to_representation(Decimal('1.1')), six.string_types)) self.assertTrue(isinstance(field.to_representation(Decimal('1.1')), six.string_types))
class TestQuantizedValueForDecimal(TestCase):
def test_int_quantized_value_for_decimal(self):
field = serializers.DecimalField(max_digits=4, decimal_places=2)
value = field.to_internal_value(12).as_tuple()
expected_digit_tuple = (0, (1, 2, 0, 0), -2)
self.assertEqual(value, expected_digit_tuple)
def test_string_quantized_value_for_decimal(self):
field = serializers.DecimalField(max_digits=4, decimal_places=2)
value = field.to_internal_value('12').as_tuple()
expected_digit_tuple = (0, (1, 2, 0, 0), -2)
self.assertEqual(value, expected_digit_tuple)
def test_part_precision_string_quantized_value_for_decimal(self):
field = serializers.DecimalField(max_digits=4, decimal_places=2)
value = field.to_internal_value('12.0').as_tuple()
expected_digit_tuple = (0, (1, 2, 0, 0), -2)
self.assertEqual(value, expected_digit_tuple)
class TestNoDecimalPlaces(FieldValues): class TestNoDecimalPlaces(FieldValues):
valid_inputs = { valid_inputs = {
'0.12345': Decimal('0.12345'), '0.12345': Decimal('0.12345'),
@ -953,7 +988,7 @@ class TestDateField(FieldValues):
class TestCustomInputFormatDateField(FieldValues): class TestCustomInputFormatDateField(FieldValues):
""" """
Valid and invalid values for `DateField` with a cutom input format. Valid and invalid values for `DateField` with a custom input format.
""" """
valid_inputs = { valid_inputs = {
'1 Jan 2001': datetime.date(2001, 1, 1), '1 Jan 2001': datetime.date(2001, 1, 1),
@ -1020,7 +1055,7 @@ class TestDateTimeField(FieldValues):
class TestCustomInputFormatDateTimeField(FieldValues): class TestCustomInputFormatDateTimeField(FieldValues):
""" """
Valid and invalid values for `DateTimeField` with a cutom input format. Valid and invalid values for `DateTimeField` with a custom input format.
""" """
valid_inputs = { valid_inputs = {
'1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()), '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
@ -1573,6 +1608,29 @@ class TestDictField(FieldValues):
"Remove `source=` from the field declaration." "Remove `source=` from the field declaration."
) )
def test_allow_null(self):
"""
If `allow_null=True` then `None` is a valid input.
"""
field = serializers.DictField(allow_null=True)
output = field.run_validation(None)
assert output is None
class TestDictFieldWithNullChild(FieldValues):
"""
Values for `ListField` with allow_null CharField as child.
"""
valid_inputs = [
({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}),
]
invalid_inputs = [
]
outputs = [
({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}),
]
field = serializers.DictField(child=serializers.CharField(allow_null=True))
class TestUnvalidatedDictField(FieldValues): class TestUnvalidatedDictField(FieldValues):
""" """

View File

@ -711,7 +711,7 @@ class OrderingFilterTests(TestCase):
serializer_class = OrderingFilterSerializer serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
oredering_fields = ('text',) ordering_fields = ('text',)
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('') request = factory.get('')
@ -819,7 +819,7 @@ class OrderingFilterTests(TestCase):
queryset = OrderingFilterModel.objects.all() queryset = OrderingFilterModel.objects.all()
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
# note: no ordering_fields and serializer_class speficied # note: no ordering_fields and serializer_class specified
def get_serializer_class(self): def get_serializer_class(self):
return OrderingFilterSerializer return OrderingFilterSerializer
@ -842,7 +842,7 @@ class OrderingFilterTests(TestCase):
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
# note: no ordering_fields and serializer_class # note: no ordering_fields and serializer_class
# or get_serializer_class speficied # or get_serializer_class specified
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('/', {'ordering': 'text'}) request = factory.get('/', {'ordering': 'text'})

View File

@ -136,7 +136,7 @@ class TestModelSerializer(TestCase):
class TestRegularFieldMappings(TestCase): class TestRegularFieldMappings(TestCase):
def test_regular_fields(self): def test_regular_fields(self):
""" """
Model fields should map to their equivelent serializer fields. Model fields should map to their equivalent serializer fields.
""" """
class TestSerializer(serializers.ModelSerializer): class TestSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -614,7 +614,7 @@ class TestRelationalFieldDisplayValue(TestCase):
fields = '__all__' fields = '__all__'
serializer = TestSerializer() serializer = TestSerializer()
expected = OrderedDict([('1', 'Red Color'), ('2', 'Yellow Color'), ('3', 'Green Color')]) expected = OrderedDict([(1, 'Red Color'), (2, 'Yellow Color'), (3, 'Green Color')])
self.assertEqual(serializer.fields['color'].choices, expected) self.assertEqual(serializer.fields['color'].choices, expected)
def test_custom_display_value(self): def test_custom_display_value(self):
@ -630,7 +630,7 @@ class TestRelationalFieldDisplayValue(TestCase):
fields = '__all__' fields = '__all__'
serializer = TestSerializer() serializer = TestSerializer()
expected = OrderedDict([('1', 'My Red Color'), ('2', 'My Yellow Color'), ('3', 'My Green Color')]) expected = OrderedDict([(1, 'My Red Color'), (2, 'My Yellow Color'), (3, 'My Green Color')])
self.assertEqual(serializer.fields['color'].choices, expected) self.assertEqual(serializer.fields['color'].choices, expected)
@ -976,3 +976,22 @@ class TestModelFieldValues(TestCase):
source = OneToOneSourceTestModel(target=target) source = OneToOneSourceTestModel(target=target)
serializer = ExampleSerializer(source) serializer = ExampleSerializer(source)
self.assertEqual(serializer.data, {'target': 1}) self.assertEqual(serializer.data, {'target': 1})
class TestUniquenessOverride(TestCase):
def test_required_not_overwritten(self):
class TestModel(models.Model):
field_1 = models.IntegerField(null=True)
field_2 = models.IntegerField()
class Meta:
unique_together = (('field_1', 'field_2'),)
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = TestModel
extra_kwargs = {'field_1': {'required': False}}
fields = TestSerializer().fields
self.assertFalse(fields['field_1'].required)
self.assertTrue(fields['field_2'].required)

View File

@ -67,8 +67,8 @@ class TestPaginationIntegration:
def test_setting_page_size_over_maximum(self): def test_setting_page_size_over_maximum(self):
""" """
When page_size parameter exceeds maxiumum allowable, When page_size parameter exceeds maximum allowable,
then it should be capped to the maxiumum. then it should be capped to the maximum.
""" """
request = factory.get('/', {'page_size': 1000}) request = factory.get('/', {'page_size': 1000})
response = self.view(request) response = self.view(request)
@ -108,6 +108,17 @@ class TestPaginationIntegration:
'count': 50 'count': 50
} }
def test_empty_query_params_are_preserved(self):
request = factory.get('/', {'page': 2, 'filter': ''})
response = self.view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {
'results': [12, 14, 16, 18, 20],
'previous': 'http://testserver/?filter=',
'next': 'http://testserver/?filter=&page=3',
'count': 50
}
def test_404_not_found_for_zero_page(self): def test_404_not_found_for_zero_page(self):
request = factory.get('/', {'page': '0'}) request = factory.get('/', {'page': '0'})
response = self.view(request) response = self.view(request)
@ -259,7 +270,7 @@ class TestPageNumberPaginationOverride:
def setup(self): def setup(self):
class OverriddenDjangoPaginator(DjangoPaginator): class OverriddenDjangoPaginator(DjangoPaginator):
# override the count in our overriden Django Paginator # override the count in our overridden Django Paginator
# we will only return one page, with one item # we will only return one page, with one item
count = 1 count = 1

View File

@ -2,8 +2,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import pytest
from django import forms from django import forms
from django.core.files.uploadhandler import MemoryFileUploadHandler from django.core.files.uploadhandler import (
MemoryFileUploadHandler, TemporaryFileUploadHandler
)
from django.test import TestCase from django.test import TestCase
from django.utils.six.moves import StringIO from django.utils.six.moves import StringIO
@ -63,8 +66,9 @@ class TestFileUploadParser(TestCase):
parser = FileUploadParser() parser = FileUploadParser()
self.stream.seek(0) self.stream.seek(0)
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
with self.assertRaises(ParseError): with pytest.raises(ParseError) as excinfo:
parser.parse(self.stream, None, self.parser_context) parser.parse(self.stream, None, self.parser_context)
assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.'
def test_parse_missing_filename_multiple_upload_handlers(self): def test_parse_missing_filename_multiple_upload_handlers(self):
""" """
@ -78,8 +82,23 @@ class TestFileUploadParser(TestCase):
MemoryFileUploadHandler() MemoryFileUploadHandler()
) )
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
with self.assertRaises(ParseError): with pytest.raises(ParseError) as excinfo:
parser.parse(self.stream, None, self.parser_context) parser.parse(self.stream, None, self.parser_context)
assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.'
def test_parse_missing_filename_large_file(self):
"""
Parse raw file upload when filename is missing with TemporaryFileUploadHandler.
"""
parser = FileUploadParser()
self.stream.seek(0)
self.parser_context['request'].upload_handlers = (
TemporaryFileUploadHandler(),
)
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
with pytest.raises(ParseError) as excinfo:
parser.parse(self.stream, None, self.parser_context)
assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.'
def test_get_filename(self): def test_get_filename(self):
parser = FileUploadParser() parser = FileUploadParser()

View File

@ -481,3 +481,90 @@ class TestHTMLFormRenderer(TestCase):
result = renderer.render(self.serializer.data, None, {}) result = renderer.render(self.serializer.data, None, {})
self.assertIsInstance(result, SafeText) self.assertIsInstance(result, SafeText)
class TestChoiceFieldHTMLFormRenderer(TestCase):
"""
Test rendering ChoiceField with HTMLFormRenderer.
"""
def setUp(self):
choices = ((1, 'Option1'), (2, 'Option2'), (12, 'Option12'))
class TestSerializer(serializers.Serializer):
test_field = serializers.ChoiceField(choices=choices,
initial=2)
self.TestSerializer = TestSerializer
self.renderer = HTMLFormRenderer()
def test_render_initial_option(self):
serializer = self.TestSerializer()
result = self.renderer.render(serializer.data)
self.assertIsInstance(result, SafeText)
self.assertInHTML('<option value="2" selected>Option2</option>',
result)
self.assertInHTML('<option value="1">Option1</option>', result)
self.assertInHTML('<option value="12">Option12</option>', result)
def test_render_selected_option(self):
serializer = self.TestSerializer(data={'test_field': '12'})
serializer.is_valid()
result = self.renderer.render(serializer.data)
self.assertIsInstance(result, SafeText)
self.assertInHTML('<option value="12" selected>Option12</option>',
result)
self.assertInHTML('<option value="1">Option1</option>', result)
self.assertInHTML('<option value="2">Option2</option>', result)
class TestMultipleChoiceFieldHTMLFormRenderer(TestCase):
"""
Test rendering MultipleChoiceField with HTMLFormRenderer.
"""
def setUp(self):
self.renderer = HTMLFormRenderer()
def test_render_selected_option_with_string_option_ids(self):
choices = (('1', 'Option1'), ('2', 'Option2'), ('12', 'Option12'),
('}', 'OptionBrace'))
class TestSerializer(serializers.Serializer):
test_field = serializers.MultipleChoiceField(choices=choices)
serializer = TestSerializer(data={'test_field': ['12']})
serializer.is_valid()
result = self.renderer.render(serializer.data)
self.assertIsInstance(result, SafeText)
self.assertInHTML('<option value="12" selected>Option12</option>',
result)
self.assertInHTML('<option value="1">Option1</option>', result)
self.assertInHTML('<option value="2">Option2</option>', result)
self.assertInHTML('<option value="}">OptionBrace</option>', result)
def test_render_selected_option_with_integer_option_ids(self):
choices = ((1, 'Option1'), (2, 'Option2'), (12, 'Option12'))
class TestSerializer(serializers.Serializer):
test_field = serializers.MultipleChoiceField(choices=choices)
serializer = TestSerializer(data={'test_field': ['12']})
serializer.is_valid()
result = self.renderer.render(serializer.data)
self.assertIsInstance(result, SafeText)
self.assertInHTML('<option value="12" selected>Option12</option>',
result)
self.assertInHTML('<option value="1">Option1</option>', result)
self.assertInHTML('<option value="2">Option2</option>', result)

View File

@ -5,6 +5,7 @@ from django.test import TestCase, override_settings
from rest_framework import filters, pagination, permissions, serializers from rest_framework import filters, pagination, permissions, serializers
from rest_framework.compat import coreapi from rest_framework.compat import coreapi
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.schemas import SchemaGenerator from rest_framework.schemas import SchemaGenerator
@ -23,16 +24,33 @@ class ExamplePagination(pagination.PageNumberPagination):
class ExampleSerializer(serializers.Serializer): class ExampleSerializer(serializers.Serializer):
a = serializers.CharField(required=True) a = serializers.CharField(required=True, help_text='A field description')
b = serializers.CharField(required=False) b = serializers.CharField(required=False)
class AnotherSerializer(serializers.Serializer):
c = serializers.CharField(required=True)
d = serializers.CharField(required=False)
class ExampleViewSet(ModelViewSet): class ExampleViewSet(ModelViewSet):
pagination_class = ExamplePagination pagination_class = ExamplePagination
permission_classes = [permissions.IsAuthenticatedOrReadOnly] permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [filters.OrderingFilter] filter_backends = [filters.OrderingFilter]
serializer_class = ExampleSerializer serializer_class = ExampleSerializer
@detail_route(methods=['post'], serializer_class=AnotherSerializer)
def custom_action(self, request, pk):
return super(ExampleSerializer, self).retrieve(self, request)
@list_route()
def custom_list_action(self, request):
return super(ExampleViewSet, self).list(self, request)
def get_serializer(self, *args, **kwargs):
assert self.request
return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
class ExampleView(APIView): class ExampleView(APIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly] permission_classes = [permissions.IsAuthenticatedOrReadOnly]
@ -74,6 +92,10 @@ class TestRouterGeneratedSchema(TestCase):
coreapi.Field('ordering', required=False, location='query') coreapi.Field('ordering', required=False, location='query')
] ]
), ),
'custom_list_action': coreapi.Link(
url='/example/custom_list_action/',
action='get'
),
'retrieve': coreapi.Link( 'retrieve': coreapi.Link(
url='/example/{pk}/', url='/example/{pk}/',
action='get', action='get',
@ -109,7 +131,7 @@ class TestRouterGeneratedSchema(TestCase):
action='post', action='post',
encoding='application/json', encoding='application/json',
fields=[ fields=[
coreapi.Field('a', required=True, location='form'), coreapi.Field('a', required=True, location='form', description='A field description'),
coreapi.Field('b', required=False, location='form') coreapi.Field('b', required=False, location='form')
] ]
), ),
@ -120,13 +142,27 @@ class TestRouterGeneratedSchema(TestCase):
coreapi.Field('pk', required=True, location='path') coreapi.Field('pk', required=True, location='path')
] ]
), ),
'custom_action': coreapi.Link(
url='/example/{pk}/custom_action/',
action='post',
encoding='application/json',
fields=[
coreapi.Field('pk', required=True, location='path'),
coreapi.Field('c', required=True, location='form'),
coreapi.Field('d', required=False, location='form'),
]
),
'custom_list_action': coreapi.Link(
url='/example/custom_list_action/',
action='get'
),
'update': coreapi.Link( 'update': coreapi.Link(
url='/example/{pk}/', url='/example/{pk}/',
action='put', action='put',
encoding='application/json', encoding='application/json',
fields=[ fields=[
coreapi.Field('pk', required=True, location='path'), coreapi.Field('pk', required=True, location='path'),
coreapi.Field('a', required=True, location='form'), coreapi.Field('a', required=True, location='form', description='A field description'),
coreapi.Field('b', required=False, location='form') coreapi.Field('b', required=False, location='form')
] ]
), ),
@ -136,7 +172,7 @@ class TestRouterGeneratedSchema(TestCase):
encoding='application/json', encoding='application/json',
fields=[ fields=[
coreapi.Field('pk', required=True, location='path'), coreapi.Field('pk', required=True, location='path'),
coreapi.Field('a', required=False, location='form'), coreapi.Field('a', required=False, location='form', description='A field description'),
coreapi.Field('b', required=False, location='form') coreapi.Field('b', required=False, location='form')
] ]
), ),

View File

@ -309,3 +309,31 @@ class TestCacheSerializerData:
pickled = pickle.dumps(serializer.data) pickled = pickle.dumps(serializer.data)
data = pickle.loads(pickled) data = pickle.loads(pickled)
assert data == {'field1': 'a', 'field2': 'b'} assert data == {'field1': 'a', 'field2': 'b'}
class TestDefaultInclusions:
def setup(self):
class ExampleSerializer(serializers.Serializer):
char = serializers.CharField(read_only=True, default='abc')
integer = serializers.IntegerField()
self.Serializer = ExampleSerializer
def test_default_should_included_on_create(self):
serializer = self.Serializer(data={'integer': 456})
assert serializer.is_valid()
assert serializer.validated_data == {'char': 'abc', 'integer': 456}
assert serializer.errors == {}
def test_default_should_be_included_on_update(self):
instance = MockObject(char='def', integer=123)
serializer = self.Serializer(instance, data={'integer': 456})
assert serializer.is_valid()
assert serializer.validated_data == {'char': 'abc', 'integer': 456}
assert serializer.errors == {}
def test_default_should_not_be_included_on_partial_update(self):
instance = MockObject(char='def', integer=123)
serializer = self.Serializer(instance, data={'integer': 456}, partial=True)
assert serializer.is_valid()
assert serializer.validated_data == {'integer': 456}
assert serializer.errors == {}

View File

@ -13,7 +13,7 @@ factory = APIRequestFactory()
class TemplateTagTests(TestCase): class TemplateTagTests(TestCase):
def test_add_query_param_with_non_latin_charactor(self): def test_add_query_param_with_non_latin_character(self):
# Ensure we don't double-escape non-latin characters # Ensure we don't double-escape non-latin characters
# that are present in the querystring. # that are present in the querystring.
# See #1314. # See #1314.

View File

@ -78,7 +78,7 @@ class TestAPITestClient(TestCase):
response = self.client.get('/session-view/') response = self.client.get('/session-view/')
self.assertEqual(response.data['active_session'], False) self.assertEqual(response.data['active_session'], False)
# Subsequant requests have an active session # Subsequent requests have an active session
response = self.client.get('/session-view/') response = self.client.get('/session-view/')
self.assertEqual(response.data['active_session'], True) self.assertEqual(response.data['active_session'], True)

View File

@ -44,14 +44,34 @@ class ReverseView(APIView):
return Response({'url': reverse('another', request=request)}) return Response({'url': reverse('another', request=request)})
class RequestInvalidVersionView(APIView): class AllowedVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs): def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class() scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2') scheme.allowed_versions = ('v1', 'v2')
return (scheme.determine_version(request, *args, **kwargs), scheme) return (scheme.determine_version(request, *args, **kwargs), scheme)
def get(self, request, *args, **kwargs):
return Response({'version': request.version}) class AllowedAndDefaultVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2')
scheme.default_version = 'v2'
return (scheme.determine_version(request, *args, **kwargs), scheme)
class AllowedWithNoneVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2', None)
return (scheme.determine_version(request, *args, **kwargs), scheme)
class AllowedWithNoneAndDefaultVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2', None)
scheme.default_version = 'v2'
return (scheme.determine_version(request, *args, **kwargs), scheme)
factory = APIRequestFactory() factory = APIRequestFactory()
@ -219,7 +239,7 @@ class TestURLReversing(URLPatternsTestCase):
class TestInvalidVersion: class TestInvalidVersion:
def test_invalid_query_param_versioning(self): def test_invalid_query_param_versioning(self):
scheme = versioning.QueryParameterVersioning scheme = versioning.QueryParameterVersioning
view = RequestInvalidVersionView.as_view(versioning_class=scheme) view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/?version=v3') request = factory.get('/endpoint/?version=v3')
response = view(request) response = view(request)
@ -228,7 +248,7 @@ class TestInvalidVersion:
@override_settings(ALLOWED_HOSTS=['*']) @override_settings(ALLOWED_HOSTS=['*'])
def test_invalid_host_name_versioning(self): def test_invalid_host_name_versioning(self):
scheme = versioning.HostNameVersioning scheme = versioning.HostNameVersioning
view = RequestInvalidVersionView.as_view(versioning_class=scheme) view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_HOST='v3.example.org') request = factory.get('/endpoint/', HTTP_HOST='v3.example.org')
response = view(request) response = view(request)
@ -236,7 +256,7 @@ class TestInvalidVersion:
def test_invalid_accept_header_versioning(self): def test_invalid_accept_header_versioning(self):
scheme = versioning.AcceptHeaderVersioning scheme = versioning.AcceptHeaderVersioning
view = RequestInvalidVersionView.as_view(versioning_class=scheme) view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=v3') request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=v3')
response = view(request) response = view(request)
@ -244,7 +264,7 @@ class TestInvalidVersion:
def test_invalid_url_path_versioning(self): def test_invalid_url_path_versioning(self):
scheme = versioning.URLPathVersioning scheme = versioning.URLPathVersioning
view = RequestInvalidVersionView.as_view(versioning_class=scheme) view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/v3/endpoint/') request = factory.get('/v3/endpoint/')
response = view(request, version='v3') response = view(request, version='v3')
@ -255,7 +275,7 @@ class TestInvalidVersion:
namespace = 'v3' namespace = 'v3'
scheme = versioning.NamespaceVersioning scheme = versioning.NamespaceVersioning
view = RequestInvalidVersionView.as_view(versioning_class=scheme) view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/v3/endpoint/') request = factory.get('/v3/endpoint/')
request.resolver_match = FakeResolverMatch request.resolver_match = FakeResolverMatch
@ -263,6 +283,52 @@ class TestInvalidVersion:
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAllowedAndDefaultVersion:
def test_missing_without_default(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_406_NOT_ACCEPTABLE
def test_missing_with_default(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {'version': 'v2'}
def test_with_default(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/',
HTTP_ACCEPT='application/json; version=v2')
response = view(request)
assert response.status_code == status.HTTP_200_OK
def test_missing_without_default_but_none_allowed(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedWithNoneVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {'version': None}
def test_missing_with_default_and_none_allowed(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedWithNoneAndDefaultVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {'version': 'v2'}
class TestHyperlinkedRelatedField(URLPatternsTestCase): class TestHyperlinkedRelatedField(URLPatternsTestCase):
included = [ included = [
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'), url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'),

View File

@ -16,8 +16,8 @@ setenv =
PYTHONWARNINGS=once PYTHONWARNINGS=once
deps = deps =
django18: Django==1.8.14 django18: Django==1.8.14
django19: Django==1.9.8 django19: Django==1.9.9
django110: Django==1.10rc1 django110: Django==1.10
djangomaster: https://github.com/django/django/archive/master.tar.gz djangomaster: https://github.com/django/django/archive/master.tar.gz
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt -rrequirements/requirements-optionals.txt