mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-07 13:54:47 +03:00
Merge branch 'master' into requests-client
This commit is contained in:
commit
5abac93c01
|
@ -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]**.
|
||||
|
||||
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.*
|
||||
*Every single sign-up makes a significant impact towards making that possible.*
|
||||
|
||||
<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>
|
||||
|
|
|
@ -49,7 +49,9 @@ Defaults to `False`
|
|||
|
||||
### `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).
|
||||
|
||||
|
@ -486,7 +488,7 @@ This field is used by default with `ModelSerializer` when including field names
|
|||
|
||||
**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 Meta:
|
||||
|
|
|
@ -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.
|
||||
* 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.
|
||||
* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ This permission is suitable if you want to your API to allow read permissions to
|
|||
|
||||
## 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.
|
||||
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model.
|
||||
|
|
|
@ -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.
|
||||
|
||||
import random
|
||||
|
||||
class RandomRateThrottle(throttling.BaseThrottle):
|
||||
def allow_request(self, request, view):
|
||||
return random.randint(1, 10) == 1
|
||||
|
|
|
@ -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:
|
||||
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
slug = SlugField(
|
||||
max_length=100,
|
||||
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:
|
||||
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
# ...
|
||||
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:
|
||||
|
||||
from rest_framework.validators import UniqueForYearValidator
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
# ...
|
||||
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(
|
||||
read_only=True,
|
||||
default=CreateOnlyDefault(timezone.now)
|
||||
default=serializers.CreateOnlyDefault(timezone.now)
|
||||
)
|
||||
|
||||
---
|
||||
|
|
|
@ -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:
|
||||
|
||||
* `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`.
|
||||
* `VERSION_PARAM`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`.
|
||||
* `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 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`:
|
||||
|
||||
|
|
|
@ -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]**.
|
||||
|
||||
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.*
|
||||
*Every single sign-up makes a significant impact towards making that possible.*
|
||||
|
||||
<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>
|
||||
|
@ -87,7 +86,7 @@ Right now we're over 58% of the way towards achieving that.
|
|||
REST framework requires the following:
|
||||
|
||||
* 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:
|
||||
|
||||
|
|
|
@ -40,6 +40,49 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
## 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
|
||||
|
||||
**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.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.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 -->
|
||||
[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
|
||||
[gh4273]: https://github.com/tomchristie/django-rest-framework/issues/4273
|
||||
[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
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '3.4.1'
|
||||
__version__ = '3.4.4'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2016 Tom Christie'
|
||||
|
|
|
@ -122,6 +122,12 @@ def _resolve_model(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):
|
||||
if django.VERSION < (1, 9):
|
||||
return _resolve_model(field.rel.to)
|
||||
|
@ -162,7 +168,7 @@ except ImportError:
|
|||
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:
|
||||
import coreapi
|
||||
import uritemplate
|
||||
|
|
|
@ -395,8 +395,8 @@ class Field(object):
|
|||
# determine if we should use null instead.
|
||||
return '' if getattr(self, 'allow_blank', False) else None
|
||||
elif ret == '' and not self.required:
|
||||
# If the field is blank, and emptyness is valid then
|
||||
# determine if we should use emptyness instead.
|
||||
# If the field is blank, and emptiness is valid then
|
||||
# determine if we should use emptiness instead.
|
||||
return '' if getattr(self, 'allow_blank', False) else empty
|
||||
return ret
|
||||
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
|
||||
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()
|
||||
if callable(self.default):
|
||||
if hasattr(self.default, 'set_context'):
|
||||
|
@ -671,6 +672,7 @@ class NullBooleanField(Field):
|
|||
|
||||
class CharField(Field):
|
||||
default_error_messages = {
|
||||
'invalid': _('Not a valid string.'),
|
||||
'blank': _('This field may not be blank.'),
|
||||
'max_length': _('Ensure this field has no more than {max_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)
|
||||
|
||||
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)
|
||||
return value.strip() if self.trim_whitespace else value
|
||||
|
||||
|
@ -804,7 +811,10 @@ class IPAddressField(CharField):
|
|||
self.validators.extend(validators)
|
||||
|
||||
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:
|
||||
if self.protocol in ('both', 'ipv6'):
|
||||
return clean_ipv6_address(data, self.unpack_ipv4)
|
||||
|
@ -952,7 +962,7 @@ class DecimalField(Field):
|
|||
if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')):
|
||||
self.fail('invalid')
|
||||
|
||||
return self.validate_precision(value)
|
||||
return self.quantize(self.validate_precision(value))
|
||||
|
||||
def validate_precision(self, value):
|
||||
"""
|
||||
|
@ -1012,10 +1022,12 @@ class DecimalField(Field):
|
|||
return value
|
||||
|
||||
context = decimal.getcontext().copy()
|
||||
context.prec = self.max_digits
|
||||
if self.max_digits is not None:
|
||||
context.prec = self.max_digits
|
||||
return value.quantize(
|
||||
decimal.Decimal('.1') ** self.decimal_places,
|
||||
context=context)
|
||||
context=context
|
||||
)
|
||||
|
||||
|
||||
# Date & time fields...
|
||||
|
@ -1341,7 +1353,7 @@ class FilePathField(ChoiceField):
|
|||
|
||||
def __init__(self, path, match=None, recursive=False, allow_files=True,
|
||||
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.
|
||||
field = DjangoFilePathField(
|
||||
path, match=match, recursive=recursive, allow_files=allow_files,
|
||||
|
@ -1644,7 +1656,7 @@ class SerializerMethodField(Field):
|
|||
def bind(self, field_name, parent):
|
||||
# In order to enforce a consistent style, we error if a redundant
|
||||
# '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)
|
||||
assert self.method_name != default_method_name, (
|
||||
"It is redundant to specify `%s` on SerializerMethodField '%s' in "
|
||||
|
|
|
@ -90,7 +90,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
|
|||
|
||||
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.
|
||||
"""
|
||||
header = request.META.get('HTTP_ACCEPT', '*/*')
|
||||
|
|
|
@ -64,10 +64,10 @@ def _get_displayed_page_numbers(current, final):
|
|||
|
||||
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
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
https://gist.github.com/tomchristie/321140cebb1c4a558b15
|
||||
|
@ -312,6 +312,9 @@ class LimitOffsetPagination(BasePagination):
|
|||
self.request = request
|
||||
if self.count > self.limit and self.template is not None:
|
||||
self.display_page_controls = True
|
||||
|
||||
if self.count == 0 or self.offset > self.count:
|
||||
return []
|
||||
return list(queryset[self.offset:self.offset + self.limit])
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
|
@ -476,10 +479,10 @@ class CursorPagination(BasePagination):
|
|||
|
||||
# Determine the position of the final item following the 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)
|
||||
else:
|
||||
has_following_postion = False
|
||||
has_following_position = False
|
||||
following_position = None
|
||||
|
||||
# If we have a reverse queryset, then the query ordering was in reverse
|
||||
|
@ -490,14 +493,14 @@ class CursorPagination(BasePagination):
|
|||
if reverse:
|
||||
# Determine next and previous positions for reverse cursors.
|
||||
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:
|
||||
self.next_position = current_position
|
||||
if self.has_previous:
|
||||
self.previous_position = following_position
|
||||
else:
|
||||
# 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)
|
||||
if self.has_next:
|
||||
self.next_position = following_position
|
||||
|
@ -534,7 +537,7 @@ class CursorPagination(BasePagination):
|
|||
# our marker.
|
||||
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
|
||||
# the offset and keep seeking to the previous item.
|
||||
compare = position
|
||||
|
@ -582,7 +585,7 @@ class CursorPagination(BasePagination):
|
|||
# our marker.
|
||||
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
|
||||
# the offset and keep seeking to the previous item.
|
||||
compare = position
|
||||
|
|
|
@ -118,6 +118,10 @@ class FileUploadParser(BaseParser):
|
|||
Parser for file upload data.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
|
@ -134,6 +138,9 @@ class FileUploadParser(BaseParser):
|
|||
upload_handlers = request.upload_handlers
|
||||
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
|
||||
# file uploads in MultiPartParser.
|
||||
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.
|
||||
for handler in upload_handlers:
|
||||
result = handler.handle_raw_input(None,
|
||||
result = handler.handle_raw_input(stream,
|
||||
meta,
|
||||
content_length,
|
||||
None,
|
||||
|
@ -178,10 +185,10 @@ class FileUploadParser(BaseParser):
|
|||
|
||||
for index, handler in enumerate(upload_handlers):
|
||||
file_obj = handler.file_complete(counters[index])
|
||||
if file_obj:
|
||||
if file_obj is not None:
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -5,6 +5,9 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework.compat import is_authenticated
|
||||
|
||||
|
||||
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
||||
|
||||
|
||||
|
@ -44,7 +47,7 @@ class IsAuthenticated(BasePermission):
|
|||
"""
|
||||
|
||||
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):
|
||||
|
@ -65,7 +68,7 @@ class IsAuthenticatedOrReadOnly(BasePermission):
|
|||
return (
|
||||
request.method in SAFE_METHODS or
|
||||
request.user and
|
||||
request.user.is_authenticated()
|
||||
is_authenticated(request.user)
|
||||
)
|
||||
|
||||
|
||||
|
@ -127,7 +130,7 @@ class DjangoModelPermissions(BasePermission):
|
|||
|
||||
return (
|
||||
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)
|
||||
)
|
||||
|
||||
|
|
|
@ -156,29 +156,35 @@ class RelatedField(Field):
|
|||
# Standard case, return the object instance.
|
||||
return get_attribute(instance, self.source_attrs)
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
def get_choices(self, cutoff=None):
|
||||
queryset = self.get_queryset()
|
||||
if queryset is None:
|
||||
# Ensure that field.choices returns something sensible
|
||||
# even when accessed with a read-only field.
|
||||
return {}
|
||||
|
||||
if cutoff is not None:
|
||||
queryset = queryset[:cutoff]
|
||||
|
||||
return OrderedDict([
|
||||
(
|
||||
six.text_type(self.to_representation(item)),
|
||||
self.to_representation(item),
|
||||
self.display_value(item)
|
||||
)
|
||||
for item in queryset
|
||||
])
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return self.get_choices()
|
||||
|
||||
@property
|
||||
def grouped_choices(self):
|
||||
return self.choices
|
||||
|
||||
def iter_options(self):
|
||||
return iter_options(
|
||||
self.grouped_choices,
|
||||
self.get_choices(cutoff=self.html_cutoff),
|
||||
cutoff=self.html_cutoff,
|
||||
cutoff_text=self.html_cutoff_text
|
||||
)
|
||||
|
@ -487,9 +493,12 @@ class ManyRelatedField(Field):
|
|||
for value in iterable
|
||||
]
|
||||
|
||||
def get_choices(self, cutoff=None):
|
||||
return self.child_relation.get_choices(cutoff)
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return self.child_relation.choices
|
||||
return self.get_choices()
|
||||
|
||||
@property
|
||||
def grouped_choices(self):
|
||||
|
@ -497,7 +506,7 @@ class ManyRelatedField(Field):
|
|||
|
||||
def iter_options(self):
|
||||
return iter_options(
|
||||
self.grouped_choices,
|
||||
self.get_choices(cutoff=self.html_cutoff),
|
||||
cutoff=self.html_cutoff,
|
||||
cutoff_text=self.html_cutoff_text
|
||||
)
|
||||
|
|
|
@ -166,13 +166,18 @@ class TemplateHTMLRenderer(BaseRenderer):
|
|||
template_names = self.get_template_names(response, view)
|
||||
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)
|
||||
|
||||
def resolve_template(self, 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:
|
||||
data['status_code'] = response.status_code
|
||||
return data
|
||||
|
@ -228,7 +233,10 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
|
|||
if response and response.exception:
|
||||
request = renderer_context['request']
|
||||
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 data
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.conf import settings
|
|||
from django.contrib.admindocs.views import simplify_regex
|
||||
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
default_mapping = {
|
||||
'get': 'read',
|
||||
|
@ -56,6 +39,10 @@ class SchemaGenerator(object):
|
|||
'patch': 'partial_update',
|
||||
'delete': 'destroy',
|
||||
}
|
||||
known_actions = (
|
||||
'create', 'read', 'retrieve', 'list',
|
||||
'update', 'partial_update', 'destroy'
|
||||
)
|
||||
|
||||
def __init__(self, title=None, url=None, patterns=None, urlconf=None):
|
||||
assert coreapi, '`coreapi` must be installed for schema support.'
|
||||
|
@ -65,45 +52,58 @@ class SchemaGenerator(object):
|
|||
urls = import_module(urlconf)
|
||||
else:
|
||||
urls = urlconf
|
||||
patterns = urls.urlpatterns
|
||||
self.patterns = urls.urlpatterns
|
||||
elif patterns is None and urlconf is None:
|
||||
urls = import_module(settings.ROOT_URLCONF)
|
||||
patterns = urls.urlpatterns
|
||||
self.patterns = urls.urlpatterns
|
||||
else:
|
||||
self.patterns = patterns
|
||||
|
||||
if url and not url.endswith('/'):
|
||||
url += '/'
|
||||
|
||||
self.title = title
|
||||
self.url = url
|
||||
self.endpoints = self.get_api_endpoints(patterns)
|
||||
self.endpoints = None
|
||||
|
||||
def get_schema(self, request=None):
|
||||
if request is None:
|
||||
endpoints = self.endpoints
|
||||
else:
|
||||
# Filter the list of endpoints to only include those that
|
||||
# the user has permission on.
|
||||
endpoints = []
|
||||
for key, link, callback in self.endpoints:
|
||||
method = link.action.upper()
|
||||
view = callback.cls()
|
||||
if self.endpoints is None:
|
||||
self.endpoints = self.get_api_endpoints(self.patterns)
|
||||
|
||||
links = []
|
||||
for path, method, category, action, callback in self.endpoints:
|
||||
view = callback.cls()
|
||||
for attr, val in getattr(callback, 'initkwargs', {}).items():
|
||||
setattr(view, attr, val)
|
||||
view.args = ()
|
||||
view.kwargs = {}
|
||||
view.format_kwarg = None
|
||||
|
||||
if request is not None:
|
||||
view.request = clone_request(request, method)
|
||||
view.format_kwarg = None
|
||||
try:
|
||||
view.check_permissions(view.request)
|
||||
except exceptions.APIException:
|
||||
pass
|
||||
else:
|
||||
endpoints.append((key, link, callback))
|
||||
continue
|
||||
else:
|
||||
view.request = None
|
||||
|
||||
if not endpoints:
|
||||
link = self.get_link(path, method, callback, view)
|
||||
links.append((category, action, link))
|
||||
|
||||
if not links:
|
||||
return None
|
||||
|
||||
# Generate the schema content structure, from the endpoints.
|
||||
# ('users', 'list'), Link -> {'users': {'list': Link()}}
|
||||
# Generate the schema content structure, eg:
|
||||
# {'users': {'list': Link()}}
|
||||
content = {}
|
||||
for key, link, callback in endpoints:
|
||||
insert_into(content, key, link)
|
||||
for category, action, link in links:
|
||||
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 coreapi.Document(title=self.title, content=content, url=self.url)
|
||||
|
@ -121,9 +121,9 @@ class SchemaGenerator(object):
|
|||
callback = pattern.callback
|
||||
if self.should_include_endpoint(path, callback):
|
||||
for method in self.get_allowed_methods(callback):
|
||||
key = self.get_key(path, method, callback)
|
||||
link = self.get_link(path, method, callback)
|
||||
endpoint = (key, link, callback)
|
||||
action = self.get_action(path, method, callback)
|
||||
category = self.get_category(path, method, callback, action)
|
||||
endpoint = (path, method, category, action, callback)
|
||||
api_endpoints.append(endpoint)
|
||||
|
||||
elif isinstance(pattern, RegexURLResolver):
|
||||
|
@ -167,35 +167,53 @@ class SchemaGenerator(object):
|
|||
|
||||
return [
|
||||
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
|
||||
given endpoint. eg. ('users', 'list').
|
||||
Return a descriptive action string for the endpoint, eg. 'list'.
|
||||
"""
|
||||
category = None
|
||||
for item in path.strip('/').split('/'):
|
||||
if '{' in item:
|
||||
break
|
||||
category = item
|
||||
|
||||
actions = getattr(callback, 'actions', self.default_mapping)
|
||||
action = actions[method.lower()]
|
||||
return actions[method.lower()]
|
||||
|
||||
if category:
|
||||
return (category, action)
|
||||
return (action,)
|
||||
def get_category(self, path, method, callback, 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...
|
||||
|
||||
def get_link(self, path, method, callback):
|
||||
def get_link(self, path, method, callback, view):
|
||||
"""
|
||||
Return a `coreapi.Link` instance for the given endpoint.
|
||||
"""
|
||||
view = callback.cls()
|
||||
|
||||
fields = self.get_path_fields(path, method, callback, view)
|
||||
fields += self.get_serializer_fields(path, method, callback, view)
|
||||
fields += self.get_pagination_fields(path, method, callback, view)
|
||||
|
@ -206,6 +224,9 @@ class SchemaGenerator(object):
|
|||
else:
|
||||
encoding = None
|
||||
|
||||
if self.url and path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
return coreapi.Link(
|
||||
url=urlparse.urljoin(self.url, path),
|
||||
action=method.lower(),
|
||||
|
@ -255,25 +276,29 @@ class SchemaGenerator(object):
|
|||
if method not in ('PUT', 'PATCH', 'POST'):
|
||||
return []
|
||||
|
||||
if not hasattr(view, 'get_serializer_class'):
|
||||
if not hasattr(view, 'get_serializer'):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
|
||||
serializer_class = view.get_serializer_class()
|
||||
serializer = serializer_class()
|
||||
serializer = view.get_serializer()
|
||||
|
||||
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):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
for field in serializer.fields.values():
|
||||
if field.read_only:
|
||||
continue
|
||||
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)
|
||||
|
||||
return fields
|
||||
|
|
|
@ -1324,9 +1324,8 @@ class ModelSerializer(Serializer):
|
|||
# Update `extra_kwargs` with any new options.
|
||||
for key, value in uniqueness_extra_kwargs.items():
|
||||
if key in extra_kwargs:
|
||||
extra_kwargs[key].update(value)
|
||||
else:
|
||||
extra_kwargs[key] = value
|
||||
value.update(extra_kwargs[key])
|
||||
extra_kwargs[key] = value
|
||||
|
||||
return extra_kwargs, hidden_fields
|
||||
|
||||
|
@ -1384,7 +1383,7 @@ class ModelSerializer(Serializer):
|
|||
|
||||
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 = (
|
||||
[self.Meta.model] +
|
||||
|
@ -1415,7 +1414,7 @@ class ModelSerializer(Serializer):
|
|||
|
||||
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_month
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load rest_framework %}
|
||||
|
||||
<div class="form-group">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">
|
||||
|
@ -9,7 +11,7 @@
|
|||
{% if style.inline %}
|
||||
{% for key, text in field.choices.items %}
|
||||
<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 }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
@ -17,7 +19,7 @@
|
|||
{% for key, text in field.choices.items %}
|
||||
<div class="checkbox">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{% load i18n %}
|
||||
{% load rest_framework %}
|
||||
|
||||
{% trans "None" as none_choice %}
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -19,7 +21,7 @@
|
|||
|
||||
{% for key, text in field.choices.items %}
|
||||
<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 }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
@ -35,7 +37,7 @@
|
|||
{% for key, text in field.choices.items %}
|
||||
<div class="radio">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load rest_framework %}
|
||||
|
||||
<div class="form-group">
|
||||
{% if field.label %}
|
||||
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">
|
||||
|
@ -16,7 +18,7 @@
|
|||
{% elif select.end_option_group %}
|
||||
</optgroup>
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{% load i18n %}
|
||||
{% load rest_framework %}
|
||||
|
||||
{% trans "No items to select." as no_items %}
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -16,7 +18,7 @@
|
|||
{% elif select.end_option_group %}
|
||||
</optgroup>
|
||||
{% 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 %}
|
||||
{% empty %}
|
||||
<option>{{ no_items }}</option>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load rest_framework %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">{{ field.label }}</label>
|
||||
|
@ -6,7 +8,7 @@
|
|||
{% for key, text in field.choices.items %}
|
||||
<div class="checkbox">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load rest_framework %}
|
||||
{% trans "None" as none_choice %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
|
@ -20,7 +21,7 @@
|
|||
{% for key, text in field.choices.items %}
|
||||
<div class="radio">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load rest_framework %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label class="sr-only">
|
||||
|
@ -15,7 +17,7 @@
|
|||
{% elif select.end_option_group %}
|
||||
</optgroup>
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load rest_framework %}
|
||||
{% trans "No items to select." as no_items %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
|
@ -15,7 +16,7 @@
|
|||
{% elif select.end_option_group %}
|
||||
</optgroup>
|
||||
{% 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 %}
|
||||
{% empty %}
|
||||
<option>{{ no_items }}</option>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load rest_framework %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
|
||||
|
@ -7,7 +9,7 @@
|
|||
<div>
|
||||
{% for key, text in field.choices.items %}
|
||||
<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 }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
@ -16,7 +18,7 @@
|
|||
{% for key, text in field.choices.items %}
|
||||
<div class="checkbox">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load rest_framework %}
|
||||
{% trans "None" as none_choice %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
|
@ -19,7 +20,7 @@
|
|||
|
||||
{% for key, text in field.choices.items %}
|
||||
<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 }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
@ -37,7 +38,7 @@
|
|||
{% for key, text in field.choices.items %}
|
||||
<div class="radio">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load rest_framework %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% if field.label %}
|
||||
<label {% if style.hide_label %}class="sr-only"{% endif %}>
|
||||
|
@ -15,7 +17,7 @@
|
|||
{% elif select.end_option_group %}
|
||||
</optgroup>
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load rest_framework %}
|
||||
{% trans "No items to select." as no_items %}
|
||||
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
|
@ -15,7 +16,7 @@
|
|||
{% elif select.end_option_group %}
|
||||
</optgroup>
|
||||
{% 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 %}
|
||||
{% empty %}
|
||||
<option>{{ no_items }}</option>
|
||||
|
|
|
@ -89,6 +89,21 @@ def add_query_param(request, 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
|
||||
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.
|
||||
|
||||
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"
|
||||
attribute.
|
||||
|
|
|
@ -8,6 +8,7 @@ import time
|
|||
from django.core.cache import cache as default_cache
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from rest_framework.compat import is_authenticated
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
|
@ -173,7 +174,7 @@ class AnonRateThrottle(SimpleRateThrottle):
|
|||
scope = 'anon'
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
if request.user.is_authenticated():
|
||||
if is_authenticated(request.user):
|
||||
return None # Only throttle unauthenticated requests.
|
||||
|
||||
return self.cache_format % {
|
||||
|
@ -193,7 +194,7 @@ class UserRateThrottle(SimpleRateThrottle):
|
|||
scope = 'user'
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
if request.user.is_authenticated():
|
||||
if is_authenticated(request.user):
|
||||
ident = request.user.pk
|
||||
else:
|
||||
ident = self.get_ident(request)
|
||||
|
@ -241,7 +242,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
|
|||
Otherwise generate the unique cache key by concatenating the user id
|
||||
with the '.throttle_scope` property of the view.
|
||||
"""
|
||||
if request.user.is_authenticated():
|
||||
if is_authenticated(request.user):
|
||||
ident = request.user.pk
|
||||
else:
|
||||
ident = self.get_ident(request)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
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
|
||||
|
||||
|
|
|
@ -32,13 +32,22 @@ def dedent(content):
|
|||
unindented text on the initial line.
|
||||
"""
|
||||
content = force_text(content)
|
||||
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
||||
for line in content.splitlines()[1:] if line.lstrip()]
|
||||
whitespace_counts = [
|
||||
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
|
||||
if whitespace_counts:
|
||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||
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()
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ def is_html_input(dictionary):
|
|||
|
||||
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.
|
||||
|
||||
* List of primitives.
|
||||
|
|
|
@ -78,7 +78,7 @@ class BoundField(object):
|
|||
))
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ def replace_query_param(url, key, val):
|
|||
parameters of the URL, and return the new 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 = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
|
||||
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.
|
||||
"""
|
||||
(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 = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
|
|
@ -30,7 +30,8 @@ class BaseVersioning(object):
|
|||
def is_allowed_version(self, version):
|
||||
if not self.allowed_versions:
|
||||
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):
|
||||
|
@ -94,7 +95,7 @@ class NamespaceVersioning(BaseVersioning):
|
|||
The difference is in the backend - this implementation uses
|
||||
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
|
||||
urlpatterns = [
|
||||
|
@ -147,7 +148,7 @@ class HostNameVersioning(BaseVersioning):
|
|||
invalid_version_message = _('Invalid version in hostname.')
|
||||
|
||||
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)
|
||||
if not match:
|
||||
return self.default_version
|
||||
|
|
|
@ -3,13 +3,17 @@ Provides an APIView class that is the base of all views in REST framework.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
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.encoding import smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views import debug
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import View
|
||||
|
||||
|
@ -91,7 +95,11 @@ def exception_handler(exc, context):
|
|||
set_rollback()
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ class ViewSetMixin(object):
|
|||
# generation can pick out these bits of information from a
|
||||
# resolved URL.
|
||||
view.cls = cls
|
||||
view.initkwargs = initkwargs
|
||||
view.suffix = initkwargs.get('suffix', None)
|
||||
view.actions = actions
|
||||
return csrf_exempt(view)
|
||||
|
@ -111,7 +112,7 @@ class ViewSetMixin(object):
|
|||
if method == 'options':
|
||||
# This is a special case as we always provide handling for the
|
||||
# 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'
|
||||
else:
|
||||
self.action = self.action_map.get(method)
|
||||
|
|
|
@ -440,7 +440,7 @@ class FailingAuthAccessedInRenderer(TestCase):
|
|||
class NoAuthenticationClassesTests(TestCase):
|
||||
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
|
||||
code returned should be 403 with the exception's message.
|
||||
"""
|
||||
|
|
|
@ -55,6 +55,30 @@ class TestSimpleBoundField:
|
|||
assert serializer['bool_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:
|
||||
def test_nested_empty_bound_field(self):
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.test import TestCase
|
|||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from rest_framework.compat import apply_markdown
|
||||
from rest_framework.utils.formatting import dedent
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
|
@ -120,3 +121,7 @@ class TestViewNamesAndDescriptions(TestCase):
|
|||
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
|
||||
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
|
||||
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'
|
||||
|
|
|
@ -535,6 +535,8 @@ class TestCharField(FieldValues):
|
|||
'abc': 'abc'
|
||||
}
|
||||
invalid_inputs = {
|
||||
(): ['Not a valid string.'],
|
||||
True: ['Not a valid string.'],
|
||||
'': ['This field may not be blank.']
|
||||
}
|
||||
outputs = {
|
||||
|
@ -663,6 +665,7 @@ class TestIPAddressField(FieldValues):
|
|||
'127.122.111.2231': ['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.'],
|
||||
1000: ['Enter a valid IPv4 or IPv6 address.'],
|
||||
}
|
||||
outputs = {}
|
||||
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):
|
||||
"""
|
||||
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))
|
||||
|
||||
|
||||
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):
|
||||
valid_inputs = {
|
||||
'0.12345': Decimal('0.12345'),
|
||||
|
@ -953,7 +988,7 @@ class TestDateField(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 = {
|
||||
'1 Jan 2001': datetime.date(2001, 1, 1),
|
||||
|
@ -1020,7 +1055,7 @@ class TestDateTimeField(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 = {
|
||||
'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."
|
||||
)
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -711,7 +711,7 @@ class OrderingFilterTests(TestCase):
|
|||
serializer_class = OrderingFilterSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
oredering_fields = ('text',)
|
||||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('')
|
||||
|
@ -819,7 +819,7 @@ class OrderingFilterTests(TestCase):
|
|||
queryset = OrderingFilterModel.objects.all()
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
# note: no ordering_fields and serializer_class speficied
|
||||
# note: no ordering_fields and serializer_class specified
|
||||
|
||||
def get_serializer_class(self):
|
||||
return OrderingFilterSerializer
|
||||
|
@ -842,7 +842,7 @@ class OrderingFilterTests(TestCase):
|
|||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
# note: no ordering_fields and serializer_class
|
||||
# or get_serializer_class speficied
|
||||
# or get_serializer_class specified
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('/', {'ordering': 'text'})
|
||||
|
|
|
@ -136,7 +136,7 @@ class TestModelSerializer(TestCase):
|
|||
class TestRegularFieldMappings(TestCase):
|
||||
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 Meta:
|
||||
|
@ -614,7 +614,7 @@ class TestRelationalFieldDisplayValue(TestCase):
|
|||
fields = '__all__'
|
||||
|
||||
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)
|
||||
|
||||
def test_custom_display_value(self):
|
||||
|
@ -630,7 +630,7 @@ class TestRelationalFieldDisplayValue(TestCase):
|
|||
fields = '__all__'
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -976,3 +976,22 @@ class TestModelFieldValues(TestCase):
|
|||
source = OneToOneSourceTestModel(target=target)
|
||||
serializer = ExampleSerializer(source)
|
||||
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)
|
||||
|
|
|
@ -67,8 +67,8 @@ class TestPaginationIntegration:
|
|||
|
||||
def test_setting_page_size_over_maximum(self):
|
||||
"""
|
||||
When page_size parameter exceeds maxiumum allowable,
|
||||
then it should be capped to the maxiumum.
|
||||
When page_size parameter exceeds maximum allowable,
|
||||
then it should be capped to the maximum.
|
||||
"""
|
||||
request = factory.get('/', {'page_size': 1000})
|
||||
response = self.view(request)
|
||||
|
@ -108,6 +108,17 @@ class TestPaginationIntegration:
|
|||
'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):
|
||||
request = factory.get('/', {'page': '0'})
|
||||
response = self.view(request)
|
||||
|
@ -259,7 +270,7 @@ class TestPageNumberPaginationOverride:
|
|||
|
||||
def setup(self):
|
||||
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
|
||||
count = 1
|
||||
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pytest
|
||||
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.utils.six.moves import StringIO
|
||||
|
||||
|
@ -63,8 +66,9 @@ class TestFileUploadParser(TestCase):
|
|||
parser = FileUploadParser()
|
||||
self.stream.seek(0)
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
|
@ -78,8 +82,23 @@ class TestFileUploadParser(TestCase):
|
|||
MemoryFileUploadHandler()
|
||||
)
|
||||
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)
|
||||
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):
|
||||
parser = FileUploadParser()
|
||||
|
|
|
@ -481,3 +481,90 @@ class TestHTMLFormRenderer(TestCase):
|
|||
result = renderer.render(self.serializer.data, None, {})
|
||||
|
||||
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)
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.test import TestCase, override_settings
|
|||
|
||||
from rest_framework import filters, pagination, permissions, serializers
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.schemas import SchemaGenerator
|
||||
|
@ -23,16 +24,33 @@ class ExamplePagination(pagination.PageNumberPagination):
|
|||
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
a = serializers.CharField(required=True)
|
||||
a = serializers.CharField(required=True, help_text='A field description')
|
||||
b = serializers.CharField(required=False)
|
||||
|
||||
|
||||
class AnotherSerializer(serializers.Serializer):
|
||||
c = serializers.CharField(required=True)
|
||||
d = serializers.CharField(required=False)
|
||||
|
||||
|
||||
class ExampleViewSet(ModelViewSet):
|
||||
pagination_class = ExamplePagination
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
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):
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
|
@ -74,6 +92,10 @@ class TestRouterGeneratedSchema(TestCase):
|
|||
coreapi.Field('ordering', required=False, location='query')
|
||||
]
|
||||
),
|
||||
'custom_list_action': coreapi.Link(
|
||||
url='/example/custom_list_action/',
|
||||
action='get'
|
||||
),
|
||||
'retrieve': coreapi.Link(
|
||||
url='/example/{pk}/',
|
||||
action='get',
|
||||
|
@ -109,7 +131,7 @@ class TestRouterGeneratedSchema(TestCase):
|
|||
action='post',
|
||||
encoding='application/json',
|
||||
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')
|
||||
]
|
||||
),
|
||||
|
@ -120,13 +142,27 @@ class TestRouterGeneratedSchema(TestCase):
|
|||
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(
|
||||
url='/example/{pk}/',
|
||||
action='put',
|
||||
encoding='application/json',
|
||||
fields=[
|
||||
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')
|
||||
]
|
||||
),
|
||||
|
@ -136,7 +172,7 @@ class TestRouterGeneratedSchema(TestCase):
|
|||
encoding='application/json',
|
||||
fields=[
|
||||
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')
|
||||
]
|
||||
),
|
||||
|
|
|
@ -309,3 +309,31 @@ class TestCacheSerializerData:
|
|||
pickled = pickle.dumps(serializer.data)
|
||||
data = pickle.loads(pickled)
|
||||
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 == {}
|
||||
|
|
|
@ -13,7 +13,7 @@ factory = APIRequestFactory()
|
|||
|
||||
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
|
||||
# that are present in the querystring.
|
||||
# See #1314.
|
||||
|
|
|
@ -78,7 +78,7 @@ class TestAPITestClient(TestCase):
|
|||
response = self.client.get('/session-view/')
|
||||
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/')
|
||||
self.assertEqual(response.data['active_session'], True)
|
||||
|
||||
|
|
|
@ -44,14 +44,34 @@ class ReverseView(APIView):
|
|||
return Response({'url': reverse('another', request=request)})
|
||||
|
||||
|
||||
class RequestInvalidVersionView(APIView):
|
||||
class AllowedVersionsView(RequestVersionView):
|
||||
def determine_version(self, request, *args, **kwargs):
|
||||
scheme = self.versioning_class()
|
||||
scheme.allowed_versions = ('v1', 'v2')
|
||||
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()
|
||||
|
@ -219,7 +239,7 @@ class TestURLReversing(URLPatternsTestCase):
|
|||
class TestInvalidVersion:
|
||||
def test_invalid_query_param_versioning(self):
|
||||
scheme = versioning.QueryParameterVersioning
|
||||
view = RequestInvalidVersionView.as_view(versioning_class=scheme)
|
||||
view = AllowedVersionsView.as_view(versioning_class=scheme)
|
||||
|
||||
request = factory.get('/endpoint/?version=v3')
|
||||
response = view(request)
|
||||
|
@ -228,7 +248,7 @@ class TestInvalidVersion:
|
|||
@override_settings(ALLOWED_HOSTS=['*'])
|
||||
def test_invalid_host_name_versioning(self):
|
||||
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')
|
||||
response = view(request)
|
||||
|
@ -236,7 +256,7 @@ class TestInvalidVersion:
|
|||
|
||||
def test_invalid_accept_header_versioning(self):
|
||||
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')
|
||||
response = view(request)
|
||||
|
@ -244,7 +264,7 @@ class TestInvalidVersion:
|
|||
|
||||
def test_invalid_url_path_versioning(self):
|
||||
scheme = versioning.URLPathVersioning
|
||||
view = RequestInvalidVersionView.as_view(versioning_class=scheme)
|
||||
view = AllowedVersionsView.as_view(versioning_class=scheme)
|
||||
|
||||
request = factory.get('/v3/endpoint/')
|
||||
response = view(request, version='v3')
|
||||
|
@ -255,7 +275,7 @@ class TestInvalidVersion:
|
|||
namespace = 'v3'
|
||||
|
||||
scheme = versioning.NamespaceVersioning
|
||||
view = RequestInvalidVersionView.as_view(versioning_class=scheme)
|
||||
view = AllowedVersionsView.as_view(versioning_class=scheme)
|
||||
|
||||
request = factory.get('/v3/endpoint/')
|
||||
request.resolver_match = FakeResolverMatch
|
||||
|
@ -263,6 +283,52 @@ class TestInvalidVersion:
|
|||
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):
|
||||
included = [
|
||||
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'),
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -16,8 +16,8 @@ setenv =
|
|||
PYTHONWARNINGS=once
|
||||
deps =
|
||||
django18: Django==1.8.14
|
||||
django19: Django==1.9.8
|
||||
django110: Django==1.10rc1
|
||||
django19: Django==1.9.9
|
||||
django110: Django==1.10
|
||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-optionals.txt
|
||||
|
|
Loading…
Reference in New Issue
Block a user