diff options
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/__init__.py | 2 | ||||
| -rw-r--r-- | rest_framework/compat.py | 5 | ||||
| -rw-r--r-- | rest_framework/fields.py | 65 | ||||
| -rw-r--r-- | rest_framework/metadata.py | 8 | ||||
| -rw-r--r-- | rest_framework/pagination.py | 6 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 10 | ||||
| -rw-r--r-- | rest_framework/request.py | 6 | ||||
| -rw-r--r-- | rest_framework/routers.py | 40 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 17 | ||||
| -rw-r--r-- | rest_framework/settings.py | 4 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/horizontal/select_multiple.html | 9 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/inline/select_multiple.html | 9 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/vertical/select_multiple.html | 9 | ||||
| -rw-r--r-- | rest_framework/templatetags/rest_framework.py | 4 | ||||
| -rw-r--r-- | rest_framework/test.py | 3 | ||||
| -rw-r--r-- | rest_framework/utils/model_meta.py | 4 | ||||
| -rw-r--r-- | rest_framework/validators.py | 7 |
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)) |
