aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/fields.py
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework/fields.py')
-rw-r--r--rest_framework/fields.py409
1 files changed, 366 insertions, 43 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index bb9a523d..a4e29a30 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -5,13 +5,16 @@ import warnings
from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.core.urlresolvers import resolve
+from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings
+from django.forms import widgets
+from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _
from rest_framework.reverse import reverse
from rest_framework.compat import parse_date, parse_datetime
from rest_framework.compat import timezone
+from urlparse import urlparse
def is_simple_callable(obj):
@@ -37,12 +40,12 @@ class Field(object):
self.source = source
- def initialize(self, parent):
+ def initialize(self, parent, field_name):
"""
Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer.
- model_field - The model field this field corrosponds to, if one exists.
+ model_field - The model field this field corresponds to, if one exists.
"""
self.parent = parent
self.root = parent.root or parent
@@ -70,6 +73,8 @@ class Field(object):
value = obj
for component in self.source.split('.'):
value = getattr(value, component)
+ if is_simple_callable(value):
+ value = value()
else:
value = getattr(obj, field_name)
return self.to_native(value)
@@ -85,6 +90,8 @@ class Field(object):
return value
elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)):
return [self.to_native(item) for item in value]
+ elif isinstance(value, dict):
+ return dict(map(self.to_native, (k, v)) for k, v in value.items())
return smart_unicode(value)
def attributes(self):
@@ -105,15 +112,20 @@ class WritableField(Field):
'required': _('This field is required.'),
'invalid': _('Invalid value.'),
}
+ widget = widgets.TextInput
+ default = None
+
+ def __init__(self, source=None, read_only=False, required=None,
+ validators=[], error_messages=None, widget=None,
+ default=None, blank=None):
- def __init__(self, source=None, readonly=False, required=None,
- validators=[], error_messages=None):
super(WritableField, self).__init__(source=source)
- self.readonly = readonly
+
+ self.read_only = read_only
if required is None:
- self.required = not(readonly)
+ self.required = not(read_only)
else:
- assert not readonly, "Cannot set required=True and readonly=True"
+ assert not read_only, "Cannot set required=True and read_only=True"
self.required = required
messages = {}
@@ -123,6 +135,14 @@ class WritableField(Field):
self.error_messages = messages
self.validators = self.default_validators + validators
+ self.default = default if default is not None else self.default
+ self.blank = blank
+
+ # Widgets are ony used for HTML forms.
+ widget = widget or self.widget
+ if isinstance(widget, type):
+ widget = widget()
+ self.widget = widget
def validate(self, value):
if value in validators.EMPTY_VALUES and self.required:
@@ -151,15 +171,18 @@ class WritableField(Field):
Given a dictionary and a field name, updates the dictionary `into`,
with the field and it's deserialized value.
"""
- if self.readonly:
+ if self.read_only:
return
try:
native = data[field_name]
except KeyError:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
+ if self.default is not None:
+ native = self.default
+ else:
+ if self.required:
+ raise ValidationError(self.error_messages['required'])
+ return
value = self.from_native(native)
if self.source == '*':
@@ -179,7 +202,7 @@ class WritableField(Field):
class ModelField(WritableField):
"""
- A generic field that can be used against an arbirtrary model field.
+ A generic field that can be used against an arbitrary model field.
"""
def __init__(self, *args, **kwargs):
try:
@@ -189,11 +212,11 @@ class ModelField(WritableField):
super(ModelField, self).__init__(*args, **kwargs)
def from_native(self, value):
- try:
- rel = self.model_field.rel
- except:
+ 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)
- return rel.to._meta.get_field(rel.field_name).to_python(value)
def field_to_native(self, obj, field_name):
value = self.model_field._get_val_from_obj(obj)
@@ -209,32 +232,119 @@ class ModelField(WritableField):
##### Relational fields #####
+# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
"""
Base class for related model fields.
+
+ If not overridden, this represents a to-one relationship, using the unicode
+ representation of the target.
"""
+ widget = widgets.Select
+ cache_choices = False
+ empty_label = None
+ default_read_only = True # TODO: Remove this
+
def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None)
super(RelatedField, self).__init__(*args, **kwargs)
+ self.read_only = kwargs.pop('read_only', self.default_read_only)
+
+ def initialize(self, parent, field_name):
+ super(RelatedField, self).initialize(parent, field_name)
+ if self.queryset is None and not self.read_only:
+ try:
+ 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()
+ except:
+ raise
+ msg = ('Serializer related fields must include a `queryset`' +
+ ' argument or set `read_only=True')
+ raise Exception(msg)
+
+ ### We need this stuff to make form choices work...
+
+ # def __deepcopy__(self, memo):
+ # result = super(RelatedField, self).__deepcopy__(memo)
+ # result.queryset = result.queryset
+ # return result
+
+ 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_unicode(obj)
+ ident = smart_unicode(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)
+
+ ### Regular serializier stuff...
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return self.to_native(value)
def field_from_native(self, data, field_name, into):
+ if self.read_only:
+ return
+
value = data.get(field_name)
- into[(self.source or field_name) + '_id'] = self.from_native(value)
+ into[(self.source or field_name)] = self.from_native(value)
class ManyRelatedMixin(object):
"""
Mixin to convert a related field to a many related field.
"""
+ widget = widgets.SelectMultiple
+
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return [self.to_native(item) for item in value.all()]
def field_from_native(self, data, field_name, into):
+ if self.read_only:
+ return
+
try:
# Form data
value = data.getlist(self.source or field_name)
@@ -250,6 +360,9 @@ class ManyRelatedMixin(object):
class ManyRelatedField(ManyRelatedMixin, RelatedField):
"""
Base class for related model managers.
+
+ If not overridden, this represents a to-many relationship, using the unicode
+ representations of the target, and is read-only.
"""
pass
@@ -258,12 +371,38 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField):
class PrimaryKeyRelatedField(RelatedField):
"""
- Serializes a related field or related object to a pk value.
+ Represents a to-one relationship as a pk value.
"""
+ default_read_only = False
+
+ # 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_unicode(obj)
+ ident = smart_unicode(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 = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
+ raise ValidationError(msg)
+
def field_to_native(self, obj, field_name):
try:
# Prefer obj.serializable_value for performance reasons
@@ -278,8 +417,23 @@ class PrimaryKeyRelatedField(RelatedField):
class ManyPrimaryKeyRelatedField(ManyRelatedField):
"""
- Serializes a to-many related field or related manager to a pk value.
+ Represents a to-many relationship as a pk value.
"""
+ default_read_only = False
+
+ 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_unicode(obj)
+ ident = smart_unicode(self.to_native(obj.pk))
+ if desc == ident:
+ return desc
+ return "%s - %s" % (desc, ident)
+
def to_native(self, pk):
return pk
@@ -294,27 +448,83 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
# Forward relationship
return [self.to_native(item.pk) for item in queryset.all()]
+ 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 = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
+ raise ValidationError(msg)
+
+### Slug relationships
+
+
+class SlugRelatedField(RelatedField):
+ default_read_only = False
+
+ 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('Object with %s=%s does not exist.' %
+ (self.slug_field, unicode(data)))
+
+
+class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
+ pass
+
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
+ """
+ Represents a to-one relationship, using hyperlinking.
+ """
pk_url_kwarg = 'pk'
- slug_url_kwarg = 'slug'
slug_field = 'slug'
+ slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
+ default_read_only = False
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
+
+ self.slug_field = kwargs.pop('slug_field', self.slug_field)
+ default_slug_kwarg = self.slug_url_kwarg or self.slug_field
+ self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
+ self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+
+ self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
+ def get_slug_field(self):
+ """
+ Get the name of a slug field to be used to look up by slug.
+ """
+ return self.slug_field
+
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)
kwargs = {self.pk_url_kwarg: obj.pk}
try:
- return reverse(view_name, kwargs=kwargs, request=request)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
@@ -325,13 +535,13 @@ class HyperlinkedRelatedField(RelatedField):
kwargs = {self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request)
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request)
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
@@ -340,6 +550,16 @@ class HyperlinkedRelatedField(RelatedField):
def from_native(self, value):
# Convert URL -> model instance pk
# TODO: Use values_list
+ if self.queryset is None:
+ raise Exception('Writable related fields must include a `queryset` argument')
+
+ if value.startswith('http:') or value.startswith('https:'):
+ # If needed convert absolute URLs to relative path
+ value = urlparse(value).path
+ prefix = get_script_prefix()
+ if value.startswith(prefix):
+ value = '/' + value[len(prefix):]
+
try:
match = resolve(value)
except:
@@ -353,7 +573,7 @@ class HyperlinkedRelatedField(RelatedField):
# Try explicit primary key.
if pk is not None:
- return pk
+ queryset = self.queryset.filter(pk=pk)
# Next, try looking up by slug.
elif slug is not None:
slug_field = self.get_slug_field()
@@ -366,48 +586,88 @@ class HyperlinkedRelatedField(RelatedField):
obj = queryset.get()
except ObjectDoesNotExist:
raise ValidationError('Invalid hyperlink - object does not exist.')
- return obj.pk
+ return obj
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
+ """
+ Represents a to-many relationship, using hyperlinking.
+ """
pass
class HyperlinkedIdentityField(Field):
"""
- A field that represents the model's identity using a hyperlink.
+ Represents the instance, or a property on the instance, using hyperlinking.
"""
+ pk_url_kwarg = 'pk'
+ slug_field = 'slug'
+ slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
+
def __init__(self, *args, **kwargs):
- # TODO: Make this mandatory, and have the HyperlinkedModelSerializer
- # set it on-the-fly
+ # TODO: Make view_name mandatory, and have the
+ # HyperlinkedModelSerializer set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
+ self.format = kwargs.pop('format', None)
+
+ self.slug_field = kwargs.pop('slug_field', self.slug_field)
+ default_slug_kwarg = self.slug_url_kwarg or self.slug_field
+ self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
+ self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
+ format = self.format or self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
- view_kwargs = {'pk': obj.pk}
- return reverse(view_name, kwargs=view_kwargs, request=request)
+ kwargs = {self.pk_url_kwarg: obj.pk}
+ try:
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ slug = getattr(obj, self.slug_field, None)
+
+ if not slug:
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+
+ kwargs = {self.slug_url_kwarg: slug}
+ try:
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
+ try:
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
##### Typed Fields #####
class BooleanField(WritableField):
type_name = 'BooleanField'
+ widget = widgets.CheckboxInput
default_error_messages = {
'invalid': _(u"'%s' value must be either True or False."),
}
+ empty = False
+
+ # Note: we set default to `False` in order to fill in missing value not
+ # supplied by html form. TODO: Fix so that only html form input gets
+ # this behavior.
+ default = False
def from_native(self, value):
- if value in (True, False):
- # if value is 1 or 0 than it's equal to True or False, but we want
- # to return a true bool for semantic reasons.
- return bool(value)
if value in ('t', 'True', '1'):
return True
if value in ('f', 'False', '0'):
return False
- raise ValidationError(self.error_messages['invalid'] % value)
+ return bool(value)
class CharField(WritableField):
@@ -421,12 +681,68 @@ class CharField(WritableField):
if max_length is not None:
self.validators.append(validators.MaxLengthValidator(max_length))
+ def validate(self, value):
+ """
+ Validates that the value is supplied (if required).
+ """
+ # if empty string and allow blank
+ if self.blank and not value:
+ return
+ else:
+ super(CharField, self).validate(value)
+
def from_native(self, value):
if isinstance(value, basestring) or value is None:
return value
return smart_unicode(value)
+class ChoiceField(WritableField):
+ type_name = 'ChoiceField'
+ widget = widgets.Select
+ default_error_messages = {
+ 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
+ }
+
+ def __init__(self, choices=(), *args, **kwargs):
+ super(ChoiceField, self).__init__(*args, **kwargs)
+ self.choices = choices
+
+ def _get_choices(self):
+ return self._choices
+
+ def _set_choices(self, value):
+ # Setting choices also sets the choices on the widget.
+ # choices can be any iterable, but we call list() on it because
+ # it will be consumed more than once.
+ self._choices = self.widget.choices = list(value)
+
+ choices = property(_get_choices, _set_choices)
+
+ def validate(self, value):
+ """
+ Validates that the input is in self.choices.
+ """
+ super(ChoiceField, self).validate(value)
+ if value and not self.valid_value(value):
+ raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+
+ def valid_value(self, value):
+ """
+ Check to see if the provided value is a valid choice.
+ """
+ for k, v in self.choices:
+ if isinstance(v, (list, tuple)):
+ # This is an optgroup, so look inside the group for options
+ for k2, v2 in v:
+ if value == smart_unicode(k2):
+ return True
+ else:
+ if value == smart_unicode(k):
+ return True
+ return False
+
+
class EmailField(CharField):
type_name = 'EmailField'
@@ -436,7 +752,10 @@ class EmailField(CharField):
default_validators = [validators.validate_email]
def from_native(self, value):
- return super(EmailField, self).from_native(value).strip()
+ ret = super(EmailField, self).from_native(value)
+ if ret is None:
+ return None
+ return ret.strip()
def __deepcopy__(self, memo):
result = copy.copy(self)
@@ -458,8 +777,9 @@ class DateField(WritableField):
empty = None
def from_native(self, value):
- if value is None:
- return 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
@@ -497,8 +817,9 @@ class DateTimeField(WritableField):
empty = None
def from_native(self, value):
- if value is None:
- return value
+ if value in validators.EMPTY_VALUES:
+ return None
+
if isinstance(value, datetime.datetime):
return value
if isinstance(value, datetime.date):
@@ -556,6 +877,7 @@ class IntegerField(WritableField):
def from_native(self, value):
if value in validators.EMPTY_VALUES:
return None
+
try:
value = int(str(value))
except (ValueError, TypeError):
@@ -571,8 +893,9 @@ class FloatField(WritableField):
}
def from_native(self, value):
- if value is None:
- return value
+ if value in validators.EMPTY_VALUES:
+ return None
+
try:
return float(value)
except (TypeError, ValueError):