From af46fd6b00f1d7f018049c19094af58acb1415fb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 12:25:57 +0100 Subject: Field tests and associated cleanup --- rest_framework/fields.py | 64 +++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 33 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0c78b3fb..db75ddf9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -12,7 +12,6 @@ from rest_framework.utils import html, representation, humanize_datetime import datetime import decimal import inspect -import warnings class empty: @@ -395,7 +394,7 @@ class IntegerField(Field): class FloatField(Field): default_error_messages = { - 'invalid': _("'%s' value must be a float."), + 'invalid': _("A valid number is required."), } def __init__(self, **kwargs): @@ -410,20 +409,20 @@ class FloatField(Field): def to_internal_value(self, value): if value is None: return None - return float(value) + try: + return float(value) + except (TypeError, ValueError): + self.fail('invalid') def to_representation(self, value): if value is None: return None - try: - return float(value) - except (TypeError, ValueError): - self.fail('invalid', value=value) + return float(value) class DecimalField(Field): default_error_messages = { - 'invalid': _('Enter a number.'), + 'invalid': _('A valid number is required.'), 'max_value': _('Ensure this value is less than or equal to {max_value}.'), 'min_value': _('Ensure this value is greater than or equal to {min_value}.'), 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'), @@ -485,7 +484,7 @@ class DecimalField(Field): if self.decimal_places is not None and decimals > self.decimal_places: self.fail('max_decimal_places', max_decimal_places=self.decimal_places) if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places): - self.fail('max_whole_digits', max_while_digits=self.max_digits - self.decimal_places) + self.fail('max_whole_digits', max_whole_digits=self.max_digits - self.decimal_places) return value @@ -511,6 +510,7 @@ class DecimalField(Field): class DateField(Field): default_error_messages = { 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'), + 'datetime': _('Expected a date but got a datetime.'), } format = api_settings.DATE_FORMAT input_formats = api_settings.DATE_INPUT_FORMATS @@ -525,12 +525,7 @@ class DateField(Field): return None if isinstance(value, datetime.datetime): - if timezone and settings.USE_TZ and timezone.is_aware(value): - # Convert aware datetimes to the default time zone - # before casting them to dates (#17742). - default_timezone = timezone.get_default_timezone() - value = timezone.make_naive(value, default_timezone) - return value.date() + self.fail('datetime') if isinstance(value, datetime.date): return value @@ -570,35 +565,38 @@ class DateField(Field): class DateTimeField(Field): default_error_messages = { 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'), + 'date': _('Expected a datetime but got a date.'), } format = api_settings.DATETIME_FORMAT input_formats = api_settings.DATETIME_INPUT_FORMATS + default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None - def __init__(self, format=None, input_formats=None, *args, **kwargs): + def __init__(self, format=None, input_formats=None, default_timezone=None, *args, **kwargs): self.format = format if format is not None else self.format self.input_formats = input_formats if input_formats is not None else self.input_formats + self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone super(DateTimeField, self).__init__(*args, **kwargs) + def enforce_timezone(self, value): + """ + When `self.default_timezone` is `None`, always return naive datetimes. + When `self.default_timezone` is not `None`, always return aware datetimes. + """ + if (self.default_timezone is not None) and not timezone.is_aware(value): + return timezone.make_aware(value, self.default_timezone) + elif (self.default_timezone is None) and timezone.is_aware(value): + return timezone.make_naive(value, timezone.UTC()) + return value + def to_internal_value(self, value): if value in (None, ''): return None - if isinstance(value, datetime.datetime): - return value + if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): + self.fail('date') - if isinstance(value, datetime.date): - value = datetime.datetime(value.year, value.month, value.day) - if settings.USE_TZ: - # For backwards compatibility, interpret naive datetimes in - # local time. This won't work during DST change, but we can't - # do much about it, so we let the exceptions percolate up the - # call stack. - warnings.warn("DateTimeField received a naive datetime (%s)" - " while time zone support is active." % value, - RuntimeWarning) - default_timezone = timezone.get_default_timezone() - value = timezone.make_aware(value, default_timezone) - return value + if isinstance(value, datetime.datetime): + return self.enforce_timezone(value) for format in self.input_formats: if format.lower() == ISO_8601: @@ -608,14 +606,14 @@ class DateTimeField(Field): pass else: if parsed is not None: - return parsed + return self.enforce_timezone(parsed) else: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): pass else: - return parsed + return self.enforce_timezone(parsed) humanized_format = humanize_datetime.datetime_formats(self.input_formats) self.fail('invalid', format=humanized_format) -- cgit v1.2.3 From afb3f8ab0ad6c33b147292e9777ba0ddf3871d14 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 13:26:47 +0100 Subject: Tests and tweaks for text fields --- rest_framework/fields.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index db75ddf9..35bd5c4b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -12,6 +12,7 @@ from rest_framework.utils import html, representation, humanize_datetime import datetime import decimal import inspect +import re class empty: @@ -325,7 +326,11 @@ class EmailField(CharField): default_error_messages = { 'invalid': _('Enter a valid email address.') } - default_validators = [validators.validate_email] + + def __init__(self, **kwargs): + super(EmailField, self).__init__(**kwargs) + validator = validators.EmailValidator(message=self.error_messages['invalid']) + self.validators = [validator] + self.validators def to_internal_value(self, data): if data == '' and not self.allow_blank: @@ -341,26 +346,37 @@ class EmailField(CharField): class RegexField(CharField): + default_error_messages = { + 'invalid': _('This value does not match the required pattern.') + } + def __init__(self, regex, **kwargs): - kwargs['validators'] = ( - [validators.RegexValidator(regex)] + - kwargs.get('validators', []) - ) super(RegexField, self).__init__(**kwargs) + validator = validators.RegexValidator(regex, message=self.error_messages['invalid']) + self.validators = [validator] + self.validators class SlugField(CharField): default_error_messages = { 'invalid': _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.") } - default_validators = [validators.validate_slug] + + def __init__(self, **kwargs): + super(SlugField, self).__init__(**kwargs) + slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$') + validator = validators.RegexValidator(slug_regex, message=self.error_messages['invalid']) + self.validators = [validator] + self.validators class URLField(CharField): default_error_messages = { 'invalid': _("Enter a valid URL.") } - default_validators = [validators.URLValidator()] + + def __init__(self, **kwargs): + super(URLField, self).__init__(**kwargs) + validator = validators.URLValidator(message=self.error_messages['invalid']) + self.validators = [validator] + self.validators # Number types... @@ -642,7 +658,7 @@ class TimeField(Field): self.input_formats = input_formats if input_formats is not None else self.input_formats super(TimeField, self).__init__(*args, **kwargs) - def from_native(self, value): + def to_internal_value(self, value): if value in (None, ''): return None -- cgit v1.2.3 From c54f394904c3f93211b8aa073de4e9e50110f831 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 13:57:45 +0100 Subject: Ensure 'messages' in fields are respected in preference to default validator messages --- rest_framework/fields.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 35bd5c4b..5105dfcb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -6,7 +6,7 @@ from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 -from rest_framework.compat import smart_text +from rest_framework.compat import smart_text, MinValueValidator, MaxValueValidator from rest_framework.settings import api_settings from rest_framework.utils import html, representation, humanize_datetime import datetime @@ -330,7 +330,7 @@ class EmailField(CharField): def __init__(self, **kwargs): super(EmailField, self).__init__(**kwargs) validator = validators.EmailValidator(message=self.error_messages['invalid']) - self.validators = [validator] + self.validators + self.validators.append(validator) def to_internal_value(self, data): if data == '' and not self.allow_blank: @@ -353,7 +353,7 @@ class RegexField(CharField): def __init__(self, regex, **kwargs): super(RegexField, self).__init__(**kwargs) validator = validators.RegexValidator(regex, message=self.error_messages['invalid']) - self.validators = [validator] + self.validators + self.validators.append(validator) class SlugField(CharField): @@ -365,7 +365,7 @@ class SlugField(CharField): super(SlugField, self).__init__(**kwargs) slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$') validator = validators.RegexValidator(slug_regex, message=self.error_messages['invalid']) - self.validators = [validator] + self.validators + self.validators.append(validator) class URLField(CharField): @@ -376,14 +376,16 @@ class URLField(CharField): def __init__(self, **kwargs): super(URLField, self).__init__(**kwargs) validator = validators.URLValidator(message=self.error_messages['invalid']) - self.validators = [validator] + self.validators + self.validators.append(validator) # Number types... class IntegerField(Field): default_error_messages = { - 'invalid': _('A valid integer is required.') + 'invalid': _('A valid integer is required.'), + 'max_value': _('Ensure this value is less than or equal to {max_value}.'), + 'min_value': _('Ensure this value is greater than or equal to {min_value}.'), } def __init__(self, **kwargs): @@ -391,9 +393,11 @@ class IntegerField(Field): min_value = kwargs.pop('min_value', None) super(IntegerField, self).__init__(**kwargs) if max_value is not None: - self.validators.append(validators.MaxValueValidator(max_value)) + 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: - self.validators.append(validators.MinValueValidator(min_value)) + message = self.error_messages['min_value'].format(min_value=min_value) + self.validators.append(MinValueValidator(min_value, message=message)) def to_internal_value(self, data): try: @@ -411,6 +415,8 @@ class IntegerField(Field): class FloatField(Field): default_error_messages = { 'invalid': _("A valid number is required."), + 'max_value': _('Ensure this value is less than or equal to {max_value}.'), + 'min_value': _('Ensure this value is greater than or equal to {min_value}.'), } def __init__(self, **kwargs): @@ -418,9 +424,11 @@ class FloatField(Field): min_value = kwargs.pop('min_value', None) super(FloatField, self).__init__(**kwargs) if max_value is not None: - self.validators.append(validators.MaxValueValidator(max_value)) + 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: - self.validators.append(validators.MinValueValidator(min_value)) + message = self.error_messages['min_value'].format(min_value=min_value) + self.validators.append(MinValueValidator(min_value, message=message)) def to_internal_value(self, value): if value is None: @@ -454,9 +462,11 @@ class DecimalField(Field): self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string super(DecimalField, self).__init__(**kwargs) if max_value is not None: - self.validators.append(validators.MaxValueValidator(max_value)) + 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: - self.validators.append(validators.MinValueValidator(min_value)) + message = self.error_messages['min_value'].format(min_value=min_value) + self.validators.append(MinValueValidator(min_value, message=message)) def to_internal_value(self, value): """ -- cgit v1.2.3 From 249253a144ba4381581809fb3f27959c7bd6e577 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 14:54:33 +0100 Subject: Fix compat issues --- rest_framework/fields.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5105dfcb..5fb99a42 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -209,8 +209,10 @@ class Field(object): """ Validate a simple representation and return the internal value. - The provided data may be `empty` if no representation was included. - May return `empty` if the field should not be included in the + The provided data may be `empty` if no representation was included + in the input. + + May raise `SkipField` if the field should not be included in the validated data. """ if data is empty: @@ -223,6 +225,10 @@ class Field(object): return value def run_validators(self, value): + """ + Test the given value against all the validators on the field, + and either raise a `ValidationError` or simply return. + """ if value in (None, '', [], (), {}): return @@ -753,8 +759,9 @@ class MultipleChoiceField(ChoiceField): } def to_internal_value(self, data): - if not hasattr(data, '__iter__'): + if isinstance(data, type('')) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) + return set([ super(MultipleChoiceField, self).to_internal_value(item) for item in data -- cgit v1.2.3 From 4db23cae213decc3e8a8613ad5c76a545f8cfb1a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 15:34:06 +0100 Subject: Tweaks to DecimalField --- rest_framework/fields.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5fb99a42..db7ceabb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -521,20 +521,21 @@ class DecimalField(Field): return value def to_representation(self, value): - if isinstance(value, decimal.Decimal): - context = decimal.getcontext().copy() - context.prec = self.max_digits - quantized = value.quantize( - decimal.Decimal('.1') ** self.decimal_places, - context=context - ) - if not self.coerce_to_string: - return quantized - return '{0:f}'.format(quantized) + if value in (None, ''): + return None + if not isinstance(value, decimal.Decimal): + value = decimal.Decimal(value) + + context = decimal.getcontext().copy() + context.prec = self.max_digits + quantized = value.quantize( + decimal.Decimal('.1') ** self.decimal_places, + context=context + ) if not self.coerce_to_string: - return value - return '%.*f' % (self.max_decimal_places, value) + return quantized + return '{0:f}'.format(quantized) # Date & time fields... -- cgit v1.2.3 From 5586b6581d9d8db05276c08f2c6deffec04ade4f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 16:02:59 +0100 Subject: Support format=None for date/time fields --- rest_framework/fields.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index db7ceabb..cbd3334a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -548,8 +548,8 @@ class DateField(Field): format = api_settings.DATE_FORMAT input_formats = api_settings.DATE_INPUT_FORMATS - def __init__(self, format=None, input_formats=None, *args, **kwargs): - self.format = format if format is not None else self.format + def __init__(self, format=empty, input_formats=None, *args, **kwargs): + self.format = format if format is not empty else self.format self.input_formats = input_formats if input_formats is not None else self.input_formats super(DateField, self).__init__(*args, **kwargs) @@ -604,8 +604,8 @@ class DateTimeField(Field): input_formats = api_settings.DATETIME_INPUT_FORMATS default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None - def __init__(self, format=None, input_formats=None, default_timezone=None, *args, **kwargs): - self.format = format if format is not None else self.format + def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): + self.format = format if format is not empty else self.format self.input_formats = input_formats if input_formats is not None else self.input_formats self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone super(DateTimeField, self).__init__(*args, **kwargs) @@ -670,8 +670,8 @@ class TimeField(Field): format = api_settings.TIME_FORMAT input_formats = api_settings.TIME_INPUT_FORMATS - def __init__(self, format=None, input_formats=None, *args, **kwargs): - self.format = format if format is not None else self.format + def __init__(self, format=empty, input_formats=None, *args, **kwargs): + self.format = format if format is not empty else self.format self.input_formats = input_formats if input_formats is not None else self.input_formats super(TimeField, self).__init__(*args, **kwargs) -- cgit v1.2.3 From e5f0a97595ff9280c7876fc917f6feb27b5ea95d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 16:45:06 +0100 Subject: More compat fixes --- rest_framework/fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cbd3334a..12975ae4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -6,7 +6,7 @@ from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 -from rest_framework.compat import smart_text, MinValueValidator, MaxValueValidator +from rest_framework.compat import smart_text, EmailValidator, MinValueValidator, MaxValueValidator, URLValidator from rest_framework.settings import api_settings from rest_framework.utils import html, representation, humanize_datetime import datetime @@ -335,7 +335,7 @@ class EmailField(CharField): def __init__(self, **kwargs): super(EmailField, self).__init__(**kwargs) - validator = validators.EmailValidator(message=self.error_messages['invalid']) + validator = EmailValidator(message=self.error_messages['invalid']) self.validators.append(validator) def to_internal_value(self, data): @@ -381,7 +381,7 @@ class URLField(CharField): def __init__(self, **kwargs): super(URLField, self).__init__(**kwargs) - validator = validators.URLValidator(message=self.error_messages['invalid']) + validator = URLValidator(message=self.error_messages['invalid']) self.validators.append(validator) @@ -525,7 +525,7 @@ class DecimalField(Field): return None if not isinstance(value, decimal.Decimal): - value = decimal.Decimal(value) + value = decimal.Decimal(str(value).strip()) context = decimal.getcontext().copy() context.prec = self.max_digits -- cgit v1.2.3 From b5454dd02290130a7fb0a0e375f3efecc58edc6d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 16:50:04 +0100 Subject: Tests and tweaks for choice fields --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 12975ae4..500018f3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -750,7 +750,7 @@ class ChoiceField(Field): self.fail('invalid_choice', input=data) def to_representation(self, value): - return value + return self.choice_strings_to_values[str(value)] class MultipleChoiceField(ChoiceField): @@ -769,7 +769,7 @@ class MultipleChoiceField(ChoiceField): ]) def to_representation(self, value): - return value + return [self.choice_strings_to_values[str(item)] for item in value] # File types... -- cgit v1.2.3 From 5a95baf2a2258fb5297062ac18582129c05fb320 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 16:52:57 +0100 Subject: Tests & tweaks for ChoiceField --- rest_framework/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 500018f3..80eadf1e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -769,7 +769,9 @@ class MultipleChoiceField(ChoiceField): ]) def to_representation(self, value): - return [self.choice_strings_to_values[str(item)] for item in value] + return set([ + self.choice_strings_to_values[str(item)] for item in value + ]) # File types... -- cgit v1.2.3 From 5d80f7f932bfcc0630ac0fdbf07072a53197b98f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 22 Sep 2014 17:46:02 +0100 Subject: allow_blank, allow_null --- rest_framework/fields.py | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 80eadf1e..48a3e1ab 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -98,14 +98,15 @@ class Field(object): _creation_counter = 0 default_error_messages = { - 'required': _('This field is required.') + 'required': _('This field is required.'), + 'null': _('This field may not be null.') } default_validators = [] def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=None, source=None, label=None, help_text=None, style=None, - error_messages=None, validators=[]): + error_messages=None, validators=[], allow_null=False): self._creation_counter = Field._creation_counter Field._creation_counter += 1 @@ -129,6 +130,7 @@ class Field(object): self.help_text = help_text self.style = {} if style is None else style self.validators = validators or self.default_validators[:] + self.allow_null = allow_null # Collect default error message from self and parent classes messages = {} @@ -220,6 +222,11 @@ class Field(object): self.fail('required') return self.get_default() + if data is None: + if not self.allow_null: + self.fail('null') + return None + value = self.to_internal_value(data) self.run_validators(value) return value @@ -315,11 +322,14 @@ class CharField(Field): self.min_length = kwargs.pop('min_length', None) super(CharField, self).__init__(**kwargs) + def run_validation(self, data=empty): + if data == '': + if not self.allow_blank: + self.fail('blank') + return '' + return super(CharField, self).run_validation(data) + def to_internal_value(self, data): - if data == '' and not self.allow_blank: - self.fail('blank') - if data is None: - return None return str(data) def to_representation(self, value): @@ -339,10 +349,6 @@ class EmailField(CharField): self.validators.append(validator) def to_internal_value(self, data): - if data == '' and not self.allow_blank: - self.fail('blank') - if data is None: - return None return str(data).strip() def to_representation(self, value): @@ -437,8 +443,6 @@ class FloatField(Field): self.validators.append(MinValueValidator(min_value, message=message)) def to_internal_value(self, value): - if value is None: - return None try: return float(value) except (TypeError, ValueError): @@ -481,9 +485,6 @@ class DecimalField(Field): than max_digits in the number, and no more than decimal_places digits after the decimal point. """ - if value in (None, ''): - return None - value = smart_text(value).strip() try: value = decimal.Decimal(value) @@ -554,9 +555,6 @@ class DateField(Field): super(DateField, self).__init__(*args, **kwargs) def to_internal_value(self, value): - if value in (None, ''): - return None - if isinstance(value, datetime.datetime): self.fail('datetime') @@ -622,9 +620,6 @@ class DateTimeField(Field): return value def to_internal_value(self, value): - if value in (None, ''): - return None - if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): self.fail('date') @@ -676,9 +671,6 @@ class TimeField(Field): super(TimeField, self).__init__(*args, **kwargs) def to_internal_value(self, value): - if value in (None, ''): - return None - if isinstance(value, datetime.time): return value -- cgit v1.2.3 From f22d0afc3dfc7478e084d1d6ed6b53f71641dec6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 23 Sep 2014 14:15:00 +0100 Subject: Tests for field choices --- rest_framework/fields.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 48a3e1ab..f5bae734 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -102,6 +102,7 @@ class Field(object): 'null': _('This field may not be null.') } default_validators = [] + default_empty_html = None def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=None, source=None, @@ -185,6 +186,11 @@ class Field(object): Given the *incoming* primative data, return the value for this field that should be validated and transformed to a native value. """ + if html.is_html_input(dictionary): + # HTML forms will represent empty fields as '', and cannot + # represent None or False values directly. + ret = dictionary.get(self.field_name, '') + return self.default_empty_html if (ret == '') else ret return dictionary.get(self.field_name, empty) def get_attribute(self, instance): @@ -236,9 +242,6 @@ class Field(object): Test the given value against all the validators on the field, and either raise a `ValidationError` or simply return. """ - if value in (None, '', [], (), {}): - return - errors = [] for validator in self.validators: try: @@ -282,16 +285,10 @@ class BooleanField(Field): default_error_messages = { 'invalid': _('`{input}` is not a valid boolean.') } + default_empty_html = False TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) - def get_value(self, dictionary): - if html.is_html_input(dictionary): - # HTML forms do not send a `False` value on an empty checkbox, - # so we override the default empty value to be False. - return dictionary.get(self.field_name, False) - return dictionary.get(self.field_name, empty) - def to_internal_value(self, data): if data in self.TRUE_VALUES: return True @@ -315,6 +312,7 @@ class CharField(Field): default_error_messages = { 'blank': _('This field may not be blank.') } + default_empty_html = '' def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) @@ -323,6 +321,9 @@ class CharField(Field): super(CharField, self).__init__(**kwargs) def run_validation(self, data=empty): + # Test for the empty string here so that it does not get validated, + # and so that subclasses do not need to handle it explicitly + # inside the `to_internal_value()` method. if data == '': if not self.allow_blank: self.fail('blank') -- cgit v1.2.3 From 0404f09a7e69f533038d47ca25caad90c0c2659f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 23 Sep 2014 14:30:17 +0100 Subject: NullBooleanField --- rest_framework/fields.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f5bae734..f859658a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -289,6 +289,10 @@ class BooleanField(Field): TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) + def __init__(self, **kwargs): + assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.' + super(BooleanField, self).__init__(**kwargs) + def to_internal_value(self, data): if data in self.TRUE_VALUES: return True @@ -297,7 +301,38 @@ class BooleanField(Field): self.fail('invalid', input=data) def to_representation(self, value): - if value is None: + if value in self.TRUE_VALUES: + return True + elif value in self.FALSE_VALUES: + return False + return bool(value) + + +class NullBooleanField(Field): + default_error_messages = { + 'invalid': _('`{input}` is not a valid boolean.') + } + default_empty_html = None + TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) + FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) + NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None)) + + def __init__(self, **kwargs): + assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.' + kwargs['allow_null'] = True + super(NullBooleanField, self).__init__(**kwargs) + + def to_internal_value(self, data): + if data in self.TRUE_VALUES: + return True + elif data in self.FALSE_VALUES: + return False + elif data in self.NULL_VALUES: + return None + self.fail('invalid', input=data) + + def to_representation(self, value): + if value in self.NULL_VALUES: return None if value in self.TRUE_VALUES: return True -- cgit v1.2.3 From 127c0bd3d68860dd6567d81047257fbc3e70b4b9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Sep 2014 20:25:59 +0100 Subject: Custom deepcopy on Field classes --- rest_framework/fields.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f859658a..1f7d964a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -9,6 +9,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import smart_text, EmailValidator, MinValueValidator, MaxValueValidator, URLValidator from rest_framework.settings import api_settings from rest_framework.utils import html, representation, humanize_datetime +import copy import datetime import decimal import inspect @@ -150,6 +151,11 @@ class Field(object): instance._kwargs = kwargs return instance + def __deepcopy__(self, memo): + args = copy.deepcopy(self._args) + kwargs = copy.deepcopy(self._kwargs) + return self.__class__(*args, **kwargs) + def bind(self, field_name, parent, root): """ Setup the context for the field instance. -- cgit v1.2.3 From fb1546ee50953faae8af505a0c90da00ac08ad92 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Sep 2014 20:53:37 +0100 Subject: Enforce field_name != source --- rest_framework/fields.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1f7d964a..9280ea3a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -160,6 +160,17 @@ class Field(object): """ Setup the context for the field instance. """ + + # In order to enforce a consistent style, we error if a redundant + # 'source' argument has been used. For example: + # my_field = serializer.CharField(source='my_field') + assert self._kwargs.get('source') != field_name, ( + "It is redundant to specify `source='%s'` on field '%s' in " + "serializer '%s', as it is the same the field name. " + "Remove the `source` keyword argument." % + (field_name, self.__class__.__name__, parent.__class__.__name__) + ) + self.field_name = field_name self.parent = parent self.root = root -- cgit v1.2.3 From 1420c76453c37c023a901dd0938d717b7b5e52ca Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 10:49:25 +0100 Subject: Ensure proper sorting of 'choices' attribute on ChoiceField --- rest_framework/fields.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9280ea3a..d1aebbaf 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -2,6 +2,7 @@ from django.conf import settings from django.core import validators from django.core.exceptions import ValidationError from django.utils import timezone +from django.utils.datastructures import SortedDict from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ @@ -166,7 +167,7 @@ class Field(object): # my_field = serializer.CharField(source='my_field') assert self._kwargs.get('source') != field_name, ( "It is redundant to specify `source='%s'` on field '%s' in " - "serializer '%s', as it is the same the field name. " + "serializer '%s', because it is the same as the field name. " "Remove the `source` keyword argument." % (field_name, self.__class__.__name__, parent.__class__.__name__) ) @@ -303,6 +304,7 @@ class BooleanField(Field): 'invalid': _('`{input}` is not a valid boolean.') } default_empty_html = False + initial = False TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) @@ -365,6 +367,7 @@ class CharField(Field): 'blank': _('This field may not be blank.') } default_empty_html = '' + initial = '' def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) @@ -775,9 +778,9 @@ class ChoiceField(Field): for item in choices ] if all(pairs): - self.choices = dict([(key, display_value) for key, display_value in choices]) + self.choices = SortedDict([(key, display_value) for key, display_value in choices]) else: - self.choices = dict([(item, item) for item in choices]) + self.choices = SortedDict([(item, item) for item in choices]) # Map the string representation of choices to the underlying value. # Allows us to deal with eg. integer choices while supporting either -- cgit v1.2.3 From 64632da3718f501cb8174243385d38b547c2fefd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 11:40:32 +0100 Subject: Clean up bind - no longer needs to be called multiple times in nested fields --- rest_framework/fields.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d1aebbaf..446732c3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -109,7 +109,8 @@ class Field(object): def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=None, source=None, label=None, help_text=None, style=None, - error_messages=None, validators=[], allow_null=False): + error_messages=None, validators=[], allow_null=False, + context=None): self._creation_counter = Field._creation_counter Field._creation_counter += 1 @@ -135,6 +136,11 @@ class Field(object): self.validators = validators or self.default_validators[:] self.allow_null = allow_null + # These are set up by `.bind()` when the field is added to a serializer. + self.field_name = None + self.parent = None + self._context = {} if (context is None) else context + # Collect default error message from self and parent classes messages = {} for cls in reversed(self.__class__.__mro__): @@ -157,7 +163,14 @@ class Field(object): kwargs = copy.deepcopy(self._kwargs) return self.__class__(*args, **kwargs) - def bind(self, field_name, parent, root): + @property + def context(self): + root = self + while root.parent is not None: + root = root.parent + return root._context + + def bind(self, field_name, parent): """ Setup the context for the field instance. """ @@ -174,10 +187,8 @@ class Field(object): self.field_name = field_name self.parent = parent - self.root = root - self.context = parent.context - # `self.label` should deafult to being based on the field name. + # `self.label` should default to being based on the field name. if self.label is None: self.label = field_name.replace('_', ' ').capitalize() -- cgit v1.2.3 From b47ca158b9ba9733baad080e648d24b0465ec697 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 12:09:12 +0100 Subject: Check for redundant on SerializerMethodField --- rest_framework/fields.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 446732c3..328e93ef 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -178,7 +178,7 @@ class Field(object): # In order to enforce a consistent style, we error if a redundant # 'source' argument has been used. For example: # my_field = serializer.CharField(source='my_field') - assert self._kwargs.get('source') != field_name, ( + assert self.source != field_name, ( "It is redundant to specify `source='%s'` on field '%s' in " "serializer '%s', because it is the same as the field name. " "Remove the `source` keyword argument." % @@ -883,17 +883,32 @@ class SerializerMethodField(Field): def get_extra_info(self, obj): return ... # Calculate some data to return. """ - def __init__(self, method_attr=None, **kwargs): - self.method_attr = method_attr + def __init__(self, method_name=None, **kwargs): + self.method_name = method_name kwargs['source'] = '*' kwargs['read_only'] = True super(SerializerMethodField, self).__init__(**kwargs) + 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') + 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 " + "serializer '%s', because it is the same as the default method name. " + "Remove the `method_name` argument." % + (self.method_name, field_name, parent.__class__.__name__) + ) + + # The method name should default to `get_{field_name}`. + if self.method_name is None: + self.method_name = default_method_name + + super(SerializerMethodField, self).bind(field_name, parent) + def to_representation(self, value): - method_attr = self.method_attr - if method_attr is None: - method_attr = 'get_{field_name}'.format(field_name=self.field_name) - method = getattr(self.parent, method_attr) + method = getattr(self.parent, self.method_name) return method(value) -- cgit v1.2.3 From 8ee92f8a18c3a31a2a95233f36754203dc60bb18 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 13:10:33 +0100 Subject: Refuse to downcast from datetime to date or time --- rest_framework/fields.py | 118 ++++++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 53 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 328e93ef..d855e6fd 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -608,120 +608,126 @@ class DecimalField(Field): # Date & time fields... -class DateField(Field): +class DateTimeField(Field): default_error_messages = { - 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'), - 'datetime': _('Expected a date but got a datetime.'), + 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'), + 'date': _('Expected a datetime but got a date.'), } - format = api_settings.DATE_FORMAT - input_formats = api_settings.DATE_INPUT_FORMATS + format = api_settings.DATETIME_FORMAT + input_formats = api_settings.DATETIME_INPUT_FORMATS + default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None - def __init__(self, format=empty, input_formats=None, *args, **kwargs): + def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): self.format = format if format is not empty else self.format self.input_formats = input_formats if input_formats is not None else self.input_formats - super(DateField, self).__init__(*args, **kwargs) + self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone + super(DateTimeField, self).__init__(*args, **kwargs) + + def enforce_timezone(self, value): + """ + When `self.default_timezone` is `None`, always return naive datetimes. + When `self.default_timezone` is not `None`, always return aware datetimes. + """ + if (self.default_timezone is not None) and not timezone.is_aware(value): + return timezone.make_aware(value, self.default_timezone) + elif (self.default_timezone is None) and timezone.is_aware(value): + return timezone.make_naive(value, timezone.UTC()) + return value def to_internal_value(self, value): - if isinstance(value, datetime.datetime): - self.fail('datetime') + if (isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): + self.fail('date') - if isinstance(value, datetime.date): - return value + if isinstance(value, datetime.datetime): + return self.enforce_timezone(value) for format in self.input_formats: if format.lower() == ISO_8601: try: - parsed = parse_date(value) + parsed = parse_datetime(value) except (ValueError, TypeError): pass else: if parsed is not None: - return parsed + return self.enforce_timezone(parsed) else: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): pass else: - return parsed.date() + return self.enforce_timezone(parsed) - humanized_format = humanize_datetime.date_formats(self.input_formats) + humanized_format = humanize_datetime.datetime_formats(self.input_formats) self.fail('invalid', format=humanized_format) def to_representation(self, value): if value is None or self.format is None: return value - if isinstance(value, datetime.datetime): - value = value.date() - if self.format.lower() == ISO_8601: - return value.isoformat() + ret = value.isoformat() + if ret.endswith('+00:00'): + ret = ret[:-6] + 'Z' + return ret return value.strftime(self.format) -class DateTimeField(Field): +class DateField(Field): default_error_messages = { - 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'), - 'date': _('Expected a datetime but got a date.'), + 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'), + 'datetime': _('Expected a date but got a datetime.'), } - format = api_settings.DATETIME_FORMAT - input_formats = api_settings.DATETIME_INPUT_FORMATS - default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None + format = api_settings.DATE_FORMAT + input_formats = api_settings.DATE_INPUT_FORMATS - def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): + def __init__(self, format=empty, input_formats=None, *args, **kwargs): self.format = format if format is not empty else self.format self.input_formats = input_formats if input_formats is not None else self.input_formats - self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone - super(DateTimeField, self).__init__(*args, **kwargs) - - def enforce_timezone(self, value): - """ - When `self.default_timezone` is `None`, always return naive datetimes. - When `self.default_timezone` is not `None`, always return aware datetimes. - """ - if (self.default_timezone is not None) and not timezone.is_aware(value): - return timezone.make_aware(value, self.default_timezone) - elif (self.default_timezone is None) and timezone.is_aware(value): - return timezone.make_naive(value, timezone.UTC()) - return value + super(DateField, self).__init__(*args, **kwargs) def to_internal_value(self, value): - if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): - self.fail('date') - if isinstance(value, datetime.datetime): - return self.enforce_timezone(value) + self.fail('datetime') + + if isinstance(value, datetime.date): + return value for format in self.input_formats: if format.lower() == ISO_8601: try: - parsed = parse_datetime(value) + parsed = parse_date(value) except (ValueError, TypeError): pass else: if parsed is not None: - return self.enforce_timezone(parsed) + return parsed else: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): pass else: - return self.enforce_timezone(parsed) + return parsed.date() - humanized_format = humanize_datetime.datetime_formats(self.input_formats) + humanized_format = humanize_datetime.date_formats(self.input_formats) self.fail('invalid', format=humanized_format) def to_representation(self, value): if value is None or self.format is None: return value + # Applying a `DateField` to a datetime value is almost always + # not a sensible thing to do, as it means naively dropping + # any explicit or implicit timezone info. + assert not isinstance(value, datetime.datetime), ( + 'Expected a `date`, but got a `datetime`. Refusing to coerce, ' + 'as this may mean losing timezone information. Use a custom ' + 'read-only field and deal with timezone issues explicitly.' + ) + if self.format.lower() == ISO_8601: - ret = value.isoformat() - if ret.endswith('+00:00'): - ret = ret[:-6] + 'Z' - return ret + return value.isoformat() return value.strftime(self.format) @@ -765,8 +771,14 @@ class TimeField(Field): if value is None or self.format is None: return value - if isinstance(value, datetime.datetime): - value = value.time() + # Applying a `TimeField` to a datetime value is almost always + # not a sensible thing to do, as it means naively dropping + # any explicit or implicit timezone info. + assert not isinstance(value, datetime.datetime), ( + 'Expected a `time`, but got a `datetime`. Refusing to coerce, ' + 'as this may mean losing timezone information. Use a custom ' + 'read-only field and deal with timezone issues explicitly.' + ) if self.format.lower() == ISO_8601: return value.isoformat() -- cgit v1.2.3 From 3a5335f09f58439f8e3c0bddbed8e4c7eeb32482 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 13:12:02 +0100 Subject: Fix syntax error --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d855e6fd..7beccbb7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -635,7 +635,7 @@ class DateTimeField(Field): return value def to_internal_value(self, value): - if (isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): + if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): self.fail('date') if isinstance(value, datetime.datetime): -- cgit v1.2.3 From 417fe1b675bd1d42518fb89a6f81547caef5b735 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Sep 2014 13:37:26 +0100 Subject: Partial support --- rest_framework/fields.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 7beccbb7..032bfd04 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -109,8 +109,7 @@ class Field(object): def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=None, source=None, label=None, help_text=None, style=None, - error_messages=None, validators=[], allow_null=False, - context=None): + error_messages=None, validators=[], allow_null=False): self._creation_counter = Field._creation_counter Field._creation_counter += 1 @@ -139,7 +138,6 @@ class Field(object): # These are set up by `.bind()` when the field is added to a serializer. self.field_name = None self.parent = None - self._context = {} if (context is None) else context # Collect default error message from self and parent classes messages = {} @@ -163,13 +161,6 @@ class Field(object): kwargs = copy.deepcopy(self._kwargs) return self.__class__(*args, **kwargs) - @property - def context(self): - root = self - while root.parent is not None: - root = root.parent - return root._context - def bind(self, field_name, parent): """ Setup the context for the field instance. @@ -254,6 +245,8 @@ class Field(object): """ if data is empty: if self.required: + if getattr(self.root, 'partial', False): + raise SkipField() self.fail('required') return self.get_default() @@ -304,7 +297,29 @@ class Field(object): raise AssertionError(msg) raise ValidationError(msg.format(**kwargs)) + @property + def root(self): + """ + Returns the top-level serializer for this field. + """ + root = self + while root.parent is not None: + root = root.parent + return root + + @property + def context(self): + """ + Returns the context as passed to the root serializer on initialization. + """ + return getattr(self.root, '_context', {}) + def __repr__(self): + """ + Fields are represented using their initial calling arguments. + This allows us to create descriptive representations for serializer + instances that show all the declared fields on the serializer. + """ return representation.field_repr(self) -- cgit v1.2.3 From 2859eaf524bca23f27e666d24a0b63ba61698a76 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 10:46:52 +0100 Subject: request.data attribute --- rest_framework/fields.py | 51 ++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 21 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 032bfd04..ec07a413 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -56,7 +56,7 @@ def get_attribute(instance, attrs): except AttributeError as exc: try: return instance[attr] - except (KeyError, TypeError): + except (KeyError, TypeError, AttributeError): raise exc return instance @@ -90,6 +90,7 @@ NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`' NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`' NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`' NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`' +USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField' MISSING_ERROR_MESSAGE = ( 'ValidationError raised by `{class_name}`, but error key `{key}` does ' 'not exist in the `error_messages` dictionary.' @@ -105,9 +106,10 @@ class Field(object): } default_validators = [] default_empty_html = None + initial = None def __init__(self, read_only=False, write_only=False, - required=None, default=empty, initial=None, source=None, + required=None, default=empty, initial=empty, source=None, label=None, help_text=None, style=None, error_messages=None, validators=[], allow_null=False): self._creation_counter = Field._creation_counter @@ -122,13 +124,14 @@ class Field(object): assert not (read_only and required), NOT_READ_ONLY_REQUIRED assert not (read_only and default is not empty), NOT_READ_ONLY_DEFAULT assert not (required and default is not empty), NOT_REQUIRED_DEFAULT + assert not (read_only and self.__class__ == Field), USE_READONLYFIELD self.read_only = read_only self.write_only = write_only self.required = required self.default = default self.source = source - self.initial = initial + self.initial = self.initial if (initial is empty) else initial self.label = label self.help_text = help_text self.style = {} if style is None else style @@ -146,24 +149,10 @@ class Field(object): messages.update(error_messages or {}) self.error_messages = messages - def __new__(cls, *args, **kwargs): - """ - When a field is instantiated, we store the arguments that were used, - so that we can present a helpful representation of the object. - """ - instance = super(Field, cls).__new__(cls) - instance._args = args - instance._kwargs = kwargs - return instance - - def __deepcopy__(self, memo): - args = copy.deepcopy(self._args) - kwargs = copy.deepcopy(self._kwargs) - return self.__class__(*args, **kwargs) - def bind(self, field_name, parent): """ - Setup the context for the field instance. + Initializes the field name and parent for the field instance. + Called when a field is added to the parent serializer instance. """ # In order to enforce a consistent style, we error if a redundant @@ -244,9 +233,9 @@ class Field(object): validated data. """ if data is empty: + if getattr(self.root, 'partial', False): + raise SkipField() if self.required: - if getattr(self.root, 'partial', False): - raise SkipField() self.fail('required') return self.get_default() @@ -314,6 +303,25 @@ class Field(object): """ return getattr(self.root, '_context', {}) + def __new__(cls, *args, **kwargs): + """ + When a field is instantiated, we store the arguments that were used, + so that we can present a helpful representation of the object. + """ + instance = super(Field, cls).__new__(cls) + instance._args = args + instance._kwargs = kwargs + return instance + + def __deepcopy__(self, memo): + """ + When cloning fields we instantiate using the arguments it was + originally created with, rather than copying the complete state. + """ + args = copy.deepcopy(self._args) + kwargs = copy.deepcopy(self._kwargs) + return self.__class__(*args, **kwargs) + def __repr__(self): """ Fields are represented using their initial calling arguments. @@ -358,6 +366,7 @@ class NullBooleanField(Field): 'invalid': _('`{input}` is not a valid boolean.') } default_empty_html = None + initial = None TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None)) -- cgit v1.2.3 From 2e87de01430d7fec83f00948e60c8d61b317053b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 13:08:20 +0100 Subject: Added ListField --- rest_framework/fields.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index ec07a413..cf42d36c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -881,6 +881,44 @@ class ImageField(Field): # Advanced field types... +class ListField(Field): + child = None + initial = [] + default_error_messages = { + 'not_a_list': _('Expected a list of items but got type `{input_type}`') + } + + def __init__(self, *args, **kwargs): + self.child = kwargs.pop('child', copy.deepcopy(self.child)) + assert self.child is not None, '`child` is a required argument.' + assert not inspect.isclass(self.child), '`child` has not been instantiated.' + super(ListField, self).__init__(*args, **kwargs) + self.child.bind(field_name='', parent=self) + + def get_value(self, dictionary): + # We override the default field access in order to support + # lists in HTML forms. + if html.is_html_input(dictionary): + return html.parse_html_list(dictionary, prefix=self.field_name) + return dictionary.get(self.field_name, empty) + + def to_internal_value(self, data): + """ + List of dicts of native values <- List of dicts of primitive datatypes. + """ + if html.is_html_input(data): + data = html.parse_html_list(data) + if isinstance(data, type('')) or not hasattr(data, '__iter__'): + self.fail('not_a_list', input_type=type(data).__name__) + return [self.child.run_validation(item) for item in data] + + def to_representation(self, data): + """ + List of object instances -> List of dicts of primitive datatypes. + """ + return [self.child.to_representation(item) for item in data] + + class ReadOnlyField(Field): """ A read-only field that simply returns the field value. -- cgit v1.2.3 From 609014460861fdfe82054551790d6439292dde7b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 14:32:44 +0100 Subject: Simplify serialization slightly --- rest_framework/fields.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cf42d36c..4c49aaba 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -202,12 +202,15 @@ class Field(object): return self.default_empty_html if (ret == '') else ret return dictionary.get(self.field_name, empty) - def get_attribute(self, instance): + def get_field_representation(self, instance): """ - Given the *outgoing* object instance, return the value for this field - that should be returned as a primative value. + Given the outgoing object instance, return the primative value + that should be used for this field. """ - return get_attribute(instance, self.source_attrs) + attribute = get_attribute(instance, self.source_attrs) + if attribute is None: + return None + return self.to_representation(attribute) def get_default(self): """ -- cgit v1.2.3 From dee3f78cb688b1bee892ef78d6eec23ccf67a80e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Sep 2014 17:06:20 +0100 Subject: FileField and ImageField --- rest_framework/fields.py | 82 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 18 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4c49aaba..f4b53279 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,3 +1,4 @@ +from django import forms from django.conf import settings from django.core import validators from django.core.exceptions import ValidationError @@ -427,8 +428,6 @@ class CharField(Field): return str(data) def to_representation(self, value): - if value is None: - return None return str(value) @@ -446,8 +445,6 @@ class EmailField(CharField): return str(data).strip() def to_representation(self, value): - if value is None: - return None return str(value).strip() @@ -513,8 +510,6 @@ class IntegerField(Field): return data def to_representation(self, value): - if value is None: - return None return int(value) @@ -543,8 +538,6 @@ class FloatField(Field): self.fail('invalid') def to_representation(self, value): - if value is None: - return None return float(value) @@ -616,9 +609,6 @@ class DecimalField(Field): return value def to_representation(self, value): - if value in (None, ''): - return None - if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value).strip()) @@ -689,7 +679,7 @@ class DateTimeField(Field): self.fail('invalid', format=humanized_format) def to_representation(self, value): - if value is None or self.format is None: + if self.format is None: return value if self.format.lower() == ISO_8601: @@ -741,7 +731,7 @@ class DateField(Field): self.fail('invalid', format=humanized_format) def to_representation(self, value): - if value is None or self.format is None: + if self.format is None: return value # Applying a `DateField` to a datetime value is almost always @@ -795,7 +785,7 @@ class TimeField(Field): self.fail('invalid', format=humanized_format) def to_representation(self, value): - if value is None or self.format is None: + if self.format is None: return value # Applying a `TimeField` to a datetime value is almost always @@ -875,14 +865,68 @@ class MultipleChoiceField(ChoiceField): # File types... class FileField(Field): - pass # TODO + default_error_messages = { + 'required': _("No file was submitted."), + 'invalid': _("The submitted data was not a file. Check the encoding type on the form."), + 'no_name': _("No filename could be determined."), + 'empty': _("The submitted file is empty."), + 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'), + } + use_url = api_settings.UPLOADED_FILES_USE_URL + def __init__(self, *args, **kwargs): + self.max_length = kwargs.pop('max_length', None) + self.allow_empty_file = kwargs.pop('allow_empty_file', False) + self.use_url = kwargs.pop('use_url', self.use_url) + super(FileField, self).__init__(*args, **kwargs) -class ImageField(Field): - pass # TODO + def to_internal_value(self, data): + try: + # `UploadedFile` objects should have name and size attributes. + file_name = data.name + file_size = data.size + except AttributeError: + self.fail('invalid') + if not file_name: + self.fail('no_name') + if not self.allow_empty_file and not file_size: + self.fail('empty') + if self.max_length and len(file_name) > self.max_length: + self.fail('max_length', max_length=self.max_length, length=len(file_name)) -# Advanced field types... + return data + + def to_representation(self, value): + if self.use_url: + return settings.MEDIA_URL + value.url + return value.name + + +class ImageField(FileField): + default_error_messages = { + 'invalid_image': _( + 'Upload a valid image. The file you uploaded was either not an ' + 'image or a corrupted image.' + ), + } + + def __init__(self, *args, **kwargs): + self._DjangoImageField = kwargs.pop('_DjangoImageField', forms.ImageField) + super(ImageField, self).__init__(*args, **kwargs) + + def to_internal_value(self, data): + # Image validation is a bit grungy, so we'll just outright + # defer to Django's implementation so we don't need to + # consider it, or treat PIL as a test dependancy. + file_object = super(ImageField, self).to_internal_value(data) + django_field = self._DjangoImageField() + django_field.error_messages = self.error_messages + django_field.to_python(file_object) + return file_object + + +# Composite field types... class ListField(Field): child = None @@ -922,6 +966,8 @@ class ListField(Field): return [self.child.to_representation(item) for item in data] +# Miscellaneous field types... + class ReadOnlyField(Field): """ A read-only field that simply returns the field value. -- cgit v1.2.3 From 43fd5a873051c99600386c1fdc9fa368edeb6eda Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Sep 2014 09:24:03 +0100 Subject: Uniqueness validation --- rest_framework/fields.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f4b53279..231f693c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -150,6 +150,10 @@ class Field(object): messages.update(error_messages or {}) self.error_messages = messages + for validator in validators: + if getattr(validator, 'requires_context', False): + validator.serializer_field = self + def bind(self, field_name, parent): """ Initializes the field name and parent for the field instance. -- cgit v1.2.3 From d2d412993f537952fd7809ded3e981f85ec318e9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Sep 2014 11:24:21 +0100 Subject: .validate() on serializer fields --- rest_framework/fields.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 231f693c..fee6080a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -254,6 +254,7 @@ class Field(object): value = self.to_internal_value(data) self.run_validators(value) + self.validate(value) return value def run_validators(self, value): @@ -270,6 +271,9 @@ class Field(object): if errors: raise ValidationError(errors) + def validate(self, value): + pass + def to_internal_value(self, data): """ Transform the *incoming* primative data into a native value. -- cgit v1.2.3 From d1b2c8ac7faec65483cbddf4f1718ca4f5805246 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Sep 2014 14:12:26 +0100 Subject: Absolute URLs for file fields --- rest_framework/fields.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fee6080a..f7ea3b0c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -150,10 +150,6 @@ class Field(object): messages.update(error_messages or {}) self.error_messages = messages - for validator in validators: - if getattr(validator, 'requires_context', False): - validator.serializer_field = self - def bind(self, field_name, parent): """ Initializes the field name and parent for the field instance. @@ -264,6 +260,8 @@ class Field(object): """ errors = [] for validator in self.validators: + if getattr(validator, 'requires_context', False): + validator.serializer_field = self try: validator(value) except ValidationError as exc: @@ -907,7 +905,11 @@ class FileField(Field): def to_representation(self, value): if self.use_url: - return settings.MEDIA_URL + value.url + url = settings.MEDIA_URL + value.url + request = self.context.get('request', None) + if request is not None: + return request.build_absolute_uri(url) + return url return value.name -- cgit v1.2.3 From 381771731f48c75e7d5951e353049cceec386512 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 1 Oct 2014 13:09:14 +0100 Subject: Use six.text_type instead of str everywhere --- rest_framework/fields.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f7ea3b0c..f3ff2233 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -2,7 +2,7 @@ from django import forms from django.conf import settings from django.core import validators from django.core.exceptions import ValidationError -from django.utils import timezone +from django.utils import six, timezone from django.utils.datastructures import SortedDict from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type @@ -431,10 +431,10 @@ class CharField(Field): return super(CharField, self).run_validation(data) def to_internal_value(self, data): - return str(data) + return six.text_type(data) def to_representation(self, value): - return str(value) + return six.text_type(value) class EmailField(CharField): @@ -448,10 +448,10 @@ class EmailField(CharField): self.validators.append(validator) def to_internal_value(self, data): - return str(data).strip() + return six.text_type(data).strip() def to_representation(self, value): - return str(value).strip() + return six.text_type(value).strip() class RegexField(CharField): @@ -510,7 +510,7 @@ class IntegerField(Field): def to_internal_value(self, data): try: - data = int(str(data)) + data = int(six.text_type(data)) except (ValueError, TypeError): self.fail('invalid') return data @@ -616,7 +616,7 @@ class DecimalField(Field): def to_representation(self, value): if not isinstance(value, decimal.Decimal): - value = decimal.Decimal(str(value).strip()) + value = decimal.Decimal(six.text_type(value).strip()) context = decimal.getcontext().copy() context.prec = self.max_digits @@ -832,19 +832,19 @@ class ChoiceField(Field): # Allows us to deal with eg. integer choices while supporting either # integer or string input, but still get the correct datatype out. self.choice_strings_to_values = dict([ - (str(key), key) for key in self.choices.keys() + (six.text_type(key), key) for key in self.choices.keys() ]) super(ChoiceField, self).__init__(**kwargs) def to_internal_value(self, data): try: - return self.choice_strings_to_values[str(data)] + return self.choice_strings_to_values[six.text_type(data)] except KeyError: self.fail('invalid_choice', input=data) def to_representation(self, value): - return self.choice_strings_to_values[str(value)] + return self.choice_strings_to_values[six.text_type(value)] class MultipleChoiceField(ChoiceField): @@ -864,7 +864,7 @@ class MultipleChoiceField(ChoiceField): def to_representation(self, value): return set([ - self.choice_strings_to_values[str(item)] for item in value + self.choice_strings_to_values[six.text_type(item)] for item in value ]) -- cgit v1.2.3 From df7b6fcf58417fd95e49655eb140b387899b1ceb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Oct 2014 16:24:24 +0100 Subject: First pass on incorperating the form rendering into the browsable API --- rest_framework/fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f3ff2233..c794963e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -689,10 +689,10 @@ class DateTimeField(Field): return value if self.format.lower() == ISO_8601: - ret = value.isoformat() - if ret.endswith('+00:00'): - ret = ret[:-6] + 'Z' - return ret + value = value.isoformat() + if value.endswith('+00:00'): + value = value[:-6] + 'Z' + return value return value.strftime(self.format) -- cgit v1.2.3 From fec7c4b45812d22423e73ec3ab801857a55d7340 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Oct 2014 18:13:15 +0100 Subject: Browsable API tweaks --- rest_framework/fields.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c794963e..3f22660c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -106,7 +106,7 @@ class Field(object): 'null': _('This field may not be null.') } default_validators = [] - default_empty_html = None + default_empty_html = empty initial = None def __init__(self, read_only=False, write_only=False, @@ -375,7 +375,6 @@ class NullBooleanField(Field): default_error_messages = { 'invalid': _('`{input}` is not a valid boolean.') } - default_empty_html = None initial = None TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) @@ -411,7 +410,6 @@ class CharField(Field): default_error_messages = { 'blank': _('This field may not be blank.') } - default_empty_html = '' initial = '' def __init__(self, **kwargs): @@ -852,6 +850,7 @@ class MultipleChoiceField(ChoiceField): 'invalid_choice': _('`{input}` is not a valid choice.'), 'not_a_list': _('Expected a list of items but got type `{input_type}`') } + default_empty_html = [] def to_internal_value(self, data): if isinstance(data, type('')) or not hasattr(data, '__iter__'): -- cgit v1.2.3 From dfab9af294972720f59890967cd9ae1a6c0796b6 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 3 Oct 2014 08:41:18 +1300 Subject: Minor: fix spelling and grammar, mostly in 3.0 announcement --- rest_framework/fields.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f3ff2233..bba8ccae 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -186,14 +186,14 @@ class Field(object): def get_initial(self): """ - Return a value to use when the field is being returned as a primative + Return a value to use when the field is being returned as a primitive value, without any object instance. """ return self.initial def get_value(self, dictionary): """ - Given the *incoming* primative data, return the value for this field + Given the *incoming* primitive data, return the value for this field that should be validated and transformed to a native value. """ if html.is_html_input(dictionary): @@ -205,7 +205,7 @@ class Field(object): def get_field_representation(self, instance): """ - Given the outgoing object instance, return the primative value + Given the outgoing object instance, return the primitive value that should be used for this field. """ attribute = get_attribute(instance, self.source_attrs) @@ -274,13 +274,13 @@ class Field(object): def to_internal_value(self, data): """ - Transform the *incoming* primative data into a native value. + Transform the *incoming* primitive data into a native value. """ raise NotImplementedError('to_internal_value() must be implemented.') def to_representation(self, value): """ - Transform the *outgoing* native value into primative data. + Transform the *outgoing* native value into primitive data. """ raise NotImplementedError('to_representation() must be implemented.') @@ -928,7 +928,7 @@ class ImageField(FileField): def to_internal_value(self, data): # Image validation is a bit grungy, so we'll just outright # defer to Django's implementation so we don't need to - # consider it, or treat PIL as a test dependancy. + # consider it, or treat PIL as a test dependency. file_object = super(ImageField, self).to_internal_value(data) django_field = self._DjangoImageField() django_field.error_messages = self.error_messages -- cgit v1.2.3 From 6bfed6f8525a49fc50df7143ac2d492528b8f2ac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Oct 2014 17:04:53 +0100 Subject: Enforce uniqueness validation for relational fields --- rest_framework/fields.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0963d4bf..9d577c53 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -224,6 +224,8 @@ class Field(object): """ if self.default is empty: raise SkipField() + if is_simple_callable(self.default): + return self.default() return self.default def run_validation(self, data=empty): -- cgit v1.2.3 From 093febb91299e332c810de6a6b6aba57c2b16a91 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 8 Oct 2014 11:04:08 +0100 Subject: Tests for relational fields --- rest_framework/fields.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9d577c53..5fb0ec8d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,7 +1,7 @@ from django import forms from django.conf import settings from django.core import validators -from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.utils import six, timezone from django.utils.datastructures import SortedDict from django.utils.dateparse import parse_date, parse_datetime, parse_time @@ -54,6 +54,8 @@ def get_attribute(instance, attrs): for attr in attrs: try: instance = getattr(instance, attr) + except ObjectDoesNotExist: + return None except AttributeError as exc: try: return instance[attr] @@ -108,6 +110,7 @@ class Field(object): default_validators = [] default_empty_html = empty initial = None + coerce_blank_to_null = True def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=empty, source=None, @@ -245,6 +248,9 @@ class Field(object): self.fail('required') return self.get_default() + if data == '' and self.coerce_blank_to_null: + data = None + if data is None: if not self.allow_null: self.fail('null') @@ -413,6 +419,7 @@ class CharField(Field): 'blank': _('This field may not be blank.') } initial = '' + coerce_blank_to_null = False def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) -- cgit v1.2.3 From 5ead8dc89d1a99d6189170dc8dac19cdc8ba7750 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 8 Oct 2014 16:59:52 +0100 Subject: Support empty file fields --- rest_framework/fields.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5fb0ec8d..f86f6626 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -913,6 +913,8 @@ class FileField(Field): def to_representation(self, value): if self.use_url: + if not value: + return None url = settings.MEDIA_URL + value.url request = self.context.get('request', None) if request is not None: -- cgit v1.2.3 From f7d43f530a94e686d2f93781471b9ac4e90d0f58 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 8 Oct 2014 17:03:14 +0100 Subject: Limit blank string -> None to just be on relational fields --- rest_framework/fields.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f86f6626..b371c7d0 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -110,7 +110,6 @@ class Field(object): default_validators = [] default_empty_html = empty initial = None - coerce_blank_to_null = True def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=empty, source=None, @@ -248,9 +247,6 @@ class Field(object): self.fail('required') return self.get_default() - if data == '' and self.coerce_blank_to_null: - data = None - if data is None: if not self.allow_null: self.fail('null') -- cgit v1.2.3 From 5d247a65c89594a7ab5ce2333612f23eadc6828d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 Oct 2014 15:11:19 +0100 Subject: First pass on nested serializers in HTML --- rest_framework/fields.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b371c7d0..7053acee 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -8,7 +8,10 @@ from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 -from rest_framework.compat import smart_text, EmailValidator, MinValueValidator, MaxValueValidator, URLValidator +from rest_framework.compat import ( + smart_text, EmailValidator, MinValueValidator, MaxValueValidator, + MinLengthValidator, MaxLengthValidator, URLValidator +) from rest_framework.settings import api_settings from rest_framework.utils import html, representation, humanize_datetime import copy @@ -138,7 +141,7 @@ class Field(object): self.label = label self.help_text = help_text self.style = {} if style is None else style - self.validators = validators or self.default_validators[:] + self.validators = validators[:] or self.default_validators[:] self.allow_null = allow_null # These are set up by `.bind()` when the field is added to a serializer. @@ -412,16 +415,24 @@ class NullBooleanField(Field): class CharField(Field): default_error_messages = { - 'blank': _('This field may not be blank.') + 'blank': _('This field may not be blank.'), + 'max_length': _('Ensure this field has no more than {max_length} characters.'), + 'min_length': _('Ensure this field has no more than {min_length} characters.') } initial = '' coerce_blank_to_null = False def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) - self.max_length = kwargs.pop('max_length', None) - self.min_length = kwargs.pop('min_length', None) + max_length = kwargs.pop('max_length', None) + 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)) def run_validation(self, data=empty): # Test for the empty string here so that it does not get validated, @@ -857,6 +868,13 @@ class MultipleChoiceField(ChoiceField): } default_empty_html = [] + def get_value(self, dictionary): + # We override the default field access in order to support + # lists in HTML forms. + if html.is_html_input(dictionary): + return dictionary.getlist(self.field_name) + return dictionary.get(self.field_name, empty) + def to_internal_value(self, data): if isinstance(data, type('')) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) -- cgit v1.2.3 From d9a199ca0ddf92f999aa37b396596d0e3e0a26d9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 Oct 2014 14:16:09 +0100 Subject: exceptions.ValidationFailed, not Django's ValidationError --- rest_framework/fields.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 7053acee..b881ad13 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,7 +1,8 @@ -from django import forms from django.conf import settings from django.core import validators -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ValidationError as DjangoValidationError +from django.forms import ImageField as DjangoImageField from django.utils import six, timezone from django.utils.datastructures import SortedDict from django.utils.dateparse import parse_date, parse_datetime, parse_time @@ -12,6 +13,7 @@ from rest_framework.compat import ( smart_text, EmailValidator, MinValueValidator, MaxValueValidator, MinLengthValidator, MaxLengthValidator, URLValidator ) +from rest_framework.exceptions import ValidationFailed from rest_framework.settings import api_settings from rest_framework.utils import html, representation, humanize_datetime import copy @@ -98,7 +100,7 @@ NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`' NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`' USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField' MISSING_ERROR_MESSAGE = ( - 'ValidationError raised by `{class_name}`, but error key `{key}` does ' + 'ValidationFailed raised by `{class_name}`, but error key `{key}` does ' 'not exist in the `error_messages` dictionary.' ) @@ -263,7 +265,7 @@ class Field(object): def run_validators(self, value): """ Test the given value against all the validators on the field, - and either raise a `ValidationError` or simply return. + and either raise a `ValidationFailed` or simply return. """ errors = [] for validator in self.validators: @@ -271,10 +273,12 @@ class Field(object): validator.serializer_field = self try: validator(value) - except ValidationError as exc: + except ValidationFailed as exc: + errors.extend(exc.detail) + except DjangoValidationError as exc: errors.extend(exc.messages) if errors: - raise ValidationError(errors) + raise ValidationFailed(errors) def validate(self, value): pass @@ -301,7 +305,8 @@ class Field(object): class_name = self.__class__.__name__ msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key) raise AssertionError(msg) - raise ValidationError(msg.format(**kwargs)) + message_string = msg.format(**kwargs) + raise ValidationFailed(message_string) @property def root(self): @@ -946,7 +951,7 @@ class ImageField(FileField): } def __init__(self, *args, **kwargs): - self._DjangoImageField = kwargs.pop('_DjangoImageField', forms.ImageField) + self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField) super(ImageField, self).__init__(*args, **kwargs) def to_internal_value(self, data): -- cgit v1.2.3 From b4f3379c7002f0c80a26605fdd9c69d7cef2f16f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 15 Oct 2014 15:13:28 +0100 Subject: Support fields that reference a simple callable --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b881ad13..24dfaaf5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -66,6 +66,8 @@ def get_attribute(instance, attrs): return instance[attr] except (KeyError, TypeError, AttributeError): raise exc + if is_simple_callable(instance): + return instance() return instance @@ -1025,8 +1027,6 @@ class ReadOnlyField(Field): super(ReadOnlyField, self).__init__(**kwargs) def to_representation(self, value): - if is_simple_callable(value): - return value() return value -- cgit v1.2.3 From 32fd82ba0d6082418e5ca5633f9e9709bd44e86b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 16 Oct 2014 20:45:36 +0100 Subject: get_attribute method on fields --- rest_framework/fields.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 24dfaaf5..597d5e12 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -212,15 +212,12 @@ class Field(object): return self.default_empty_html if (ret == '') else ret return dictionary.get(self.field_name, empty) - def get_field_representation(self, instance): + def get_attribute(self, instance): """ - Given the outgoing object instance, return the primitive value + Given the *outgoing* object instance, return the primitive value that should be used for this field. """ - attribute = get_attribute(instance, self.source_attrs) - if attribute is None: - return None - return self.to_representation(attribute) + return get_attribute(instance, self.source_attrs) def get_default(self): """ -- cgit v1.2.3 From 05cbec9dd7f9f0b6a9b59b29ac6c9272b6ae50d8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 17 Oct 2014 13:23:14 +0100 Subject: Use serializers.ValidationError --- rest_framework/fields.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 597d5e12..2da4aa8b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -13,7 +13,7 @@ from rest_framework.compat import ( smart_text, EmailValidator, MinValueValidator, MaxValueValidator, MinLengthValidator, MaxLengthValidator, URLValidator ) -from rest_framework.exceptions import ValidationFailed +from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings from rest_framework.utils import html, representation, humanize_datetime import copy @@ -102,7 +102,7 @@ NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`' NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`' USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField' MISSING_ERROR_MESSAGE = ( - 'ValidationFailed raised by `{class_name}`, but error key `{key}` does ' + 'ValidationError raised by `{class_name}`, but error key `{key}` does ' 'not exist in the `error_messages` dictionary.' ) @@ -264,7 +264,7 @@ class Field(object): def run_validators(self, value): """ Test the given value against all the validators on the field, - and either raise a `ValidationFailed` or simply return. + and either raise a `ValidationError` or simply return. """ errors = [] for validator in self.validators: @@ -272,12 +272,12 @@ class Field(object): validator.serializer_field = self try: validator(value) - except ValidationFailed as exc: + except ValidationError as exc: errors.extend(exc.detail) except DjangoValidationError as exc: errors.extend(exc.messages) if errors: - raise ValidationFailed(errors) + raise ValidationError(errors) def validate(self, value): pass @@ -305,7 +305,7 @@ class Field(object): msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key) raise AssertionError(msg) message_string = msg.format(**kwargs) - raise ValidationFailed(message_string) + raise ValidationError(message_string) @property def root(self): -- cgit v1.2.3 From ae53fdff9c6bb3e81a1ec005134462f0d629688f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 22 Oct 2014 13:30:28 +0100 Subject: First pass at unique_for_date, unique_for_month, unique_for_year --- rest_framework/fields.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2da4aa8b..e939b2f2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -268,11 +268,17 @@ class Field(object): """ errors = [] for validator in self.validators: - if getattr(validator, 'requires_context', False): - validator.serializer_field = self + if hasattr(validator, 'set_context'): + validator.set_context(self) + try: validator(value) except ValidationError as exc: + # If the validation error contains a mapping of fields to + # errors then simply raise it immediately rather than + # attempting to accumulate a list of errors. + if isinstance(exc.detail, dict): + raise errors.extend(exc.detail) except DjangoValidationError as exc: errors.extend(exc.messages) -- cgit v1.2.3 From 9ebaabd6eb31e18cf0bb1c70893f719f18ecb0f9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 28 Oct 2014 16:21:49 +0000 Subject: unique_for_date/unique_for_month/unique_for_year --- rest_framework/fields.py | 54 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e939b2f2..82b7eb37 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -92,13 +92,35 @@ def set_value(dictionary, keys, value): dictionary[keys[-1]] = value +class CreateOnlyDefault: + """ + This class may be used to provide default values that are only used + for create operations, but that do not return any value for update + operations. + """ + def __init__(self, default): + self.default = default + + def set_context(self, serializer_field): + self.is_update = serializer_field.parent.instance is not None + + def __call__(self): + if self.is_update: + raise SkipField() + if callable(self.default): + return self.default() + return self.default + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, repr(self.default)) + + class SkipField(Exception): pass NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`' NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`' -NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`' NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`' USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField' MISSING_ERROR_MESSAGE = ( @@ -132,7 +154,6 @@ class Field(object): # Some combinations of keyword arguments do not make sense. assert not (read_only and write_only), NOT_READ_ONLY_WRITE_ONLY assert not (read_only and required), NOT_READ_ONLY_REQUIRED - assert not (read_only and default is not empty), NOT_READ_ONLY_DEFAULT assert not (required and default is not empty), NOT_REQUIRED_DEFAULT assert not (read_only and self.__class__ == Field), USE_READONLYFIELD @@ -230,7 +251,9 @@ class Field(object): """ if self.default is empty: raise SkipField() - if is_simple_callable(self.default): + if callable(self.default): + if hasattr(self.default, 'set_context'): + self.default.set_context(self) return self.default() return self.default @@ -244,6 +267,9 @@ class Field(object): May raise `SkipField` if the field should not be included in the validated data. """ + if self.read_only: + return self.get_default() + if data is empty: if getattr(self.root, 'partial', False): raise SkipField() @@ -1033,6 +1059,28 @@ class ReadOnlyField(Field): return value +class HiddenField(Field): + """ + A hidden field does not take input from the user, or present any output, + but it does populate a field in `validated_data`, based on its default + value. This is particularly useful when we have a `unique_for_date` + constrain on a pair of fields, as we need some way to include the date in + the validated data. + """ + def __init__(self, **kwargs): + assert 'default' in kwargs, 'default is a required argument.' + kwargs['write_only'] = True + super(HiddenField, self).__init__(**kwargs) + + def get_value(self, dictionary): + # We always use the default value for `HiddenField`. + # User input is never provided or accepted. + return empty + + def to_internal_value(self, data): + return data + + class SerializerMethodField(Field): """ A read-only field that get its representation from calling a method on the -- cgit v1.2.3 From bacf8cfa9d9331d0876e7ad7adccdabc5f4833a5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 31 Oct 2014 15:40:08 +0000 Subject: Guard against malicious string inputs for numbers. Closes #1903. --- rest_framework/fields.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 82b7eb37..d5dccd72 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -545,7 +545,9 @@ class IntegerField(Field): 'invalid': _('A valid integer is required.'), 'max_value': _('Ensure this value is less than or equal to {max_value}.'), 'min_value': _('Ensure this value is greater than or equal to {min_value}.'), + 'max_string_length': _('String value too large') } + MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. def __init__(self, **kwargs): max_value = kwargs.pop('max_value', None) @@ -559,8 +561,11 @@ class IntegerField(Field): self.validators.append(MinValueValidator(min_value, message=message)) def to_internal_value(self, data): + if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: + self.fail('max_string_length') + try: - data = int(six.text_type(data)) + data = int(data) except (ValueError, TypeError): self.fail('invalid') return data @@ -574,7 +579,9 @@ class FloatField(Field): 'invalid': _("A valid number is required."), 'max_value': _('Ensure this value is less than or equal to {max_value}.'), 'min_value': _('Ensure this value is greater than or equal to {min_value}.'), + 'max_string_length': _('String value too large') } + MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. def __init__(self, **kwargs): max_value = kwargs.pop('max_value', None) @@ -587,9 +594,12 @@ class FloatField(Field): message = self.error_messages['min_value'].format(min_value=min_value) self.validators.append(MinValueValidator(min_value, message=message)) - def to_internal_value(self, value): + def to_internal_value(self, data): + if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: + self.fail('max_string_length') + try: - return float(value) + return float(data) except (TypeError, ValueError): self.fail('invalid') @@ -604,8 +614,10 @@ class DecimalField(Field): 'min_value': _('Ensure this value is greater than or equal to {min_value}.'), 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'), 'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'), - 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.') + 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'), + 'max_string_length': _('String value too large') } + MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. coerce_to_string = api_settings.COERCE_DECIMAL_TO_STRING @@ -621,16 +633,19 @@ class DecimalField(Field): message = self.error_messages['min_value'].format(min_value=min_value) self.validators.append(MinValueValidator(min_value, message=message)) - def to_internal_value(self, value): + def to_internal_value(self, data): """ Validates that the input is a decimal number. Returns a Decimal instance. Returns None for empty values. Ensures that there are no more than max_digits in the number, and no more than decimal_places digits after the decimal point. """ - value = smart_text(value).strip() + data = smart_text(data).strip() + if len(data) > self.MAX_STRING_LENGTH: + self.fail('max_string_length') + try: - value = decimal.Decimal(value) + value = decimal.Decimal(data) except decimal.DecimalException: self.fail('invalid') -- cgit v1.2.3 From 207208fedff2457e921ef7d825ea7c3933b5dd6e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 31 Oct 2014 16:38:39 +0000 Subject: Lazy loading of fields and validators. Closes #1963. --- rest_framework/fields.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d5dccd72..8cc8e81e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -143,7 +143,7 @@ class Field(object): def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=empty, source=None, label=None, help_text=None, style=None, - error_messages=None, validators=[], allow_null=False): + error_messages=None, validators=None, allow_null=False): self._creation_counter = Field._creation_counter Field._creation_counter += 1 @@ -166,9 +166,11 @@ class Field(object): self.label = label self.help_text = help_text self.style = {} if style is None else style - self.validators = validators[:] or self.default_validators[:] self.allow_null = allow_null + if validators is not None: + self.validators = validators[:] + # These are set up by `.bind()` when the field is added to a serializer. self.field_name = None self.parent = None @@ -214,6 +216,21 @@ class Field(object): else: self.source_attrs = self.source.split('.') + # .validators is a lazily loaded property, that gets its default + # value from `get_validators`. + @property + def validators(self): + if not hasattr(self, '_validators'): + self._validators = self.get_validators() + return self._validators + + @validators.setter + def validators(self, validators): + self._validators = validators + + def get_validators(self): + return self.default_validators[:] + def get_initial(self): """ Return a value to use when the field is being returned as a primitive -- cgit v1.2.3 From 26b6180f5047e965383553177cd5cd7938bbe63e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Nov 2014 10:28:34 +0000 Subject: Support None if initial part of dotted lookup returns None. Closes #1223. --- rest_framework/fields.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 8cc8e81e..24ddb7a4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -57,6 +57,9 @@ def get_attribute(instance, attrs): Also accepts either attribute lookup on objects or dictionary lookups. """ for attr in attrs: + if instance is None: + # Break out early if we get `None` at any point in a nested lookup. + return None try: instance = getattr(instance, attr) except ObjectDoesNotExist: -- cgit v1.2.3 From 0a5d088287be1bb56f37504cc75cee10fb4e74a0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Nov 2014 10:48:30 +0000 Subject: Fix failing copy of fields when RegexValidator is used. Closes #1954. --- rest_framework/fields.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 24ddb7a4..363b684f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,7 +1,7 @@ from django.conf import settings -from django.core import validators from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.validators import RegexValidator from django.forms import ImageField as DjangoImageField from django.utils import six, timezone from django.utils.datastructures import SortedDict @@ -392,7 +392,15 @@ class Field(object): originally created with, rather than copying the complete state. """ args = copy.deepcopy(self._args) - kwargs = copy.deepcopy(self._kwargs) + kwargs = dict(self._kwargs) + # Bit ugly, but we need to special case 'validators' as Django's + # RegexValidator does not support deepcopy. + # We treat validator callables as immutable objects. + # See https://github.com/tomchristie/django-rest-framework/issues/1954 + validators = kwargs.pop('validators', None) + kwargs = copy.deepcopy(kwargs) + if validators is not None: + kwargs['validators'] = validators return self.__class__(*args, **kwargs) def __repr__(self): @@ -531,7 +539,7 @@ class RegexField(CharField): def __init__(self, regex, **kwargs): super(RegexField, self).__init__(**kwargs) - validator = validators.RegexValidator(regex, message=self.error_messages['invalid']) + validator = RegexValidator(regex, message=self.error_messages['invalid']) self.validators.append(validator) @@ -543,7 +551,7 @@ class SlugField(CharField): def __init__(self, **kwargs): super(SlugField, self).__init__(**kwargs) slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$') - validator = validators.RegexValidator(slug_regex, message=self.error_messages['invalid']) + validator = RegexValidator(slug_regex, message=self.error_messages['invalid']) self.validators.append(validator) -- cgit v1.2.3 From 73daf407150b4d0c456b529c4d9bba0e035fc370 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Nov 2014 15:51:45 +0000 Subject: ModelField should support max_length. Closes #2018. --- rest_framework/fields.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 363b684f..b554f238 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1175,10 +1175,20 @@ class ModelField(Field): This is used by `ModelSerializer` when dealing with custom model fields, that do not have a serializer field to be mapped to. """ + default_error_messages = { + 'max_length': _('Ensure this field has no more than {max_length} characters.'), + } + def __init__(self, model_field, **kwargs): self.model_field = model_field kwargs['source'] = '*' + # The `max_length` option is supported by Django's base `Field` class, + # so we'd better support it here. + max_length = kwargs.pop('max_length', None) super(ModelField, 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)) def to_internal_value(self, data): rel = getattr(self.model_field, 'rel', None) -- cgit v1.2.3 From ed541864e637681e1aca3a808be1f26202b4c271 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Nov 2014 10:34:59 +0000 Subject: Support for bulk create. Closes #1965. --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b554f238..a9e2e48b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -943,7 +943,7 @@ class ChoiceField(Field): class MultipleChoiceField(ChoiceField): default_error_messages = { 'invalid_choice': _('`{input}` is not a valid choice.'), - 'not_a_list': _('Expected a list of items but got type `{input_type}`') + 'not_a_list': _('Expected a list of items but got type `{input_type}`.') } default_empty_html = [] -- cgit v1.2.3 From 4e001dbb7ac0bc13d6d5fbb4524e905184610aa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Nov 2014 12:00:30 +0000 Subject: Drop usage of SortedDict. Closes #2027. --- rest_framework/fields.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a9e2e48b..71377326 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -4,14 +4,13 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.core.validators import RegexValidator from django.forms import ImageField as DjangoImageField from django.utils import six, timezone -from django.utils.datastructures import SortedDict from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 from rest_framework.compat import ( smart_text, EmailValidator, MinValueValidator, MaxValueValidator, - MinLengthValidator, MaxLengthValidator, URLValidator + MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict ) from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings @@ -917,9 +916,9 @@ class ChoiceField(Field): for item in choices ] if all(pairs): - self.choices = SortedDict([(key, display_value) for key, display_value in choices]) + self.choices = OrderedDict([(key, display_value) for key, display_value in choices]) else: - self.choices = SortedDict([(item, item) for item in choices]) + self.choices = OrderedDict([(item, item) for item in choices]) # Map the string representation of choices to the underlying value. # Allows us to deal with eg. integer choices while supporting either -- cgit v1.2.3 From ea76dc18b6770aefcc53a6c5752674c2fff0ddcf Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Thu, 6 Nov 2014 18:20:55 +0100 Subject: Fixed base_url duplication in FileField.use_url Django already joins base_url when constructing FileField.url--- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 71377326..95cda2ce 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1007,7 +1007,7 @@ class FileField(Field): if self.use_url: if not value: return None - url = settings.MEDIA_URL + value.url + url = value.url request = self.context.get('request', None) if request is not None: return request.build_absolute_uri(url) -- cgit v1.2.3 From eafb7e1e24c31f0a6795ef5b8ac52940569d5854 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Nov 2014 11:44:16 +0000 Subject: ModelField fix. Closes #2018. --- rest_framework/fields.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 95cda2ce..4933d8db 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1180,7 +1180,6 @@ class ModelField(Field): def __init__(self, model_field, **kwargs): self.model_field = model_field - kwargs['source'] = '*' # The `max_length` option is supported by Django's base `Field` class, # so we'd better support it here. max_length = kwargs.pop('max_length', None) @@ -1195,6 +1194,11 @@ class ModelField(Field): return rel.to._meta.get_field(rel.field_name).to_python(data) return self.model_field.to_python(data) + def get_attribute(self, obj): + # We pass the object instance onto `to_representation`, + # not just the field attribute. + return obj + def to_representation(self, obj): value = self.model_field._get_val_from_obj(obj) if is_protected_type(value): -- cgit v1.2.3 From 62ce653c61bb596f7fca99db86280c497c71b7ac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 10 Nov 2014 14:44:26 +0000 Subject: Update fields.py --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4933d8db..58482db5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1106,7 +1106,7 @@ class HiddenField(Field): A hidden field does not take input from the user, or present any output, but it does populate a field in `validated_data`, based on its default value. This is particularly useful when we have a `unique_for_date` - constrain on a pair of fields, as we need some way to include the date in + constraint on a pair of fields, as we need some way to include the date in the validated data. """ def __init__(self, **kwargs): -- cgit v1.2.3 From 0f508c58211051c873aae5a2d1c65a0c595e732a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Nov 2014 18:36:32 +0000 Subject: Docs for advanced default argument usage. Closes #1945 --- rest_framework/fields.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 58482db5..36afe7a9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -117,6 +117,17 @@ class CreateOnlyDefault: return '%s(%s)' % (self.__class__.__name__, repr(self.default)) +class CurrentUserDefault: + def set_context(self, serializer_field): + self.user = serializer_field.context['request'].user + + def __call__(self): + return self.user + + def __repr__(self): + return '%s()' % self.__class__.__name__ + + class SkipField(Exception): pass -- cgit v1.2.3 From e49d22dbda5ac889ab89f277e17752c840819de2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Nov 2014 09:31:26 +0000 Subject: Allow blank choices to render. Closes #2071. --- rest_framework/fields.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 36afe7a9..bb43708d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -947,6 +947,8 @@ class ChoiceField(Field): self.fail('invalid_choice', input=data) def to_representation(self, value): + if value in ('', None): + return value return self.choice_strings_to_values[six.text_type(value)] -- cgit v1.2.3 From 6794b3380a32b53fa88547a8b2a2b34834fe4df7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Nov 2014 12:15:33 +0000 Subject: Fixes for defaulting empty HTML fields to '', None, or empty. --- rest_framework/fields.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb43708d..778bc718 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -181,6 +181,9 @@ class Field(object): self.style = {} if style is None else style self.allow_null = allow_null + if allow_null and self.default_empty_html is empty: + self.default_empty_html = None + if validators is not None: self.validators = validators[:] @@ -495,6 +498,7 @@ class CharField(Field): } initial = '' coerce_blank_to_null = False + default_empty_html = '' def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) -- cgit v1.2.3 From 06fd63dade20e1a19276b7414a54b9f5d2ef8329 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 25 Nov 2014 11:14:28 +0000 Subject: Don't use default_empty_html value for partial updates. Closes #2118. --- rest_framework/fields.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 778bc718..3cf34886 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -262,7 +262,11 @@ class Field(object): if html.is_html_input(dictionary): # HTML forms will represent empty fields as '', and cannot # represent None or False values directly. - ret = dictionary.get(self.field_name, '') + if self.field_name not in dictionary: + if getattr(self.root, 'partial', False): + return empty + return self.default_empty_html + ret = dictionary[self.field_name] return self.default_empty_html if (ret == '') else ret return dictionary.get(self.field_name, empty) -- cgit v1.2.3 From 1ffe4857ec51c3b6c5a90059cca0fb82820d759e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Nov 2014 08:10:52 +0000 Subject: Support callable attributes in dotted source. Closes #2142. --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3cf34886..ca770f77 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -68,8 +68,8 @@ def get_attribute(instance, attrs): return instance[attr] except (KeyError, TypeError, AttributeError): raise exc - if is_simple_callable(instance): - return instance() + if is_simple_callable(instance): + return instance() return instance -- cgit v1.2.3 From 8579222cbdff98aef50e244e179b7c32a76ce8ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Nov 2014 15:14:11 +0000 Subject: Fix for nested attribute lookups where one is a callable --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index ca770f77..2e2a68b8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -69,7 +69,7 @@ def get_attribute(instance, attrs): except (KeyError, TypeError, AttributeError): raise exc if is_simple_callable(instance): - return instance() + instance = instance() return instance -- cgit v1.2.3 From d2d7e1dfde2a62ee8f6d904368dbd6581de278c9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Nov 2014 16:41:53 +0000 Subject: Drop Field.validate --- rest_framework/fields.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'rest_framework/fields.py') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2e2a68b8..ca9c479f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -321,7 +321,6 @@ class Field(object): value = self.to_internal_value(data) self.run_validators(value) - self.validate(value) return value def run_validators(self, value): @@ -348,9 +347,6 @@ class Field(object): if errors: raise ValidationError(errors) - def validate(self, value): - pass - def to_internal_value(self, data): """ Transform the *incoming* primitive data into a native value. -- cgit v1.2.3