Commit 4ac4676a by Tom Christie

First pass

parent 371d30aa
"""
Serializer fields perform validation on incoming data.
from rest_framework.utils import html
They are very similar to Django's form fields.
"""
from __future__ import unicode_literals
import copy
import datetime
import inspect
import re
import warnings
from decimal import Decimal, DecimalException
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.conf import settings
from django.db.models.fields import BLANK_CHOICE_DASH
from django.http import QueryDict
from django.forms import widgets
from django.utils import six, timezone
from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from rest_framework import ISO_8601
from rest_framework.compat import (
BytesIO, smart_text,
force_text, is_non_str_iterable
)
from rest_framework.settings import api_settings
def is_simple_callable(obj):
class empty:
"""
True if the object is a callable that takes no arguments.
"""
function = inspect.isfunction(obj)
method = inspect.ismethod(obj)
if not (function or method):
return False
This class is used to represent no data being provided for a given input
or output value.
args, _, _, defaults = inspect.getargspec(obj)
len_args = len(args) if function else len(args) - 1
len_defaults = len(defaults) if defaults else 0
return len_args <= len_defaults
It is required because `None` may be a valid input or output value.
"""
pass
def get_component(obj, attr_name):
def get_attribute(instance, attrs):
"""
Given an object, and an attribute name,
return that attribute on the object.
Similar to Python's built in `getattr(instance, attr)`,
but takes a list of nested attributes, instead of a single attribute.
"""
if isinstance(obj, dict):
val = obj.get(attr_name)
else:
val = getattr(obj, attr_name)
if is_simple_callable(val):
return val()
return val
def readable_datetime_formats(formats):
format = ', '.join(formats).replace(
ISO_8601,
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
)
return humanize_strptime(format)
def readable_date_formats(formats):
format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
return humanize_strptime(format)
for attr in attrs:
instance = getattr(instance, attr)
return instance
def readable_time_formats(formats):
format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
return humanize_strptime(format)
def humanize_strptime(format_string):
# Note that we're missing some of the locale specific mappings that
# don't really make sense.
mapping = {
"%Y": "YYYY",
"%y": "YY",
"%m": "MM",
"%b": "[Jan-Dec]",
"%B": "[January-December]",
"%d": "DD",
"%H": "hh",
"%I": "hh", # Requires '%p' to differentiate from '%H'.
"%M": "mm",
"%S": "ss",
"%f": "uuuuuu",
"%a": "[Mon-Sun]",
"%A": "[Monday-Sunday]",
"%p": "[AM|PM]",
"%z": "[+HHMM|-HHMM]"
}
for key, val in mapping.items():
format_string = format_string.replace(key, val)
return format_string
def strip_multiple_choice_msg(help_text):
def set_value(dictionary, keys, value):
"""
Remove the 'Hold down "control" ...' message that is Django enforces in
select multiple fields on ModelForms. (Required for 1.5 and earlier)
Similar to Python's built in `dictionary[key] = value`,
but takes a list of nested keys instead of a single key.
See https://code.djangoproject.com/ticket/9321
set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2}
set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2}
set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}}
"""
multiple_choice_msg = _(' Hold down "Control", or "Command" on a Mac, to select more than one.')
multiple_choice_msg = force_text(multiple_choice_msg)
if not keys:
dictionary.update(value)
return
return help_text.replace(multiple_choice_msg, '')
for key in keys[:-1]:
if key not in dictionary:
dictionary[key] = {}
dictionary = dictionary[key]
dictionary[keys[-1]] = value
class Field(object):
read_only = True
creation_counter = 0
empty = ''
type_name = None
partial = False
use_files = False
form_field_class = forms.CharField
type_label = 'field'
widget = None
def __init__(self, source=None, label=None, help_text=None):
self.parent = None
class ValidationError(Exception):
pass
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
self.source = source
class SkipField(Exception):
pass
if label is not None:
self.label = smart_text(label)
else:
self.label = None
if help_text is not None:
self.help_text = strip_multiple_choice_msg(smart_text(help_text))
else:
self.help_text = None
class Field(object):
_creation_counter = 0
self._errors = []
self._value = None
self._name = None
MESSAGES = {
'required': 'This field is required.'
}
@property
def errors(self):
return self._errors
_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`'
_MISSING_ERROR_MESSAGE = (
'ValidationError raised by `{class_name}`, but error key `{key}` does '
'not exist in the `MESSAGES` dictionary.'
)
def widget_html(self):
if not self.widget:
return ''
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
label=None, style=None):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
attrs = {}
if 'id' not in self.widget.attrs:
attrs['id'] = self._name
# If `required` is unset, then use `True` unless a default is provided.
if required is None:
required = default is empty and not read_only
return self.widget.render(self._name, self._value, attrs=attrs)
# Some combinations of keyword arguments do not make sense.
assert not (read_only and write_only), self._NOT_READ_ONLY_WRITE_ONLY
assert not (read_only and required), self._NOT_READ_ONLY_REQUIRED
assert not (read_only and default is not empty), self._NOT_READ_ONLY_DEFAULT
assert not (required and default is not empty), self._NOT_REQUIRED_DEFAULT
def label_tag(self):
return '<label for="%s">%s:</label>' % (self._name, self.label)
self.read_only = read_only
self.write_only = write_only
self.required = required
self.default = default
self.source = source
self.initial = initial
self.label = label
self.style = {} if style is None else style
def initialize(self, parent, field_name):
def bind(self, field_name, parent, root):
"""
Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer.
field_name - The name of the field being initialized.
Setup the context for the field instance.
"""
self.field_name = field_name
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
self.root = root
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
# `self.label` should deafult to being based on the field name.
if self.label is None:
self.label = self.field_name.replace('_', ' ').capitalize()
def field_to_native(self, obj, field_name):
"""
Given an object and a field name, returns the value that should be
serialized for that field.
"""
if obj is None:
return self.empty
# self.source should default to being the same as the field name.
if self.source is None:
self.source = field_name
# self.source_attrs is a list of attributes that need to be looked up
# when serializing the instance, or populating the validated data.
if self.source == '*':
return self.to_native(obj)
source = self.source or field_name
value = obj
for component in source.split('.'):
value = get_component(value, component)
if value is None:
break
return self.to_native(value)
self.source_attrs = []
else:
self.source_attrs = self.source.split('.')
def to_native(self, value):
def get_initial(self):
"""
Converts the field's value into it's simple representation.
Return a value to use when the field is being returned as a primative
value, without any object instance.
"""
if is_simple_callable(value):
value = value()
if is_protected_type(value):
return value
elif (is_non_str_iterable(value) and
not isinstance(value, (dict, six.string_types))):
return [self.to_native(item) for item in value]
elif isinstance(value, dict):
# Make sure we preserve field ordering, if it exists
ret = SortedDict()
for key, val in value.items():
ret[key] = self.to_native(val)
return ret
return force_text(value)
return self.initial
def attributes(self):
def get_value(self, dictionary):
"""
Returns a dictionary of attributes to be used when serializing to xml.
Given the *incoming* primative data, return the value for this field
that should be validated and transformed to a native value.
"""
if self.type_name:
return {'type': self.type_name}
return {}
def metadata(self):
metadata = SortedDict()
metadata['type'] = self.type_label
metadata['required'] = getattr(self, 'required', False)
optional_attrs = ['read_only', 'label', 'help_text',
'min_length', 'max_length']
for attr in optional_attrs:
value = getattr(self, attr, None)
if value is not None and value != '':
metadata[attr] = force_text(value, strings_only=True)
return metadata
class WritableField(Field):
"""
Base for read/write fields.
"""
write_only = False
default_validators = []
default_error_messages = {
'required': _('This field is required.'),
'invalid': _('Invalid value.'),
}
widget = widgets.TextInput
default = None
def __init__(self, source=None, label=None, help_text=None,
read_only=False, write_only=False, required=None,
validators=[], error_messages=None, widget=None,
default=None, blank=None):
super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
self.read_only = read_only
self.write_only = write_only
assert not (read_only and write_only), "Cannot set read_only=True and write_only=True"
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
return dictionary.get(self.field_name, empty)
self.validators = self.default_validators + validators
self.default = default if default is not None else self.default
# Widgets are only used for HTML forms.
widget = widget or self.widget
if isinstance(widget, type):
widget = widget()
self.widget = widget
def get_attribute(self, instance):
"""
Given the *outgoing* object instance, return the value for this field
that should be returned as a primative value.
"""
return get_attribute(instance, self.source_attrs)
def __deepcopy__(self, memo):
result = copy.copy(self)
memo[id(self)] = result
result.validators = self.validators[:]
return result
def get_default(self):
"""
Return the default value to use when validating data if no input
is provided for this field.
def get_default_value(self):
if is_simple_callable(self.default):
return self.default()
If a default has not been set for this field then this will simply
return `empty`, indicating that no value should be set in the
validated data for this field.
"""
if self.default is empty:
raise SkipField()
return self.default
def validate(self, value):
if value in validators.EMPTY_VALUES and self.required:
raise ValidationError(self.error_messages['required'])
def validate(self, data=empty):
"""
Validate a simple representation and return the internal value.
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)
The provided data may be `empty` if no representation was included.
May return `empty` if the field should not be included in the
validated data.
"""
if data is empty:
if self.required:
self.fail('required')
return self.get_default()
def field_to_native(self, obj, field_name):
if self.write_only:
return None
return super(WritableField, self).field_to_native(obj, field_name)
return self.to_native(data)
def field_from_native(self, data, files, field_name, into):
def to_native(self, data):
"""
Given a dictionary and a field name, updates the dictionary `into`,
with the field and it's deserialized value.
Transform the *incoming* primative data into a native value.
"""
if self.read_only:
return
try:
data = data or {}
if self.use_files:
files = files or {}
try:
native = files[field_name]
except KeyError:
native = data[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.get_default_value()
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
return data
def from_native(self, value):
def to_primative(self, value):
"""
Reverts a simple representation back to the field's value.
Transform the *outgoing* native value into primative data.
"""
return value
class ModelField(WritableField):
"""
A generic field that can be used against an arbitrary model field.
"""
def __init__(self, *args, **kwargs):
def fail(self, key, **kwargs):
"""
A helper method that simply raises a validation error.
"""
try:
self.model_field = kwargs.pop('model_field')
raise ValidationError(self.MESSAGES[key].format(**kwargs))
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))
self.min_value = kwargs.pop('min_value',
getattr(self.model_field, 'min_value', None))
self.max_value = kwargs.pop('max_value',
getattr(self.model_field, 'max_value', 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))
if self.min_value is not None:
self.validators.append(validators.MinValueValidator(self.min_value))
if self.max_value is not None:
self.validators.append(validators.MaxValueValidator(self.max_value))
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)
class_name = self.__class__.__name__
msg = self._MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
raise AssertionError(msg)
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'
type_label = 'boolean'
form_field_class = forms.BooleanField
widget = widgets.CheckboxInput
default_error_messages = {
'invalid': _("'%s' value must be either True or False."),
class BooleanField(Field):
MESSAGES = {
'required': 'This field is required.',
'invalid_value': '`{input}` is not a valid boolean.'
}
empty = False
def field_from_native(self, data, files, field_name, into):
# HTML checkboxes do not explicitly represent unchecked as `False`
# we deal with that here...
if isinstance(data, QueryDict) and self.default is None:
self.default = False
return super(BooleanField, self).field_from_native(
data, files, field_name, into
)
def from_native(self, value):
if value in ('true', 't', 'True', '1'):
TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
FALSE_VALUES = {'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_native(self, data):
if data in self.TRUE_VALUES:
return True
if value in ('false', 'f', 'False', '0'):
elif data in self.FALSE_VALUES:
return False
return bool(value)
self.fail('invalid_value', input=data)
class CharField(WritableField):
type_name = 'CharField'
type_label = 'string'
form_field_class = forms.CharField
class CharField(Field):
MESSAGES = {
'required': 'This field is required.',
'blank': 'This field may not be blank.'
}
def __init__(self, max_length=None, min_length=None, allow_none=False, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length
self.allow_none = allow_none
def __init__(self, *args, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
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):
return value
if value is None and not self.allow_none:
return ''
return smart_text(value)
class URLField(CharField):
type_name = 'URLField'
type_label = 'url'
def __init__(self, **kwargs):
if 'validators' not in kwargs:
kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs)
def to_native(self, data):
if data == '' and not self.allow_blank:
self.fail('blank')
return str(data)
class SlugField(CharField):
type_name = 'SlugField'
type_label = 'slug'
form_field_class = forms.SlugField
default_error_messages = {
'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
" underscores or hyphens."),
class ChoiceField(Field):
MESSAGES = {
'required': 'This field is required.',
'invalid_choice': '`{input}` is not a valid choice.'
}
default_validators = [validators.validate_slug]
coerce_to_type = str
def __init__(self, *args, **kwargs):
super(SlugField, self).__init__(*args, **kwargs)
choices = kwargs.pop('choices')
assert choices, '`choices` argument is required and may not be empty'
# Allow either single or paired choices style:
# choices = [1, 2, 3]
# choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
pairs = [
isinstance(item, (list, tuple)) and len(item) == 2
for item in choices
]
if all(pairs):
self.choices = {key: val for key, val in choices}
else:
self.choices = {item: item for item in choices}
class ChoiceField(WritableField):
type_name = 'ChoiceField'
type_label = 'choice'
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.'),
}
# Map the string representation of choices to the underlying value.
# 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 = {
str(key): key for key in self.choices.keys()
}
def __init__(self, choices=(), blank_display_value=None, *args, **kwargs):
self.empty = kwargs.pop('empty', '')
super(ChoiceField, self).__init__(*args, **kwargs)
self.choices = choices
if not self.required:
if blank_display_value is None:
blank_choice = BLANK_CHOICE_DASH
else:
blank_choice = [('', blank_display_value)]
self.choices = blank_choice + self.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 metadata(self):
data = super(ChoiceField, self).metadata()
data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices]
return data
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
def from_native(self, value):
value = super(ChoiceField, self).from_native(value)
if value == self.empty or value in validators.EMPTY_VALUES:
return self.empty
return value
class EmailField(CharField):
type_name = 'EmailField'
type_label = 'email'
form_field_class = forms.EmailField
default_error_messages = {
'invalid': _('Enter a valid email 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()
class RegexField(CharField):
type_name = 'RegexField'
type_label = 'regex'
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)
class DateField(WritableField):
type_name = 'DateField'
type_label = 'date'
widget = widgets.DateInput
form_field_class = forms.DateField
default_error_messages = {
'invalid': _("Date has wrong format. Use one of these formats instead: %s"),
}
empty = None
input_formats = api_settings.DATE_INPUT_FORMATS
format = api_settings.DATE_FORMAT
def __init__(self, input_formats=None, format=None, *args, **kwargs):
self.input_formats = input_formats if input_formats is not None else self.input_formats
self.format = format if format is not None else self.format
super(DateField, self).__init__(*args, **kwargs)
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
for format in self.input_formats:
if format.lower() == ISO_8601:
try:
parsed = parse_date(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None:
return parsed
else:
try:
parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
return parsed.date()
msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats)
raise ValidationError(msg)
def to_native(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()
return value.strftime(self.format)
class DateTimeField(WritableField):
type_name = 'DateTimeField'
type_label = 'datetime'
widget = widgets.DateTimeInput
form_field_class = forms.DateTimeField
default_error_messages = {
'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"),
}
empty = None
input_formats = api_settings.DATETIME_INPUT_FORMATS
format = api_settings.DATETIME_FORMAT
def __init__(self, input_formats=None, format=None, *args, **kwargs):
self.input_formats = input_formats if input_formats is not None else self.input_formats
self.format = format if format is not None else self.format
super(DateTimeField, self).__init__(*args, **kwargs)
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
for format in self.input_formats:
if format.lower() == ISO_8601:
try:
parsed = parse_datetime(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None:
return parsed
else:
try:
parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
return parsed
msg = self.error_messages['invalid'] % readable_datetime_formats(self.input_formats)
raise ValidationError(msg)
def to_native(self, value):
if value is None or self.format is None:
return value
if self.format.lower() == ISO_8601:
ret = value.isoformat()
if ret.endswith('+00:00'):
ret = ret[:-6] + 'Z'
return ret
return value.strftime(self.format)
class TimeField(WritableField):
type_name = 'TimeField'
type_label = 'time'
widget = widgets.TimeInput
form_field_class = forms.TimeField
default_error_messages = {
'invalid': _("Time has wrong format. Use one of these formats instead: %s"),
}
empty = None
input_formats = api_settings.TIME_INPUT_FORMATS
format = api_settings.TIME_FORMAT
def __init__(self, input_formats=None, format=None, *args, **kwargs):
self.input_formats = input_formats if input_formats is not None else self.input_formats
self.format = format if format is not None else self.format
super(TimeField, self).__init__(*args, **kwargs)
def from_native(self, value):
if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.time):
return value
for format in self.input_formats:
if format.lower() == ISO_8601:
try:
parsed = parse_time(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None:
return parsed
else:
try:
parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
return parsed.time()
msg = self.error_messages['invalid'] % readable_time_formats(self.input_formats)
raise ValidationError(msg)
def to_native(self, value):
if value is None or self.format is None:
return value
if isinstance(value, datetime.datetime):
value = value.time()
if self.format.lower() == ISO_8601:
return value.isoformat()
return value.strftime(self.format)
class IntegerField(WritableField):
type_name = 'IntegerField'
type_label = 'integer'
form_field_class = forms.IntegerField
empty = 0
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'
type_label = 'float'
form_field_class = forms.FloatField
empty = 0
default_error_messages = {
'invalid': _("'%s' value must be a float."),
}
def from_native(self, value):
if value in validators.EMPTY_VALUES:
return None
def to_native(self, data):
try:
return float(value)
except (TypeError, ValueError):
msg = self.error_messages['invalid'] % value
raise ValidationError(msg)
return self.choice_strings_to_values[str(data)]
except KeyError:
self.fail('invalid_choice', input=data)
class DecimalField(WritableField):
type_name = 'DecimalField'
type_label = 'decimal'
form_field_class = forms.DecimalField
empty = Decimal('0')
default_error_messages = {
'invalid': _('Enter a 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.'),
'max_digits': _('Ensure that there are no more than %s digits in total.'),
'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
class MultipleChoiceField(ChoiceField):
MESSAGES = {
'required': 'This field is required.',
'invalid_choice': '`{input}` is not a valid choice.',
'not_a_list': 'Expected a list of items but got type `{input_type}`'
}
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
self.max_digits, self.decimal_places = max_digits, decimal_places
super(DecimalField, 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):
"""
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.
"""
if value in validators.EMPTY_VALUES:
return None
value = smart_text(value).strip()
try:
value = Decimal(value)
except DecimalException:
raise ValidationError(self.error_messages['invalid'])
return value
def validate(self, value):
super(DecimalField, self).validate(value)
if value in validators.EMPTY_VALUES:
return
# Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
# since it is never equal to itself. However, NaN is the only value that
# isn't equal to itself, so we can use this to identify NaN
if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
raise ValidationError(self.error_messages['invalid'])
sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent)
# digittuple doesn't include any leading zeros.
digits = len(digittuple)
if decimals > digits:
# We have leading zeros up to or past the decimal point. Count
# everything past the decimal point as a digit. We do not count
# 0 before the decimal point as a digit since that would mean
# we would not allow max_digits = decimal_places.
digits = decimals
whole_digits = digits - decimals
if self.max_digits is not None and digits > self.max_digits:
raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
if self.decimal_places is not None and decimals > self.decimal_places:
raise ValidationError(self.error_messages['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):
raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
return value
def to_native(self, data):
if not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
return set([
super(MultipleChoiceField, self).to_native(item)
for item in data
])
class FileField(WritableField):
use_files = True
type_name = 'FileField'
type_label = 'file upload'
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.')
class IntegerField(Field):
MESSAGES = {
'required': 'This field is required.',
'invalid_integer': 'A valid integer is required.'
}
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.
def to_native(self, data):
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'])
data = int(str(data))
except (ValueError, TypeError):
self.fail('invalid_integer')
return data
def to_native(self, value):
return value.name
class ImageField(FileField):
use_files = True
type_name = 'ImageField'
type_label = 'image upload'
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 rest_framework.compat import Image
assert Image is not None, 'Either Pillow or 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, *args, **kwargs):
self.method_name = method_name
super(SerializerMethodField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
value = getattr(self.parent, self.method_name)(obj)
return self.to_native(value)
class MethodField(Field):
def __init__(self, **kwargs):
kwargs['source'] = '*'
kwargs['read_only'] = True
super(MethodField, self).__init__(**kwargs)
def to_primative(self, value):
attr = 'get_{field_name}'.format(field_name=self.field_name)
method = getattr(self.parent, attr)
return method(value)
......@@ -79,18 +79,16 @@ class GenericAPIView(views.APIView):
'view': self
}
def get_serializer(self, instance=None, data=None, files=None, many=False,
partial=False, allow_add_remove=False):
def get_serializer(self, instance=None, data=None, many=False, partial=False):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
return serializer_class(instance, data=data, files=files,
many=many, partial=partial,
allow_add_remove=allow_add_remove,
context=context)
return serializer_class(
instance, data=data, many=many, partial=partial, context=context
)
def get_pagination_serializer(self, page):
"""
......
......@@ -36,12 +36,10 @@ class CreateModelMixin(object):
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
serializer = self.get_serializer(data=request.DATA)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
self.object = serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
......@@ -90,26 +88,20 @@ class UpdateModelMixin(object):
partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none()
serializer = self.get_serializer(self.object, data=request.DATA,
files=request.FILES, partial=partial)
serializer = self.get_serializer(self.object, data=request.DATA, partial=partial)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
self.pre_save(serializer.object)
except ValidationError as err:
# full_clean on model instance may be called in pre_save,
# so we have to handle eventual errors.
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extras = {self.lookup_field: lookup_value}
if self.object is None:
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
self.object = serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_201_CREATED)
self.object = serializer.save(force_update=True)
self.post_save(self.object, created=False)
self.object = serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs):
......
......@@ -48,17 +48,17 @@ class DefaultObjectSerializer(serializers.Field):
super(DefaultObjectSerializer, self).__init__(source=source)
class PaginationSerializerOptions(serializers.SerializerOptions):
"""
An object that stores the options that may be provided to a
pagination serializer by using the inner `Meta` class.
# class PaginationSerializerOptions(serializers.SerializerOptions):
# """
# An object that stores the options that may be provided to a
# pagination serializer by using the inner `Meta` class.
Accessible on the instance as `serializer.opts`.
"""
def __init__(self, meta):
super(PaginationSerializerOptions, self).__init__(meta)
self.object_serializer_class = getattr(meta, 'object_serializer_class',
DefaultObjectSerializer)
# Accessible on the instance as `serializer.opts`.
# """
# def __init__(self, meta):
# super(PaginationSerializerOptions, self).__init__(meta)
# self.object_serializer_class = getattr(meta, 'object_serializer_class',
# DefaultObjectSerializer)
class BasePaginationSerializer(serializers.Serializer):
......@@ -66,7 +66,7 @@ class BasePaginationSerializer(serializers.Serializer):
A base class for pagination serializers to inherit from,
to make implementing custom serializers more easy.
"""
_options_class = PaginationSerializerOptions
# _options_class = PaginationSerializerOptions
results_field = 'results'
def __init__(self, *args, **kwargs):
......
"""
Serializer fields that deal with relationships.
These fields allow you to specify the style that should be used to represent
model relationships, including hyperlinks, primary keys, or slugs.
"""
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
from rest_framework.reverse import reverse
from rest_framework.compat import urlparse
from rest_framework.compat import smart_text
# Relational fields
# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
"""
Base class for related model fields.
This represents a relationship using the unicode representation of the target.
"""
widget = widgets.Select
many_widget = widgets.SelectMultiple
form_field_class = forms.ChoiceField
many_form_field_class = forms.MultipleChoiceField
null_values = (None, '', 'None')
cache_choices = False
empty_label = None
read_only = True
many = False
def __init__(self, *args, **kwargs):
queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many)
if self.many:
self.widget = self.many_widget
self.form_field_class = self.many_form_field_class
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
super(RelatedField, self).__init__(*args, **kwargs)
if not self.required:
# Accessed in ModelChoiceIterator django/forms/models.py:1034
# If set adds empty choice.
self.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only:
manager = getattr(self.parent.opts.model, self.source or field_name)
if hasattr(manager, 'related'): # Forward
self.queryset = manager.related.model._default_manager.all()
else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all()
# We need this stuff to make form choices work...
def prepare_value(self, obj):
return self.to_native(obj)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
queryset = property(_get_queryset, _set_queryset)
def _get_choices(self):
# If self._choices is set, then somebody must have manually set
# the property self.choices. In this case, just return self._choices.
if hasattr(self, '_choices'):
return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
# time _get_choices() is called (and, thus, each time self.choices is
# accessed) so that we can ensure the QuerySet has not been consumed. This
# construct might look complicated but it allows for lazy evaluation of
# the queryset.
return ModelChoiceIterator(self)
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)
# Default value handling
def get_default_value(self):
default = super(RelatedField, self).get_default_value()
if self.many and default is None:
return []
return default
# Regular serializer stuff...
def field_to_native(self, obj, field_name):
try:
if self.source == '*':
return self.to_native(obj)
source = self.source or field_name
value = obj
for component in source.split('.'):
if value is None:
break
value = get_component(value, component)
except ObjectDoesNotExist:
return None
if value is None:
return None
if self.many:
if is_simple_callable(getattr(value, 'all', None)):
return [self.to_native(item) for item in value.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item) for item in value]
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
try:
if self.many:
try:
# Form data
value = data.getlist(field_name)
if value == [''] or value == []:
raise KeyError
except AttributeError:
# Non-form data
value = data[field_name]
else:
value = data[field_name]
except KeyError:
if self.partial:
return
value = self.get_default_value()
if value in self.null_values:
if self.required:
raise ValidationError(self.error_messages['required'])
into[(self.source or field_name)] = None
elif self.many:
into[(self.source or field_name)] = [self.from_native(item) for item in value]
else:
into[(self.source or field_name)] = self.from_native(value)
# PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField):
"""
Represents a relationship as a pk value.
"""
read_only = False
default_error_messages = {
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
}
# TODO: Remove these field hacks...
def prepare_value(self, obj):
return self.to_native(obj.pk)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj.pk))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
# TODO: Possibly change this to just take `obj`, through prob less performant
def to_native(self, pk):
return pk
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
msg = self.error_messages['does_not_exist'] % smart_text(data)
raise ValidationError(msg)
except (TypeError, ValueError):
received = type(data).__name__
msg = self.error_messages['incorrect_type'] % received
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
if self.many:
# To-many relationship
queryset = None
if not self.source:
# Prefer obj.serializable_value for performance reasons
try:
queryset = obj.serializable_value(field_name)
except AttributeError:
pass
if queryset is None:
# RelatedManager (reverse relationship)
source = self.source or field_name
queryset = obj
for component in source.split('.'):
if queryset is None:
return []
queryset = get_component(queryset, component)
# Forward relationship
if is_simple_callable(getattr(queryset, 'all', None)):
return [self.to_native(item.pk) for item in queryset.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item.pk) for item in queryset]
# To-one relationship
try:
# Prefer obj.serializable_value for performance reasons
pk = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedObject (reverse relationship)
try:
pk = getattr(obj, self.source or field_name).pk
except (ObjectDoesNotExist, AttributeError):
return None
# Forward relationship
return self.to_native(pk)
# Slug relationships
class SlugRelatedField(RelatedField):
"""
Represents a relationship using a unique field on the target.
"""
read_only = False
default_error_messages = {
'does_not_exist': _("Object with %s=%s does not exist."),
'invalid': _('Invalid value.'),
}
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
assert self.slug_field, 'slug_field is required'
super(SlugRelatedField, self).__init__(*args, **kwargs)
def to_native(self, obj):
return getattr(obj, self.slug_field)
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(**{self.slug_field: data})
except ObjectDoesNotExist:
raise ValidationError(self.error_messages['does_not_exist'] %
(self.slug_field, smart_text(data)))
except (TypeError, ValueError):
msg = self.error_messages['invalid']
raise ValidationError(msg)
# Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
Represents a relationship using hyperlinking.
"""
read_only = False
lookup_field = 'pk'
default_error_messages = {
'no_match': _('Invalid hyperlink - No URL match'),
'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
'configuration_error': _('Invalid hyperlink due to configuration error'),
'does_not_exist': _("Invalid hyperlink - object does not exist."),
'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
}
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except KeyError:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
lookup_field = getattr(obj, self.lookup_field)
kwargs = {self.lookup_field: lookup_field}
return reverse(view_name, kwargs=kwargs, request=request, format=format)
def get_object(self, queryset, view_name, view_args, view_kwargs):
"""
Return the object corresponding to a matched URL.
Takes the matched URL conf arguments, and the queryset, and should
return an object instance, or raise an `ObjectDoesNotExist` exception.
"""
lookup_value = view_kwargs[self.lookup_field]
filter_kwargs = {self.lookup_field: lookup_value}
return queryset.get(**filter_kwargs)
def to_native(self, obj):
view_name = self.view_name
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
assert request is not None, (
"`HyperlinkedRelatedField` requires the request in the serializer "
"context. Add `context={'request': request}` when instantiating "
"the serializer."
)
# If the object has not yet been saved then we cannot hyperlink to it.
if getattr(obj, 'pk', None) is None:
return
# Return the hyperlink, or error if incorrectly configured.
try:
return self.get_url(obj, view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
'view name "%s". You may have failed to include the related '
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
raise Exception(msg % view_name)
def from_native(self, value):
# Convert URL -> model instance pk
# TODO: Use values_list
queryset = self.queryset
if queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
msg = self.error_messages['incorrect_type']
raise ValidationError(msg % type(value).__name__)
if http_prefix:
# If needed convert absolute URLs to relative path
value = urlparse.urlparse(value).path
prefix = get_script_prefix()
if value.startswith(prefix):
value = '/' + value[len(prefix):]
try:
match = resolve(value)
except Exception:
raise ValidationError(self.error_messages['no_match'])
if match.view_name != self.view_name:
raise ValidationError(self.error_messages['incorrect_match'])
try:
return self.get_object(queryset, match.view_name,
match.args, match.kwargs)
except (ObjectDoesNotExist, TypeError, ValueError):
raise ValidationError(self.error_messages['does_not_exist'])
class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
"""
lookup_field = 'pk'
read_only = True
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except KeyError:
msg = "HyperlinkedIdentityField requires 'view_name' argument"
raise ValueError(msg)
self.format = kwargs.pop('format', None)
lookup_field = kwargs.pop('lookup_field', None)
self.lookup_field = lookup_field or self.lookup_field
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
format = self.context.get('format', None)
view_name = self.view_name
assert request is not None, (
"`HyperlinkedIdentityField` requires the request in the serializer"
" context. Add `context={'request': request}` when instantiating "
"the serializer."
)
# By default use whatever format is given for the current context
# unless the target is a different type to the source.
#
# Eg. Consider a HyperlinkedIdentityField pointing from a json
# representation to an html property of that representation...
#
# '/snippets/1/' should link to '/snippets/1/highlight/'
# ...but...
# '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
if format and self.format and self.format != format:
format = self.format
# Return the hyperlink, or error if incorrectly configured.
try:
return self.get_url(obj, view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
'view name "%s". You may have failed to include the related '
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
raise Exception(msg % view_name)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
lookup_field = getattr(obj, self.lookup_field, None)
kwargs = {self.lookup_field: lookup_field}
# Handle unsaved object case
if lookup_field is None:
return None
return reverse(view_name, kwargs=kwargs, request=request, format=format)
......@@ -458,7 +458,7 @@ class BrowsableAPIRenderer(BaseRenderer):
):
return
serializer = view.get_serializer(instance=obj, data=data, files=files)
serializer = view.get_serializer(instance=obj, data=data)
serializer.is_valid()
data = serializer.data
......@@ -579,10 +579,10 @@ class BrowsableAPIRenderer(BaseRenderer):
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
'response_headers': response_headers,
'put_form': self.get_rendered_html_form(view, 'PUT', request),
'post_form': self.get_rendered_html_form(view, 'POST', request),
'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
#'put_form': self.get_rendered_html_form(view, 'PUT', request),
#'post_form': self.get_rendered_html_form(view, 'POST', request),
#'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
#'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form,
......
......@@ -10,21 +10,14 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
from __future__ import unicode_literals
import copy
import datetime
import inspect
import types
from decimal import Decimal
from django.contrib.contenttypes.generic import GenericForeignKey
from django.core.paginator import Page
from django.db import models
from django.forms import widgets
from django.utils import six
from django.utils.datastructures import SortedDict
from django.core.exceptions import ObjectDoesNotExist
from collections import namedtuple, OrderedDict
from rest_framework.fields import empty, set_value, Field, SkipField, ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html
import copy
import inspect
# Note: We do the following so that users of the framework can use this style:
#
......@@ -37,635 +30,339 @@ from rest_framework.relations import * # NOQA
from rest_framework.fields import * # NOQA
def _resolve_model(obj):
"""
Resolve supplied `obj` to a Django model class.
FieldResult = namedtuple('FieldResult', ['field', 'value', 'error'])
`obj` must be a Django model class itself, or a string
representation of one. Useful in situtations like GH #1225 where
Django may not have resolved a string-based reference to a model in
another model's foreign key definition.
String representations should have the format:
'appname.ModelName'
"""
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.')
return models.get_model(app_name, model_name)
elif inspect.isclass(obj) and issubclass(obj, models.Model):
return obj
else:
raise ValueError("{0} is not a Django model".format(obj))
def pretty_name(name):
"""Converts 'first_name' to 'First name'"""
if not name:
return ''
return name.replace('_', ' ').capitalize()
class BaseSerializer(Field):
def __init__(self, instance=None, data=None, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.instance = instance
self._initial_data = data
class RelationsList(list):
_deleted = []
def to_native(self, data):
raise NotImplementedError()
def to_primative(self, instance):
raise NotImplementedError()
class NestedValidationError(ValidationError):
"""
The default ValidationError behavior is to stringify each item in the list
if the messages are a list of error messages.
def update(self, instance):
raise NotImplementedError()
In the case of nested serializers, where the parent has many children,
then the child's `serializer.errors` will be a list of dicts. In the case
of a single child, the `serializer.errors` will be a dict.
def create(self):
raise NotImplementedError()
We need to override the default behavior to get properly nested error dicts.
"""
def save(self, extras=None):
if extras is not None:
self._validated_data.update(extras)
def __init__(self, message):
if isinstance(message, dict):
self._messages = [message]
if self.instance is not None:
self.update(self.instance)
else:
self._messages = message
@property
def messages(self):
return self._messages
self.instance = self.create()
return self.instance
class DictWithMetadata(dict):
"""
A dict-like object, that can have additional properties attached.
"""
def __getstate__(self):
"""
Used by pickle (e.g., caching).
Overridden to remove the metadata from the dict, since it shouldn't be
pickled and may in some instances be unpickleable.
"""
return dict(self)
def is_valid(self):
try:
self._validated_data = self.to_native(self._initial_data)
except ValidationError as exc:
self._validated_data = {}
self._errors = exc.args[0]
return False
self._errors = {}
return True
class SortedDictWithMetadata(SortedDict):
"""
A sorted dict-like object, that can have additional properties attached.
"""
def __getstate__(self):
"""
Used by pickle (e.g., caching).
Overriden to remove the metadata from the dict, since it shouldn't be
pickle and may in some instances be unpickleable.
"""
return SortedDict(self).__dict__
@property
def data(self):
if not hasattr(self, '_data'):
if self.instance is not None:
self._data = self.to_primative(self.instance)
elif self._initial_data is not None:
self._data = {
field_name: field.get_value(self._initial_data)
for field_name, field in self.fields.items()
}
else:
self._data = self.get_initial()
return self._data
@property
def errors(self):
if not hasattr(self, '_errors'):
msg = 'You must call `.is_valid()` before accessing `.errors`.'
raise AssertionError(msg)
return self._errors
def _is_protected_type(obj):
"""
True if the object is a native datatype that does not need to
be serialized further.
"""
return isinstance(obj, (
types.NoneType,
int, long,
datetime.datetime, datetime.date, datetime.time,
float, Decimal,
basestring)
)
@property
def validated_data(self):
if not hasattr(self, '_validated_data'):
msg = 'You must call `.is_valid()` before accessing `.validated_data`.'
raise AssertionError(msg)
return self._validated_data
def _get_declared_fields(bases, attrs):
class SerializerMetaclass(type):
"""
Create a list of serializer field instances from the passed in 'attrs',
plus any fields on the base classes (in 'bases').
This metaclass sets a dictionary named `base_fields` on the class.
Note that all fields from the base classes are used.
Any fields included as attributes on either the class or it's superclasses
will be include in the `base_fields` dictionary.
"""
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(six.iteritems(attrs))
if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1].creation_counter)
# If this class is subclassing another Serializer, add that Serializer's
# fields. Note that we loop over the bases in *reverse*. This is necessary
# in order to maintain the correct order of fields.
for base in bases[::-1]:
if hasattr(base, 'base_fields'):
fields = list(base.base_fields.items()) + fields
@classmethod
def _get_fields(cls, bases, attrs):
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(attrs.items())
if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
return SortedDict(fields)
# If this class is subclassing another Serializer, add that Serializer's
# fields. Note that we loop over the bases in *reverse*. This is necessary
# in order to maintain the correct order of fields.
for base in bases[::-1]:
if hasattr(base, 'base_fields'):
fields = list(base.base_fields.items()) + fields
return OrderedDict(fields)
class SerializerMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['base_fields'] = _get_declared_fields(bases, attrs)
attrs['base_fields'] = cls._get_fields(bases, attrs)
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
class SerializerOptions(object):
"""
Meta class options for Serializer
"""
def __init__(self, meta):
self.depth = getattr(meta, 'depth', 0)
self.fields = getattr(meta, 'fields', ())
self.exclude = getattr(meta, 'exclude', ())
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
def __new__(cls, *args, **kwargs):
many = kwargs.pop('many', False)
if many:
class DynamicListSerializer(ListSerializer):
child = cls()
return DynamicListSerializer(*args, **kwargs)
return super(Serializer, cls).__new__(cls)
class BaseSerializer(WritableField):
"""
This is the Serializer implementation.
We need to implement it as `BaseSerializer` due to metaclass magicks.
"""
class Meta(object):
pass
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata
def __init__(self, instance=None, data=None, files=None,
context=None, partial=False, many=False,
allow_add_remove=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
self.parent = None
self.root = None
self.partial = partial
self.many = many
self.allow_add_remove = allow_add_remove
def __init__(self, *args, **kwargs):
kwargs.pop('context', None)
kwargs.pop('partial', None)
kwargs.pop('many', False)
self.context = context or {}
super(Serializer, self).__init__(*args, **kwargs)
self.init_data = data
self.init_files = files
self.object = instance
# Every new serializer is created with a clone of the field instances.
# This allows users to dynamically modify the fields on a serializer
# instance without affecting every other serializer class.
self.fields = self.get_fields()
self._data = None
self._files = None
self._errors = None
if many and instance is not None and not hasattr(instance, '__iter__'):
raise ValueError('instance should be a queryset or other iterable with many=True')
if allow_add_remove and not many:
raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True')
#####
# Methods to determine which fields to use when (de)serializing objects.
def get_default_fields(self):
"""
Return the complete set of default fields for the object, as a dict.
"""
return {}
def get_fields(self):
"""
Returns the complete set of fields for the object as a dict.
This will be the set of any explicitly declared fields,
plus the set of fields returned by get_default_fields().
"""
ret = SortedDict()
# Get the explicitly declared fields
base_fields = copy.deepcopy(self.base_fields)
for key, field in base_fields.items():
ret[key] = field
# Add in the default fields
default_fields = self.get_default_fields()
for key, val in default_fields.items():
if key not in ret:
ret[key] = val
# If 'fields' is specified, use those fields, in that order.
if self.opts.fields:
assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple'
new = SortedDict()
for key in self.opts.fields:
new[key] = ret[key]
ret = new
# Remove anything in 'exclude'
if self.opts.exclude:
assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple'
for key in self.opts.exclude:
ret.pop(key, None)
for key, field in ret.items():
field.initialize(parent=self, field_name=key)
return ret
#####
# Methods to convert or revert from objects <--> primitive representations.
def get_field_key(self, field_name):
"""
Return the key that should be used for a given field.
"""
return field_name
def restore_fields(self, data, files):
"""
Core of deserialization, together with `restore_object`.
Converts a dictionary of data into a dictionary of deserialized fields.
"""
reverted_data = {}
if data is not None and not isinstance(data, dict):
self._errors['non_field_errors'] = ['Invalid data']
return None
# Setup all the child fields, to provide them with the current context.
for field_name, field in self.fields.items():
field.initialize(parent=self, field_name=field_name)
try:
field.field_from_native(data, files, field_name, reverted_data)
except ValidationError as err:
self._errors[field_name] = list(err.messages)
field.bind(field_name, self, self)
return reverted_data
def get_fields(self):
return copy.deepcopy(self.base_fields)
def perform_validation(self, attrs):
"""
Run `validate_<fieldname>()` and `validate()` methods on the serializer
"""
def bind(self, field_name, parent, root):
# If the serializer is used as a field then when it becomes bound
# it also needs to bind all its child fields.
super(Serializer, self).bind(field_name, parent, root)
for field_name, field in self.fields.items():
if field_name in self._errors:
continue
field.bind(field_name, self, root)
source = field.source or field_name
if self.partial and source not in attrs:
continue
try:
validate_method = getattr(self, 'validate_%s' % field_name, None)
if validate_method:
attrs = validate_method(attrs, source)
except ValidationError as err:
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
# If there are already errors, we don't run .validate() because
# field-validation failed and thus `attrs` may not be complete.
# which in turn can cause inconsistent validation errors.
if not self._errors:
try:
attrs = self.validate(attrs)
except ValidationError as err:
if hasattr(err, 'message_dict'):
for field_name, error_messages in err.message_dict.items():
self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
elif hasattr(err, 'messages'):
self._errors['non_field_errors'] = err.messages
return attrs
def get_initial(self):
return {
field.field_name: field.get_initial()
for field in self.fields.values()
}
def validate(self, attrs):
"""
Stub method, to be overridden in Serializer subclasses
"""
return attrs
def get_value(self, dictionary):
# We override the default field access in order to support
# nested HTML forms.
if html.is_html_input(dictionary):
return html.parse_html_dict(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
def restore_object(self, attrs, instance=None):
def to_native(self, data):
"""
Deserialize a dictionary of attributes into an object instance.
You should override this method to control how deserialized objects
are instantiated.
Dict of native values <- Dict of primitive datatypes.
"""
if instance is not None:
instance.update(attrs)
return instance
return attrs
ret = {}
errors = {}
fields = [field for field in self.fields.values() if not field.read_only]
def to_native(self, obj):
"""
Serialize objects -> primitives.
"""
ret = self._dict_class()
ret.fields = self._dict_class()
for field in fields:
primitive_value = field.get_value(data)
try:
validated_value = field.validate(primitive_value)
except ValidationError as exc:
errors[field.field_name] = str(exc)
except SkipField:
pass
else:
set_value(ret, field.source_attrs, validated_value)
for field_name, field in self.fields.items():
if field.read_only and obj is None:
continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
method = getattr(self, 'transform_%s' % field_name, None)
if callable(method):
value = method(obj, value)
if not getattr(field, 'write_only', False):
ret[key] = value
ret.fields[key] = self.augment_field(field, field_name, key, value)
if errors:
raise ValidationError(errors)
return ret
def from_native(self, data, files=None):
"""
Deserialize primitives -> objects.
"""
self._errors = {}
if data is not None or files is not None:
attrs = self.restore_fields(data, files)
if attrs is not None:
attrs = self.perform_validation(attrs)
else:
self._errors['non_field_errors'] = ['No input provided']
if not self._errors:
return self.restore_object(attrs, instance=getattr(self, 'object', None))
def augment_field(self, field, field_name, key, value):
# This horrible stuff is to manage serializers rendering to HTML
field._errors = self._errors.get(key) if self._errors else None
field._name = field_name
field._value = self.init_data.get(key) if self._errors and self.init_data else value
if not field.label:
field.label = pretty_name(key)
return field
def field_to_native(self, obj, field_name):
def to_primative(self, instance):
"""
Override default so that the serializer can be used as a nested field
across relationships.
Object instance -> Dict of primitive datatypes.
"""
if self.write_only:
return None
ret = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
if self.source == '*':
return self.to_native(obj)
for field in fields:
native_value = field.get_attribute(instance)
ret[field.field_name] = field.to_primative(native_value)
# Get the raw field value
try:
source = self.source or field_name
value = obj
for component in source.split('.'):
if value is None:
break
value = get_component(value, component)
except ObjectDoesNotExist:
return None
return ret
if is_simple_callable(getattr(value, 'all', None)):
return [self.to_native(item) for item in value.all()]
def __iter__(self):
errors = self.errors if hasattr(self, '_errors') else {}
for field in self.fields.values():
value = self.data.get(field.field_name) if self.data else None
error = errors.get(field.field_name)
yield FieldResult(field, value, error)
if value is None:
return None
if self.many:
return [self.to_native(item) for item in value]
return self.to_native(value)
class ListSerializer(BaseSerializer):
child = None
initial = []
def field_from_native(self, data, files, field_name, into):
"""
Override default so that the serializer can be used as a writable
nested field across relationships.
"""
if self.read_only:
return
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.'
try:
value = data[field_name]
except KeyError:
if self.default is not None and not self.partial:
# Note: partial updates shouldn't set defaults
value = copy.deepcopy(self.default)
else:
if self.required:
raise ValidationError(self.error_messages['required'])
return
if self.source == '*':
if value:
reverted_data = self.restore_fields(value, {})
if not self._errors:
into.update(reverted_data)
else:
if value in (None, ''):
into[(self.source or field_name)] = None
else:
# Set the serializer object if it exists
obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
# If we have a model manager or similar object then we need
# to iterate through each instance.
if (
self.many and
not hasattr(obj, '__iter__') and
is_simple_callable(getattr(obj, 'all', None))
):
obj = obj.all()
kwargs = {
'instance': obj,
'data': value,
'context': self.context,
'partial': self.partial,
'many': self.many,
'allow_add_remove': self.allow_add_remove
}
serializer = self.__class__(**kwargs)
kwargs.pop('context', None)
kwargs.pop('partial', None)
if serializer.is_valid():
into[self.source or field_name] = serializer.object
else:
# Propagate errors up to our parent
raise NestedValidationError(serializer.errors)
super(ListSerializer, self).__init__(*args, **kwargs)
self.child.bind('', self, self)
def get_identity(self, data):
"""
This hook is required for bulk update.
It is used to determine the canonical identity of a given object.
def bind(self, field_name, parent, root):
# If the list is used as a field then it needs to provide
# the current context to the child serializer.
super(ListSerializer, self).bind(field_name, parent, root)
self.child.bind(field_name, self, root)
Note that the data has not been validated at this point, so we need
to make sure that we catch any cases of incorrect datatypes being
passed to this method.
"""
try:
return data.get('id', None)
except AttributeError:
return None
def get_value(self, dictionary):
# We override the default field access in order to support
# lists in HTML forms.
if is_html_input(dictionary):
return html.parse_html_list(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
@property
def errors(self):
def to_native(self, data):
"""
Run deserialization and return error data,
setting self.object if no errors occurred.
List of dicts of native values <- List of dicts of primitive datatypes.
"""
if self._errors is None:
data, files = self.init_data, self.init_files
if html.is_html_input(data):
data = html.parse_html_list(data)
if self.many is not None:
many = self.many
else:
many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
if many:
warnings.warn('Implicit list/queryset serialization is deprecated. '
'Use the `many=True` flag when instantiating the serializer.',
DeprecationWarning, stacklevel=3)
if many:
ret = RelationsList()
errors = []
update = self.object is not None
if update:
# If this is a bulk update we need to map all the objects
# to a canonical identity so we can determine which
# individual object is being updated for each item in the
# incoming data
objects = self.object
identities = [self.get_identity(self.to_native(obj)) for obj in objects]
identity_to_objects = dict(zip(identities, objects))
if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)):
for item in data:
if update:
# Determine which object we're updating
identity = self.get_identity(item)
self.object = identity_to_objects.pop(identity, None)
if self.object is None and not self.allow_add_remove:
ret.append(None)
errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
continue
ret.append(self.from_native(item, None))
errors.append(self._errors)
if update and self.allow_add_remove:
ret._deleted = identity_to_objects.values()
self._errors = any(errors) and errors or []
else:
self._errors = {'non_field_errors': ['Expected a list of items.']}
else:
ret = self.from_native(data, files)
if not self._errors:
self.object = ret
return self._errors
def is_valid(self):
return not self.errors
return [self.child.validate(item) for item in data]
@property
def data(self):
def to_primative(self, data):
"""
Returns the serialized data on the serializer.
List of object instances -> List of dicts of primitive datatypes.
"""
if self._data is None:
obj = self.object
return [self.child.to_primative(item) for item in data]
if self.many is not None:
many = self.many
else:
many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
if many:
warnings.warn('Implicit list/queryset serialization is deprecated. '
'Use the `many=True` flag when instantiating the serializer.',
DeprecationWarning, stacklevel=2)
if many:
self._data = [self.to_native(item) for item in obj]
else:
self._data = self.to_native(obj)
def create(self, attrs_list):
return [self.child.create(attrs) for attrs in attrs_list]
return self._data
def save(self):
if self.instance is not None:
self.update(self.instance, self.validated_data)
self.instance = self.create(self.validated_data)
return self.instance
def save_object(self, obj, **kwargs):
obj.save(**kwargs)
def delete_object(self, obj):
obj.delete()
def save(self, **kwargs):
"""
Save the deserialized object and return it.
"""
# Clear cached _data, which may be invalidated by `save()`
self._data = None
if isinstance(self.object, list):
[self.save_object(item, **kwargs) for item in self.object]
if self.object._deleted:
[self.delete_object(item) for item in self.object._deleted]
else:
self.save_object(self.object, **kwargs)
return self.object
def metadata(self):
"""
Return a dictionary of metadata about the fields on the serializer.
Useful for things like responding to OPTIONS requests, or generating
API schemas for auto-documentation.
"""
return SortedDict(
[
(field_name, field.metadata())
for field_name, field in six.iteritems(self.fields)
]
)
def _resolve_model(obj):
"""
Resolve supplied `obj` to a Django model class.
`obj` must be a Django model class itself, or a string
representation of one. Useful in situtations like GH #1225 where
Django may not have resolved a string-based reference to a model in
another model's foreign key definition.
class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
pass
String representations should have the format:
'appname.ModelName'
"""
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.')
return models.get_model(app_name, model_name)
elif inspect.isclass(obj) and issubclass(obj, models.Model):
return obj
else:
raise ValueError("{0} is not a Django model".format(obj))
class ModelSerializerOptions(SerializerOptions):
class ModelSerializerOptions(object):
"""
Meta class options for ModelSerializer
"""
def __init__(self, meta):
super(ModelSerializerOptions, self).__init__(meta)
self.model = getattr(meta, 'model', None)
self.read_only_fields = getattr(meta, 'read_only_fields', ())
self.write_only_fields = getattr(meta, 'write_only_fields', ())
self.model = getattr(meta, 'model')
self.fields = getattr(meta, 'fields', ())
self.depth = getattr(meta, 'depth', 0)
class ModelSerializer(Serializer):
"""
A serializer that deals with model instances and querysets.
"""
_options_class = ModelSerializerOptions
field_mapping = {
models.AutoField: IntegerField,
models.FloatField: FloatField,
# models.FloatField: FloatField,
models.IntegerField: IntegerField,
models.PositiveIntegerField: IntegerField,
models.SmallIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
models.DateTimeField: DateTimeField,
models.DateField: DateField,
models.TimeField: TimeField,
models.DecimalField: DecimalField,
models.EmailField: EmailField,
# models.DateTimeField: DateTimeField,
# models.DateField: DateField,
# models.TimeField: TimeField,
# models.DecimalField: DecimalField,
# models.EmailField: EmailField,
models.CharField: CharField,
models.URLField: URLField,
models.SlugField: SlugField,
# models.URLField: URLField,
# models.SlugField: SlugField,
models.TextField: CharField,
models.CommaSeparatedIntegerField: CharField,
models.BooleanField: BooleanField,
models.NullBooleanField: BooleanField,
models.FileField: FileField,
models.ImageField: ImageField,
# models.FileField: FileField,
# models.ImageField: ImageField,
}
_options_class = ModelSerializerOptions
def __init__(self, *args, **kwargs):
self.opts = self._options_class(self.Meta)
super(ModelSerializer, self).__init__(*args, **kwargs)
def get_fields(self):
# Get the explicitly declared fields.
fields = copy.deepcopy(self.base_fields)
# Add in the default fields.
for key, val in self.get_default_fields().items():
if key not in fields:
fields[key] = val
# If `fields` is set on the `Meta` class,
# then use only those fields, and in that order.
if self.opts.fields:
fields = OrderedDict([
(key, fields[key]) for key in self.opts.fields
])
return fields
def get_default_fields(self):
"""
Return all the fields that should be serialized for the model.
"""
cls = self.opts.model
assert cls is not None, (
"Serializer class '%s' is missing 'model' Meta option" %
self.__class__.__name__
)
opts = cls._meta.concrete_model._meta
ret = SortedDict()
ret = OrderedDict()
nested = bool(self.opts.depth)
# Deal with adding the primary key field
......@@ -694,29 +391,9 @@ class ModelSerializer(Serializer):
has_through_model = True
if model_field.rel and nested:
if len(inspect.getargspec(self.get_nested_field).args) == 2:
warnings.warn(
'The `get_nested_field(model_field)` call signature '
'is deprecated. '
'Use `get_nested_field(model_field, related_model, '
'to_many) instead',
DeprecationWarning
)
field = self.get_nested_field(model_field)
else:
field = self.get_nested_field(model_field, related_model, to_many)
field = self.get_nested_field(model_field, related_model, to_many)
elif model_field.rel:
if len(inspect.getargspec(self.get_nested_field).args) == 3:
warnings.warn(
'The `get_related_field(model_field, to_many)` call '
'signature is deprecated. '
'Use `get_related_field(model_field, related_model, '
'to_many) instead',
DeprecationWarning
)
field = self.get_related_field(model_field, to_many=to_many)
else:
field = self.get_related_field(model_field, related_model, to_many)
field = self.get_related_field(model_field, related_model, to_many)
else:
field = self.get_field(model_field)
......@@ -763,38 +440,6 @@ class ModelSerializer(Serializer):
ret[accessor_name] = field
# Ensure that 'read_only_fields' is an iterable
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
# Add the `read_only` flag to any fields that have been specified
# in the `read_only_fields` option
for field_name in self.opts.read_only_fields:
assert field_name not in self.base_fields.keys(), (
"field '%s' on serializer '%s' specified in "
"`read_only_fields`, but also added "
"as an explicit field. Remove it from `read_only_fields`." %
(field_name, self.__class__.__name__))
assert field_name in ret, (
"Non-existant field '%s' specified in `read_only_fields` "
"on serializer '%s'." %
(field_name, self.__class__.__name__))
ret[field_name].read_only = True
# Ensure that 'write_only_fields' is an iterable
assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
for field_name in self.opts.write_only_fields:
assert field_name not in self.base_fields.keys(), (
"field '%s' on serializer '%s' specified in "
"`write_only_fields`, but also added "
"as an explicit field. Remove it from `write_only_fields`." %
(field_name, self.__class__.__name__))
assert field_name in ret, (
"Non-existant field '%s' specified in `write_only_fields` "
"on serializer '%s'." %
(field_name, self.__class__.__name__))
ret[field_name].write_only = True
return ret
def get_pk_field(self, model_field):
......@@ -825,28 +470,24 @@ class ModelSerializer(Serializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
kwargs = {
'queryset': related_model._default_manager,
'many': to_many
}
kwargs = {}
# 'queryset': related_model._default_manager,
# 'many': to_many
# }
if model_field:
kwargs['required'] = not(model_field.null or model_field.blank)
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
# if model_field.help_text is not None:
# kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
if not model_field.editable:
kwargs['read_only'] = True
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
return PrimaryKeyRelatedField(**kwargs)
return IntegerField(**kwargs)
# TODO: return PrimaryKeyRelatedField(**kwargs)
def get_field(self, model_field):
"""
......@@ -869,8 +510,8 @@ class ModelSerializer(Serializer):
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
# if model_field.help_text is not None:
# kwargs['help_text'] = model_field.help_text
# TODO: TypedChoiceField?
if model_field.flatchoices: # This ModelField contains choices
......@@ -880,7 +521,7 @@ class ModelSerializer(Serializer):
return ChoiceField(**kwargs)
# put this below the ChoiceField because min_value isn't a valid initializer
if issubclass(model_field.__class__, models.PositiveIntegerField) or\
if issubclass(model_field.__class__, models.PositiveIntegerField) or \
issubclass(model_field.__class__, models.PositiveSmallIntegerField):
kwargs['min_value'] = 0
......@@ -888,170 +529,27 @@ class ModelSerializer(Serializer):
issubclass(model_field.__class__, (models.CharField, models.TextField)):
kwargs['allow_none'] = True
attribute_dict = {
models.CharField: ['max_length'],
models.CommaSeparatedIntegerField: ['max_length'],
models.DecimalField: ['max_digits', 'decimal_places'],
models.EmailField: ['max_length'],
models.FileField: ['max_length'],
models.ImageField: ['max_length'],
models.SlugField: ['max_length'],
models.URLField: ['max_length'],
}
if model_field.__class__ in attribute_dict:
attributes = attribute_dict[model_field.__class__]
for attribute in attributes:
kwargs.update({attribute: getattr(model_field, attribute)})
# attribute_dict = {
# models.CharField: ['max_length'],
# models.CommaSeparatedIntegerField: ['max_length'],
# models.DecimalField: ['max_digits', 'decimal_places'],
# models.EmailField: ['max_length'],
# models.FileField: ['max_length'],
# models.ImageField: ['max_length'],
# models.SlugField: ['max_length'],
# models.URLField: ['max_length'],
# }
# if model_field.__class__ in attribute_dict:
# attributes = attribute_dict[model_field.__class__]
# for attribute in attributes:
# kwargs.update({attribute: getattr(model_field, attribute)})
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
return ModelField(model_field=model_field, **kwargs)
def get_validation_exclusions(self, instance=None):
"""
Return a list of field names to exclude from model validation.
"""
cls = self.opts.model
opts = cls._meta.concrete_model._meta
exclusions = [field.name for field in opts.fields + opts.many_to_many]
for field_name, field in self.fields.items():
field_name = field.source or field_name
if (
field_name in exclusions
and not field.read_only
and (field.required or hasattr(instance, field_name))
and not isinstance(field, Serializer)
):
exclusions.remove(field_name)
return exclusions
def full_clean(self, instance):
"""
Perform Django's full_clean, and populate the `errors` dictionary
if any validation errors occur.
Note that we don't perform this inside the `.restore_object()` method,
so that subclasses can override `.restore_object()`, and still get
the full_clean validation checking.
"""
try:
instance.full_clean(exclude=self.get_validation_exclusions(instance))
except ValidationError as err:
self._errors = err.message_dict
return None
return instance
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
"""
m2m_data = {}
related_data = {}
nested_forward_relations = {}
meta = self.opts.model._meta
# Reverse fk or one-to-one relations
for (obj, model) in meta.get_all_related_objects_with_model():
field_name = obj.get_accessor_name()
if field_name in attrs:
related_data[field_name] = attrs.pop(field_name)
# Reverse m2m relations
for (obj, model) in meta.get_all_related_m2m_objects_with_model():
field_name = obj.get_accessor_name()
if field_name in attrs:
m2m_data[field_name] = attrs.pop(field_name)
# Forward m2m relations
for field in meta.many_to_many + meta.virtual_fields:
if isinstance(field, GenericForeignKey):
continue
if field.name in attrs:
m2m_data[field.name] = attrs.pop(field.name)
# Nested forward relations - These need to be marked so we can save
# them before saving the parent model instance.
for field_name in attrs.keys():
if isinstance(self.fields.get(field_name, None), Serializer):
nested_forward_relations[field_name] = attrs[field_name]
# Create an empty instance of the model
if instance is None:
instance = self.opts.model()
for key, val in attrs.items():
try:
setattr(instance, key, val)
except ValueError:
self._errors[key] = [self.error_messages['required']]
# Any relations that cannot be set until we've
# saved the model get hidden away on these
# private attributes, so we can deal with them
# at the point of save.
instance._related_data = related_data
instance._m2m_data = m2m_data
instance._nested_forward_relations = nested_forward_relations
return instance
def from_native(self, data, files):
"""
Override the default method to also include model field validation.
"""
instance = super(ModelSerializer, self).from_native(data, files)
if not self._errors:
return self.full_clean(instance)
def save_object(self, obj, **kwargs):
"""
Save the deserialized object.
"""
if getattr(obj, '_nested_forward_relations', None):
# Nested relationships need to be saved before we can save the
# parent instance.
for field_name, sub_object in obj._nested_forward_relations.items():
if sub_object:
self.save_object(sub_object)
setattr(obj, field_name, sub_object)
obj.save(**kwargs)
if getattr(obj, '_m2m_data', None):
for accessor_name, object_list in obj._m2m_data.items():
setattr(obj, accessor_name, object_list)
del(obj._m2m_data)
if getattr(obj, '_related_data', None):
related_fields = dict([
(field.get_accessor_name(), field)
for field, model
in obj._meta.get_all_related_objects_with_model()
])
for accessor_name, related in obj._related_data.items():
if isinstance(related, RelationsList):
# Nested reverse fk relationship
for related_item in related:
fk_field = related_fields[accessor_name].field.name
setattr(related_item, fk_field, obj)
self.save_object(related_item)
# Delete any removed objects
if related._deleted:
[self.delete_object(item) for item in related._deleted]
elif isinstance(related, models.Model):
# Nested reverse one-one relationship
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
setattr(related, fk_field, obj)
self.save_object(related)
else:
# Reverse FK or reverse one-one
setattr(obj, accessor_name, related)
del(obj._related_data)
# TODO: Change this to `return ModelField(model_field=model_field, **kwargs)`
return CharField(**kwargs)
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
......@@ -1066,14 +564,10 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
class HyperlinkedModelSerializer(ModelSerializer):
"""
A subclass of ModelSerializer that uses hyperlinked relationships,
instead of primary key relationships.
"""
_options_class = HyperlinkedModelSerializerOptions
_default_view_name = '%(model_name)s-detail'
_hyperlink_field_class = HyperlinkedRelatedField
_hyperlink_identify_field_class = HyperlinkedIdentityField
#_hyperlink_field_class = HyperlinkedRelatedField
#_hyperlink_identify_field_class = HyperlinkedIdentityField
def get_default_fields(self):
fields = super(HyperlinkedModelSerializer, self).get_default_fields()
......@@ -1081,15 +575,15 @@ class HyperlinkedModelSerializer(ModelSerializer):
if self.opts.view_name is None:
self.opts.view_name = self._get_default_view_name(self.opts.model)
if self.opts.url_field_name not in fields:
url_field = self._hyperlink_identify_field_class(
view_name=self.opts.view_name,
lookup_field=self.opts.lookup_field
)
ret = self._dict_class()
ret[self.opts.url_field_name] = url_field
ret.update(fields)
fields = ret
# if self.opts.url_field_name not in fields:
# url_field = self._hyperlink_identify_field_class(
# view_name=self.opts.view_name,
# lookup_field=self.opts.lookup_field
# )
# ret = self._dict_class()
# ret[self.opts.url_field_name] = url_field
# ret.update(fields)
# fields = ret
return fields
......@@ -1103,33 +597,25 @@ class HyperlinkedModelSerializer(ModelSerializer):
"""
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
kwargs = {
'queryset': related_model._default_manager,
'view_name': self._get_default_view_name(related_model),
'many': to_many
}
# kwargs = {
# 'queryset': related_model._default_manager,
# 'view_name': self._get_default_view_name(related_model),
# 'many': to_many
# }
kwargs = {}
if model_field:
kwargs['required'] = not(model_field.null or model_field.blank)
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
# if model_field.help_text is not None:
# kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
if self.opts.lookup_field:
kwargs['lookup_field'] = self.opts.lookup_field
return self._hyperlink_field_class(**kwargs)
return IntegerField(**kwargs)
# if self.opts.lookup_field:
# kwargs['lookup_field'] = self.opts.lookup_field
def get_identity(self, data):
"""
This hook is required for bulk update.
We need to override the default, to use the url as the identity.
"""
try:
return data.get(self.opts.url_field_name, None)
except AttributeError:
return None
# return self._hyperlink_field_class(**kwargs)
def _get_default_view_name(self, model):
"""
......
......@@ -7,7 +7,7 @@ from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict
from django.utils.functional import Promise
from rest_framework.compat import force_text
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
# from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime
import decimal
import types
......@@ -106,14 +106,14 @@ else:
SortedDict,
yaml.representer.SafeRepresenter.represent_dict
)
SafeDumper.add_representer(
DictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict
)
SafeDumper.add_representer(
SortedDictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict
)
# SafeDumper.add_representer(
# DictWithMetadata,
# yaml.representer.SafeRepresenter.represent_dict
# )
# SafeDumper.add_representer(
# SortedDictWithMetadata,
# yaml.representer.SafeRepresenter.represent_dict
# )
SafeDumper.add_representer(
types.GeneratorType,
yaml.representer.SafeRepresenter.represent_list
......
"""
Helpers for dealing with HTML input.
"""
def is_html_input(dictionary):
# MultiDict type datastructures are used to represent HTML form input,
# which may have more than one value for each key.
return hasattr(dictionary, 'getlist')
def parse_html_list(dictionary, prefix=''):
"""
Used to suport list values in HTML forms.
Supports lists of primitives and/or dictionaries.
* List of primitives.
{
'[0]': 'abc',
'[1]': 'def',
'[2]': 'hij'
}
-->
[
'abc',
'def',
'hij'
]
* List of dictionaries.
{
'[0]foo': 'abc',
'[0]bar': 'def',
'[1]foo': 'hij',
'[2]bar': 'klm',
}
-->
[
{'foo': 'abc', 'bar': 'def'},
{'foo': 'hij', 'bar': 'klm'}
]
"""
Dict = type(dictionary)
ret = {}
regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
for field, value in dictionary.items():
match = regex.match(field)
if not match:
continue
index, key = match.groups()
index = int(index)
if not key:
ret[index] = value
elif isinstance(ret.get(index), dict):
ret[index][key] = value
else:
ret[index] = Dict({key: value})
return [ret[item] for item in sorted(ret.keys())]
def parse_html_dict(dictionary, prefix):
"""
Used to support dictionary values in HTML forms.
{
'profile.username': 'example',
'profile.email': 'example@example.com',
}
-->
{
'profile': {
'username': 'example,
'email': 'example@example.com'
}
}
"""
ret = {}
regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
for field, value in dictionary.items():
match = regex.match(field)
if not match:
continue
key = match.groups()[0]
ret[key] = value
return ret
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment