aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/compat.py5
-rw-r--r--rest_framework/fields.py65
-rw-r--r--rest_framework/metadata.py8
-rw-r--r--rest_framework/pagination.py6
-rw-r--r--rest_framework/renderers.py10
-rw-r--r--rest_framework/request.py6
-rw-r--r--rest_framework/routers.py40
-rw-r--r--rest_framework/serializers.py17
-rw-r--r--rest_framework/settings.py4
-rw-r--r--rest_framework/templates/rest_framework/horizontal/select_multiple.html9
-rw-r--r--rest_framework/templates/rest_framework/inline/select_multiple.html9
-rw-r--r--rest_framework/templates/rest_framework/vertical/select_multiple.html9
-rw-r--r--rest_framework/templatetags/rest_framework.py4
-rw-r--r--rest_framework/test.py3
-rw-r--r--rest_framework/utils/model_meta.py4
-rw-r--r--rest_framework/validators.py7
17 files changed, 121 insertions, 87 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 9b58f09f..f8bbeee3 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '3.0.5'
+__version__ = '3.1.0'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2015 Tom Christie'
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 50f37014..c6a4a869 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -12,7 +12,10 @@ from django.utils.six.moves.urllib.parse import urlparse as _urlparse
from django.utils import six
import django
import inspect
-
+try:
+ import importlib
+except ImportError:
+ from django.utils import importlib
def unicode_repr(instance):
# Get the repr of an instance, but ensure it is a unicode string
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f2791a13..c0f93816 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -563,15 +563,15 @@ class CharField(Field):
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
- max_length = kwargs.pop('max_length', None)
- min_length = kwargs.pop('min_length', None)
+ self.max_length = kwargs.pop('max_length', None)
+ self.min_length = kwargs.pop('min_length', None)
super(CharField, self).__init__(**kwargs)
- if max_length is not None:
- message = self.error_messages['max_length'].format(max_length=max_length)
- self.validators.append(MaxLengthValidator(max_length, message=message))
- if min_length is not None:
- message = self.error_messages['min_length'].format(min_length=min_length)
- self.validators.append(MinLengthValidator(min_length, message=message))
+ if self.max_length is not None:
+ message = self.error_messages['max_length'].format(max_length=self.max_length)
+ self.validators.append(MaxLengthValidator(self.max_length, message=message))
+ if self.min_length is not None:
+ message = self.error_messages['min_length'].format(min_length=self.min_length)
+ self.validators.append(MinLengthValidator(self.min_length, message=message))
def run_validation(self, data=empty):
# Test for the empty string here so that it does not get validated,
@@ -665,15 +665,15 @@ class IntegerField(Field):
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
def __init__(self, **kwargs):
- max_value = kwargs.pop('max_value', None)
- min_value = kwargs.pop('min_value', None)
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
super(IntegerField, self).__init__(**kwargs)
- if max_value is not None:
- message = self.error_messages['max_value'].format(max_value=max_value)
- self.validators.append(MaxValueValidator(max_value, message=message))
- if min_value is not None:
- message = self.error_messages['min_value'].format(min_value=min_value)
- self.validators.append(MinValueValidator(min_value, message=message))
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data):
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
@@ -699,15 +699,15 @@ class FloatField(Field):
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
def __init__(self, **kwargs):
- max_value = kwargs.pop('max_value', None)
- min_value = kwargs.pop('min_value', None)
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
super(FloatField, self).__init__(**kwargs)
- if max_value is not None:
- message = self.error_messages['max_value'].format(max_value=max_value)
- self.validators.append(MaxValueValidator(max_value, message=message))
- if min_value is not None:
- message = self.error_messages['min_value'].format(min_value=min_value)
- self.validators.append(MinValueValidator(min_value, message=message))
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data):
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
@@ -740,13 +740,18 @@ class DecimalField(Field):
self.max_digits = max_digits
self.decimal_places = decimal_places
self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string
+
+ self.max_value = max_value
+ self.min_value = min_value
+
super(DecimalField, self).__init__(**kwargs)
- if max_value is not None:
- message = self.error_messages['max_value'].format(max_value=max_value)
- self.validators.append(MaxValueValidator(max_value, message=message))
- if min_value is not None:
- message = self.error_messages['min_value'].format(min_value=min_value)
- self.validators.append(MinValueValidator(min_value, message=message))
+
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data):
"""
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
index 3b058fab..bf3611aa 100644
--- a/rest_framework/metadata.py
+++ b/rest_framework/metadata.py
@@ -115,7 +115,13 @@ class SimpleMetadata(BaseMetadata):
field_info['type'] = self.label_lookup[field]
field_info['required'] = getattr(field, 'required', False)
- for attr in ['read_only', 'label', 'help_text', 'min_length', 'max_length']:
+ attrs = [
+ 'read_only', 'label', 'help_text',
+ 'min_length', 'max_length',
+ 'min_value', 'max_value'
+ ]
+
+ for attr in attrs:
value = getattr(field, attr, None)
if value is not None and value != '':
field_info[attr] = force_text(value, strings_only=True)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 496500ba..80985873 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -259,7 +259,7 @@ class PageNumberPagination(BasePagination):
)
raise NotFound(msg)
- if paginator.count > 1:
+ if paginator.count > 1 and self.template is not None:
# The browsable API should display pagination controls.
self.display_page_controls = True
@@ -347,7 +347,7 @@ class LimitOffsetPagination(BasePagination):
self.offset = self.get_offset(request)
self.count = _get_count(queryset)
self.request = request
- if self.count > self.limit:
+ if self.count > self.limit and self.template is not None:
self.display_page_controls = True
return queryset[self.offset:self.offset + self.limit]
@@ -518,7 +518,7 @@ class CursorPagination(BasePagination):
# Display page controls in the browsable API if there is more
# than one page.
- if self.has_previous or self.has_next:
+ if (self.has_previous or self.has_next) and self.template is not None:
self.display_page_controls = True
return self.page
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 6256acdd..920d2bc4 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -305,7 +305,7 @@ class HTMLFormRenderer(BaseRenderer):
})
def render_field(self, field, parent_style):
- if isinstance(field, serializers.HiddenField):
+ if isinstance(field._field, serializers.HiddenField):
return ''
style = dict(self.default_style[field])
@@ -458,8 +458,8 @@ class BrowsableAPIRenderer(BaseRenderer):
return True # Don't actually need to return a form
if (
- not getattr(view, 'get_serializer', None)
- or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
+ not getattr(view, 'get_serializer', None) or
+ not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
):
return
@@ -503,8 +503,8 @@ class BrowsableAPIRenderer(BaseRenderer):
# If we're not using content overloading there's no point in
# supplying a generic form, as the view won't treat the form's
# value as the content of the request.
- if not (api_settings.FORM_CONTENT_OVERRIDE
- and api_settings.FORM_CONTENTTYPE_OVERRIDE):
+ if not (api_settings.FORM_CONTENT_OVERRIDE and
+ api_settings.FORM_CONTENTTYPE_OVERRIDE):
return None
# Check permissions
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 081ace23..fd4f6a3e 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -383,9 +383,9 @@ class Request(object):
# We only need to use form overloading on form POST requests.
if (
- not USE_FORM_OVERLOADING
- or self._request.method != 'POST'
- or not is_form_media_type(self._content_type)
+ self._request.method != 'POST' or
+ not USE_FORM_OVERLOADING or
+ not is_form_media_type(self._content_type)
):
return
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 6a4184e2..b1e39ff7 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -165,34 +165,30 @@ class SimpleRouter(BaseRouter):
else:
list_routes.append((httpmethods, methodname))
+ def _get_dynamic_routes(route, dynamic_routes):
+ ret = []
+ for httpmethods, methodname in dynamic_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ initkwargs = route.initkwargs.copy()
+ initkwargs.update(method_kwargs)
+ url_path = initkwargs.pop("url_path", None) or methodname
+ ret.append(Route(
+ url=replace_methodname(route.url, url_path),
+ mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
+ name=replace_methodname(route.name, url_path),
+ initkwargs=initkwargs,
+ ))
+
+ return ret
+
ret = []
for route in self.routes:
if isinstance(route, DynamicDetailRoute):
# Dynamic detail routes (@detail_route decorator)
- for httpmethods, methodname in detail_routes:
- method_kwargs = getattr(viewset, methodname).kwargs
- url_path = method_kwargs.pop("url_path", None) or methodname
- initkwargs = route.initkwargs.copy()
- initkwargs.update(method_kwargs)
- ret.append(Route(
- url=replace_methodname(route.url, url_path),
- mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, url_path),
- initkwargs=initkwargs,
- ))
+ ret += _get_dynamic_routes(route, detail_routes)
elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator)
- for httpmethods, methodname in list_routes:
- method_kwargs = getattr(viewset, methodname).kwargs
- url_path = method_kwargs.pop("url_path", None) or methodname
- initkwargs = route.initkwargs.copy()
- initkwargs.update(method_kwargs)
- ret.append(Route(
- url=replace_methodname(route.url, url_path),
- mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, url_path),
- initkwargs=initkwargs,
- ))
+ ret += _get_dynamic_routes(route, list_routes)
else:
# Standard route
ret.append(route)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index c60574d4..2eef6eeb 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -13,6 +13,7 @@ response content is handled by parsers and renderers.
from __future__ import unicode_literals
from django.db import models
from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField
+from django.db.models import query
from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import postgres_fields, unicode_to_repr
from rest_framework.utils import model_meta
@@ -336,8 +337,8 @@ class Serializer(BaseSerializer):
return OrderedDict([
(field_name, field.get_value(self.initial_data))
for field_name, field in self.fields.items()
- if field.get_value(self.initial_data) is not empty
- and not field.read_only
+ if (field.get_value(self.initial_data) is not empty) and
+ not field.read_only
])
return OrderedDict([
@@ -562,7 +563,7 @@ class ListSerializer(BaseSerializer):
"""
# Dealing with nested relationships, data can be a Manager,
# so, first get a queryset from the Manager if needed
- iterable = data.all() if isinstance(data, models.Manager) else data
+ iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data
return [
self.child.to_representation(item) for item in iterable
]
@@ -653,8 +654,9 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data):
# ...
# profile = ProfileSerializer()
assert not any(
- isinstance(field, BaseSerializer) and (key in validated_data)
- and isinstance(validated_data[key], (list, dict))
+ isinstance(field, BaseSerializer) and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
for key, field in serializer.fields.items()
), (
'The `.{method_name}()` method does not support writable nested'
@@ -673,8 +675,9 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data):
# ...
# address = serializer.CharField('profile.address')
assert not any(
- '.' in field.source and (key in validated_data)
- and isinstance(validated_data[key], (list, dict))
+ '.' in field.source and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
for key, field in serializer.fields.items()
), (
'The `.{method_name}()` method does not support writable dotted-source '
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 7331f265..394b1262 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -20,9 +20,9 @@ back to the defaults.
from __future__ import unicode_literals
from django.test.signals import setting_changed
from django.conf import settings
-from django.utils import importlib, six
+from django.utils import six
from rest_framework import ISO_8601
-
+from rest_framework.compat import importlib
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None)
diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
index 01c251fb..0735f280 100644
--- a/rest_framework/templates/rest_framework/horizontal/select_multiple.html
+++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
@@ -1,11 +1,16 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
<div class="form-group">
{% if field.label %}
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
{% endif %}
<div class="col-sm-10">
- <select multiple class="form-control" name="{{ field.name }}">
+ <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %}
- <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ {% empty %}
+ <option>{{ no_items }}</option>
{% endfor %}
</select>
{% if field.errors %}
diff --git a/rest_framework/templates/rest_framework/inline/select_multiple.html b/rest_framework/templates/rest_framework/inline/select_multiple.html
index feddf7ab..5a8b2494 100644
--- a/rest_framework/templates/rest_framework/inline/select_multiple.html
+++ b/rest_framework/templates/rest_framework/inline/select_multiple.html
@@ -1,10 +1,15 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
<div class="form-group {% if field.errors %}has-error{% endif %}">
{% if field.label %}
<label class="sr-only">{{ field.label }}</label>
{% endif %}
- <select multiple class="form-control" name="{{ field.name }}">
+ <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %}
- <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ {% empty %}
+ <option>{{ no_items }}</option>
{% endfor %}
</select>
</div>
diff --git a/rest_framework/templates/rest_framework/vertical/select_multiple.html b/rest_framework/templates/rest_framework/vertical/select_multiple.html
index 54839294..81b25c2a 100644
--- a/rest_framework/templates/rest_framework/vertical/select_multiple.html
+++ b/rest_framework/templates/rest_framework/vertical/select_multiple.html
@@ -1,10 +1,15 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
<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>
{% endif %}
- <select multiple class="form-control" name="{{ field.name }}">
+ <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
{% for key, text in field.choices.items %}
- <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ {% empty %}
+ <option>{{ no_items }}</option>
{% endfor %}
</select>
{% if field.errors %}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 699ea897..bf0dc7b8 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -162,8 +162,8 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
lead = lead + opening
# Keep parentheses at the end only if they're balanced.
if (
- middle.endswith(closing)
- and middle.count(closing) == middle.count(opening) + 1
+ middle.endswith(closing) and
+ middle.count(closing) == middle.count(opening) + 1
):
middle = middle[:-len(closing)]
trail = closing + trail
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 4f4b7c20..a83d082a 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -209,7 +209,8 @@ class APIClient(APIRequestFactory, DjangoClient):
self.handler._force_user = None
self.handler._force_token = None
- return super(APIClient, self).logout()
+ if self.session:
+ super(APIClient, self).logout()
class APITransactionTestCase(testcases.TransactionTestCase):
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index dd92f8b6..d92bceb9 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -145,8 +145,8 @@ def _get_reverse_relationships(opts):
related_model=related,
to_many=True,
has_through_model=(
- (getattr(relation.field.rel, 'through', None) is not None)
- and not relation.field.rel.through._meta.auto_created
+ (getattr(relation.field.rel, 'through', None) is not None) and
+ not relation.field.rel.through._meta.auto_created
)
)
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index e3719b8d..ab361614 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -138,7 +138,12 @@ class UniqueTogetherValidator:
queryset = self.queryset
queryset = self.filter_queryset(attrs, queryset)
queryset = self.exclude_current_instance(attrs, queryset)
- if queryset.exists():
+
+ # Ignore validation if any field is None
+ checked_values = [
+ value for field, value in attrs.items() if field in self.fields
+ ]
+ if None not in checked_values and queryset.exists():
field_names = ', '.join(self.fields)
raise ValidationError(self.message.format(field_names=field_names))