Merge branch 'master' into requests-client

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

View File

@ -18,8 +18,7 @@ REST framework commercially we strongly encourage you to invest in its
continued development by **[signing up for a paid plan][funding]**.
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>

View File

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

View File

@ -241,7 +241,6 @@ For more details on using filter sets see the [django-filter documentation][djan
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
* 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.
---

View File

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

View File

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

View File

@ -64,6 +64,8 @@ It takes a single required argument, and an optional `messages` argument:
This validator should be applied to *serializer fields*, like so:
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)
)
---

View File

@ -71,8 +71,8 @@ You can also set the versioning scheme on an individual view. Typically you won'
The following settings keys are also used to control versioning:
* `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`:

View File

@ -68,8 +68,7 @@ REST framework commercially we strongly encourage you to invest in its
continued development by **[signing up for a paid plan][funding]**.
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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -166,13 +166,18 @@ class TemplateHTMLRenderer(BaseRenderer):
template_names = self.get_template_names(response, view)
template = self.resolve_template(template_names)
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)
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

View File

@ -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()
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()
view.request = clone_request(request, method)
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)
try:
view.check_permissions(view.request)
except exceptions.APIException:
pass
continue
else:
endpoints.append((key, link, callback))
view.request = None
if not endpoints:
link = self.get_link(path, method, callback, view)
links.append((category, action, link))
if not links:
return None
# 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

View File

@ -1324,8 +1324,7 @@ 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:
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,17 @@ Provides an APIView class that is the base of all views in REST framework.
"""
from __future__ import unicode_literals
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 == {}

View File

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

View File

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

View File

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

View File

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