mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 10:03:57 +03:00
bd7977eed7
Most of these had obvious exceptions which would be thrown. Some I'm not sure about but they should at least catch only Exception so as not to ignore SystemExit and other inappropriate Error classes.
672 lines
22 KiB
Python
672 lines
22 KiB
Python
from __future__ import unicode_literals
|
|
|
|
import copy
|
|
import datetime
|
|
import inspect
|
|
import re
|
|
import warnings
|
|
|
|
from django.core import validators
|
|
from django.core.exceptions import ValidationError
|
|
from django.conf import settings
|
|
from django import forms
|
|
from django.forms import widgets
|
|
from django.utils.encoding import is_protected_type
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from rest_framework.compat import parse_date, parse_datetime
|
|
from rest_framework.compat import timezone
|
|
from rest_framework.compat import BytesIO
|
|
from rest_framework.compat import six
|
|
from rest_framework.compat import smart_text
|
|
|
|
|
|
def is_simple_callable(obj):
|
|
"""
|
|
True if the object is a callable that takes no arguments.
|
|
"""
|
|
return (
|
|
(inspect.isfunction(obj) and not inspect.getargspec(obj)[0]) or
|
|
(inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1)
|
|
)
|
|
|
|
|
|
class Field(object):
|
|
read_only = True
|
|
creation_counter = 0
|
|
empty = ''
|
|
type_name = None
|
|
partial = False
|
|
use_files = False
|
|
form_field_class = forms.CharField
|
|
|
|
def __init__(self, source=None):
|
|
self.parent = None
|
|
|
|
self.creation_counter = Field.creation_counter
|
|
Field.creation_counter += 1
|
|
|
|
self.source = source
|
|
|
|
def initialize(self, parent, field_name):
|
|
"""
|
|
Called to set up a field prior to field_to_native or field_from_native.
|
|
|
|
parent - The parent serializer.
|
|
model_field - The model field this field corresponds to, if one exists.
|
|
"""
|
|
self.parent = parent
|
|
self.root = parent.root or parent
|
|
self.context = self.root.context
|
|
self.partial = self.root.partial
|
|
if self.partial:
|
|
self.required = False
|
|
|
|
def field_from_native(self, data, files, field_name, into):
|
|
"""
|
|
Given a dictionary and a field name, updates the dictionary `into`,
|
|
with the field and it's deserialized value.
|
|
"""
|
|
return
|
|
|
|
def field_to_native(self, obj, field_name):
|
|
"""
|
|
Given and object and a field name, returns the value that should be
|
|
serialized for that field.
|
|
"""
|
|
if obj is None:
|
|
return self.empty
|
|
|
|
if self.source == '*':
|
|
return self.to_native(obj)
|
|
|
|
if self.source:
|
|
value = obj
|
|
for component in self.source.split('.'):
|
|
value = getattr(value, component)
|
|
if is_simple_callable(value):
|
|
value = value()
|
|
else:
|
|
value = getattr(obj, field_name)
|
|
return self.to_native(value)
|
|
|
|
def to_native(self, value):
|
|
"""
|
|
Converts the field's value into it's simple representation.
|
|
"""
|
|
if is_simple_callable(value):
|
|
value = value()
|
|
|
|
if is_protected_type(value):
|
|
return value
|
|
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
|
|
return [self.to_native(item) for item in value]
|
|
elif isinstance(value, dict):
|
|
return dict(map(self.to_native, (k, v)) for k, v in value.items())
|
|
return smart_text(value)
|
|
|
|
def attributes(self):
|
|
"""
|
|
Returns a dictionary of attributes to be used when serializing to xml.
|
|
"""
|
|
if self.type_name:
|
|
return {'type': self.type_name}
|
|
return {}
|
|
|
|
|
|
class WritableField(Field):
|
|
"""
|
|
Base for read/write fields.
|
|
"""
|
|
default_validators = []
|
|
default_error_messages = {
|
|
'required': _('This field is required.'),
|
|
'invalid': _('Invalid value.'),
|
|
}
|
|
widget = widgets.TextInput
|
|
default = None
|
|
|
|
def __init__(self, source=None, read_only=False, required=None,
|
|
validators=[], error_messages=None, widget=None,
|
|
default=None, blank=None):
|
|
|
|
# 'blank' is to be deprecated in favor of 'required'
|
|
if blank is not None:
|
|
warnings.warn('The `blank` keyword argument is due to deprecated. '
|
|
'Use the `required` keyword argument instead.',
|
|
PendingDeprecationWarning, stacklevel=2)
|
|
required = not(blank)
|
|
|
|
super(WritableField, self).__init__(source=source)
|
|
|
|
self.read_only = read_only
|
|
if required is None:
|
|
self.required = not(read_only)
|
|
else:
|
|
assert not (read_only and required), "Cannot set required=True and read_only=True"
|
|
self.required = required
|
|
|
|
messages = {}
|
|
for c in reversed(self.__class__.__mro__):
|
|
messages.update(getattr(c, 'default_error_messages', {}))
|
|
messages.update(error_messages or {})
|
|
self.error_messages = messages
|
|
|
|
self.validators = self.default_validators + validators
|
|
self.default = default if default is not None else self.default
|
|
|
|
# Widgets are ony used for HTML forms.
|
|
widget = widget or self.widget
|
|
if isinstance(widget, type):
|
|
widget = widget()
|
|
self.widget = widget
|
|
|
|
def validate(self, value):
|
|
if value in validators.EMPTY_VALUES and self.required:
|
|
raise ValidationError(self.error_messages['required'])
|
|
|
|
def run_validators(self, value):
|
|
if value in validators.EMPTY_VALUES:
|
|
return
|
|
errors = []
|
|
for v in self.validators:
|
|
try:
|
|
v(value)
|
|
except ValidationError as e:
|
|
if hasattr(e, 'code') and e.code in self.error_messages:
|
|
message = self.error_messages[e.code]
|
|
if e.params:
|
|
message = message % e.params
|
|
errors.append(message)
|
|
else:
|
|
errors.extend(e.messages)
|
|
if errors:
|
|
raise ValidationError(errors)
|
|
|
|
def field_from_native(self, data, files, field_name, into):
|
|
"""
|
|
Given a dictionary and a field name, updates the dictionary `into`,
|
|
with the field and it's deserialized value.
|
|
"""
|
|
if self.read_only:
|
|
return
|
|
|
|
try:
|
|
if self.use_files:
|
|
files = files or {}
|
|
native = files[field_name]
|
|
else:
|
|
native = data[field_name]
|
|
except KeyError:
|
|
if self.default is not None and not self.partial:
|
|
# Note: partial updates shouldn't set defaults
|
|
native = self.default
|
|
else:
|
|
if self.required:
|
|
raise ValidationError(self.error_messages['required'])
|
|
return
|
|
|
|
value = self.from_native(native)
|
|
if self.source == '*':
|
|
if value:
|
|
into.update(value)
|
|
else:
|
|
self.validate(value)
|
|
self.run_validators(value)
|
|
into[self.source or field_name] = value
|
|
|
|
def from_native(self, value):
|
|
"""
|
|
Reverts a simple representation back to the field's value.
|
|
"""
|
|
return value
|
|
|
|
|
|
class ModelField(WritableField):
|
|
"""
|
|
A generic field that can be used against an arbitrary model field.
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
try:
|
|
self.model_field = kwargs.pop('model_field')
|
|
except KeyError:
|
|
raise ValueError("ModelField requires 'model_field' kwarg")
|
|
|
|
self.min_length = kwargs.pop('min_length',
|
|
getattr(self.model_field, 'min_length', None))
|
|
self.max_length = kwargs.pop('max_length',
|
|
getattr(self.model_field, 'max_length', None))
|
|
|
|
super(ModelField, self).__init__(*args, **kwargs)
|
|
|
|
if self.min_length is not None:
|
|
self.validators.append(validators.MinLengthValidator(self.min_length))
|
|
if self.max_length is not None:
|
|
self.validators.append(validators.MaxLengthValidator(self.max_length))
|
|
|
|
def from_native(self, value):
|
|
rel = getattr(self.model_field, "rel", None)
|
|
if rel is not None:
|
|
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
|
else:
|
|
return self.model_field.to_python(value)
|
|
|
|
def field_to_native(self, obj, field_name):
|
|
value = self.model_field._get_val_from_obj(obj)
|
|
if is_protected_type(value):
|
|
return value
|
|
return self.model_field.value_to_string(obj)
|
|
|
|
def attributes(self):
|
|
return {
|
|
"type": self.model_field.get_internal_type()
|
|
}
|
|
|
|
|
|
##### Typed Fields #####
|
|
|
|
class BooleanField(WritableField):
|
|
type_name = 'BooleanField'
|
|
form_field_class = forms.BooleanField
|
|
widget = widgets.CheckboxInput
|
|
default_error_messages = {
|
|
'invalid': _("'%s' value must be either True or False."),
|
|
}
|
|
empty = False
|
|
|
|
# Note: we set default to `False` in order to fill in missing value not
|
|
# supplied by html form. TODO: Fix so that only html form input gets
|
|
# this behavior.
|
|
default = False
|
|
|
|
def from_native(self, value):
|
|
if value in ('true', 't', 'True', '1'):
|
|
return True
|
|
if value in ('false', 'f', 'False', '0'):
|
|
return False
|
|
return bool(value)
|
|
|
|
|
|
class CharField(WritableField):
|
|
type_name = 'CharField'
|
|
form_field_class = forms.CharField
|
|
|
|
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
|
|
self.max_length, self.min_length = max_length, min_length
|
|
super(CharField, self).__init__(*args, **kwargs)
|
|
if min_length is not None:
|
|
self.validators.append(validators.MinLengthValidator(min_length))
|
|
if max_length is not None:
|
|
self.validators.append(validators.MaxLengthValidator(max_length))
|
|
|
|
def from_native(self, value):
|
|
if isinstance(value, six.string_types) or value is None:
|
|
return value
|
|
return smart_text(value)
|
|
|
|
|
|
class URLField(CharField):
|
|
type_name = 'URLField'
|
|
|
|
def __init__(self, **kwargs):
|
|
kwargs['max_length'] = kwargs.get('max_length', 200)
|
|
kwargs['validators'] = [validators.URLValidator()]
|
|
super(URLField, self).__init__(**kwargs)
|
|
|
|
|
|
class SlugField(CharField):
|
|
type_name = 'SlugField'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs['max_length'] = kwargs.get('max_length', 50)
|
|
super(SlugField, self).__init__(*args, **kwargs)
|
|
|
|
|
|
class ChoiceField(WritableField):
|
|
type_name = 'ChoiceField'
|
|
form_field_class = forms.ChoiceField
|
|
widget = widgets.Select
|
|
default_error_messages = {
|
|
'invalid_choice': _('Select a valid choice. %(value)s is not one of '
|
|
'the available choices.'),
|
|
}
|
|
|
|
def __init__(self, choices=(), *args, **kwargs):
|
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
|
self.choices = choices
|
|
|
|
def _get_choices(self):
|
|
return self._choices
|
|
|
|
def _set_choices(self, value):
|
|
# Setting choices also sets the choices on the widget.
|
|
# choices can be any iterable, but we call list() on it because
|
|
# it will be consumed more than once.
|
|
self._choices = self.widget.choices = list(value)
|
|
|
|
choices = property(_get_choices, _set_choices)
|
|
|
|
def validate(self, value):
|
|
"""
|
|
Validates that the input is in self.choices.
|
|
"""
|
|
super(ChoiceField, self).validate(value)
|
|
if value and not self.valid_value(value):
|
|
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
|
|
|
|
def valid_value(self, value):
|
|
"""
|
|
Check to see if the provided value is a valid choice.
|
|
"""
|
|
for k, v in self.choices:
|
|
if isinstance(v, (list, tuple)):
|
|
# This is an optgroup, so look inside the group for options
|
|
for k2, v2 in v:
|
|
if value == smart_text(k2):
|
|
return True
|
|
else:
|
|
if value == smart_text(k) or value == k:
|
|
return True
|
|
return False
|
|
|
|
|
|
class EmailField(CharField):
|
|
type_name = 'EmailField'
|
|
form_field_class = forms.EmailField
|
|
|
|
default_error_messages = {
|
|
'invalid': _('Enter a valid e-mail address.'),
|
|
}
|
|
default_validators = [validators.validate_email]
|
|
|
|
def from_native(self, value):
|
|
ret = super(EmailField, self).from_native(value)
|
|
if ret is None:
|
|
return None
|
|
return ret.strip()
|
|
|
|
def __deepcopy__(self, memo):
|
|
result = copy.copy(self)
|
|
memo[id(self)] = result
|
|
#result.widget = copy.deepcopy(self.widget, memo)
|
|
result.validators = self.validators[:]
|
|
return result
|
|
|
|
|
|
class RegexField(CharField):
|
|
type_name = 'RegexField'
|
|
form_field_class = forms.RegexField
|
|
|
|
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
|
|
super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
|
|
self.regex = regex
|
|
|
|
def _get_regex(self):
|
|
return self._regex
|
|
|
|
def _set_regex(self, regex):
|
|
if isinstance(regex, six.string_types):
|
|
regex = re.compile(regex)
|
|
self._regex = regex
|
|
if hasattr(self, '_regex_validator') and self._regex_validator in self.validators:
|
|
self.validators.remove(self._regex_validator)
|
|
self._regex_validator = validators.RegexValidator(regex=regex)
|
|
self.validators.append(self._regex_validator)
|
|
|
|
regex = property(_get_regex, _set_regex)
|
|
|
|
def __deepcopy__(self, memo):
|
|
result = copy.copy(self)
|
|
memo[id(self)] = result
|
|
result.validators = self.validators[:]
|
|
return result
|
|
|
|
|
|
class DateField(WritableField):
|
|
type_name = 'DateField'
|
|
widget = widgets.DateInput
|
|
form_field_class = forms.DateField
|
|
|
|
default_error_messages = {
|
|
'invalid': _("'%s' value has an invalid date format. It must be "
|
|
"in YYYY-MM-DD format."),
|
|
'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) "
|
|
"but it is an invalid date."),
|
|
}
|
|
empty = None
|
|
|
|
def from_native(self, value):
|
|
if value in validators.EMPTY_VALUES:
|
|
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()
|
|
if isinstance(value, datetime.date):
|
|
return value
|
|
|
|
try:
|
|
parsed = parse_date(value)
|
|
if parsed is not None:
|
|
return parsed
|
|
except ValueError:
|
|
msg = self.error_messages['invalid_date'] % value
|
|
raise ValidationError(msg)
|
|
|
|
msg = self.error_messages['invalid'] % value
|
|
raise ValidationError(msg)
|
|
|
|
|
|
class DateTimeField(WritableField):
|
|
type_name = 'DateTimeField'
|
|
widget = widgets.DateTimeInput
|
|
form_field_class = forms.DateTimeField
|
|
|
|
default_error_messages = {
|
|
'invalid': _("'%s' value has an invalid format. It must be in "
|
|
"YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."),
|
|
'invalid_date': _("'%s' value has the correct format "
|
|
"(YYYY-MM-DD) but it is an invalid date."),
|
|
'invalid_datetime': _("'%s' value has the correct format "
|
|
"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
|
|
"but it is an invalid date/time."),
|
|
}
|
|
empty = None
|
|
|
|
def from_native(self, value):
|
|
if value in validators.EMPTY_VALUES:
|
|
return None
|
|
|
|
if isinstance(value, datetime.datetime):
|
|
return value
|
|
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
|
|
|
|
try:
|
|
parsed = parse_datetime(value)
|
|
if parsed is not None:
|
|
return parsed
|
|
except ValueError:
|
|
msg = self.error_messages['invalid_datetime'] % value
|
|
raise ValidationError(msg)
|
|
|
|
try:
|
|
parsed = parse_date(value)
|
|
if parsed is not None:
|
|
return datetime.datetime(parsed.year, parsed.month, parsed.day)
|
|
except ValueError:
|
|
msg = self.error_messages['invalid_date'] % value
|
|
raise ValidationError(msg)
|
|
|
|
msg = self.error_messages['invalid'] % value
|
|
raise ValidationError(msg)
|
|
|
|
|
|
class IntegerField(WritableField):
|
|
type_name = 'IntegerField'
|
|
form_field_class = forms.IntegerField
|
|
|
|
default_error_messages = {
|
|
'invalid': _('Enter a whole number.'),
|
|
'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
|
|
'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
|
|
}
|
|
|
|
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
|
|
self.max_value, self.min_value = max_value, min_value
|
|
super(IntegerField, self).__init__(*args, **kwargs)
|
|
|
|
if max_value is not None:
|
|
self.validators.append(validators.MaxValueValidator(max_value))
|
|
if min_value is not None:
|
|
self.validators.append(validators.MinValueValidator(min_value))
|
|
|
|
def from_native(self, value):
|
|
if value in validators.EMPTY_VALUES:
|
|
return None
|
|
|
|
try:
|
|
value = int(str(value))
|
|
except (ValueError, TypeError):
|
|
raise ValidationError(self.error_messages['invalid'])
|
|
return value
|
|
|
|
|
|
class FloatField(WritableField):
|
|
type_name = 'FloatField'
|
|
form_field_class = forms.FloatField
|
|
|
|
default_error_messages = {
|
|
'invalid': _("'%s' value must be a float."),
|
|
}
|
|
|
|
def from_native(self, value):
|
|
if value in validators.EMPTY_VALUES:
|
|
return None
|
|
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
msg = self.error_messages['invalid'] % value
|
|
raise ValidationError(msg)
|
|
|
|
|
|
class FileField(WritableField):
|
|
use_files = True
|
|
type_name = 'FileField'
|
|
form_field_class = forms.FileField
|
|
widget = widgets.FileInput
|
|
|
|
default_error_messages = {
|
|
'invalid': _("No file was submitted. Check the encoding type on the form."),
|
|
'missing': _("No file was submitted."),
|
|
'empty': _("The submitted file is empty."),
|
|
'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'),
|
|
'contradiction': _('Please either submit a file or check the clear checkbox, not both.')
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.max_length = kwargs.pop('max_length', None)
|
|
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
|
super(FileField, self).__init__(*args, **kwargs)
|
|
|
|
def from_native(self, data):
|
|
if data in validators.EMPTY_VALUES:
|
|
return None
|
|
|
|
# UploadedFile objects should have name and size attributes.
|
|
try:
|
|
file_name = data.name
|
|
file_size = data.size
|
|
except AttributeError:
|
|
raise ValidationError(self.error_messages['invalid'])
|
|
|
|
if self.max_length is not None and len(file_name) > self.max_length:
|
|
error_values = {'max': self.max_length, 'length': len(file_name)}
|
|
raise ValidationError(self.error_messages['max_length'] % error_values)
|
|
if not file_name:
|
|
raise ValidationError(self.error_messages['invalid'])
|
|
if not self.allow_empty_file and not file_size:
|
|
raise ValidationError(self.error_messages['empty'])
|
|
|
|
return data
|
|
|
|
def to_native(self, value):
|
|
return value.name
|
|
|
|
|
|
class ImageField(FileField):
|
|
use_files = True
|
|
form_field_class = forms.ImageField
|
|
|
|
default_error_messages = {
|
|
'invalid_image': _("Upload a valid image. The file you uploaded was "
|
|
"either not an image or a corrupted image."),
|
|
}
|
|
|
|
def from_native(self, data):
|
|
"""
|
|
Checks that the file-upload field data contains a valid image (GIF, JPG,
|
|
PNG, possibly others -- whatever the Python Imaging Library supports).
|
|
"""
|
|
f = super(ImageField, self).from_native(data)
|
|
if f is None:
|
|
return None
|
|
|
|
from compat import Image
|
|
assert Image is not None, 'PIL must be installed for ImageField support'
|
|
|
|
# We need to get a file object for PIL. We might have a path or we might
|
|
# have to read the data into memory.
|
|
if hasattr(data, 'temporary_file_path'):
|
|
file = data.temporary_file_path()
|
|
else:
|
|
if hasattr(data, 'read'):
|
|
file = BytesIO(data.read())
|
|
else:
|
|
file = BytesIO(data['content'])
|
|
|
|
try:
|
|
# load() could spot a truncated JPEG, but it loads the entire
|
|
# image in memory, which is a DoS vector. See #3848 and #18520.
|
|
# verify() must be called immediately after the constructor.
|
|
Image.open(file).verify()
|
|
except ImportError:
|
|
# Under PyPy, it is possible to import PIL. However, the underlying
|
|
# _imaging C module isn't available, so an ImportError will be
|
|
# raised. Catch and re-raise.
|
|
raise
|
|
except Exception: # Python Imaging Library doesn't recognize it as an image
|
|
raise ValidationError(self.error_messages['invalid_image'])
|
|
if hasattr(f, 'seek') and callable(f.seek):
|
|
f.seek(0)
|
|
return f
|
|
|
|
|
|
class SerializerMethodField(Field):
|
|
"""
|
|
A field that gets its value by calling a method on the serializer it's attached to.
|
|
"""
|
|
|
|
def __init__(self, method_name):
|
|
self.method_name = method_name
|
|
super(SerializerMethodField, self).__init__()
|
|
|
|
def field_to_native(self, obj, field_name):
|
|
value = getattr(self.parent, self.method_name)(obj)
|
|
return self.to_native(value)
|