aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/fields.py
diff options
context:
space:
mode:
authorTom Christie2012-10-30 14:32:31 +0000
committerTom Christie2012-10-30 14:32:31 +0000
commit9b30dab4f772f67a626e176dc4fae0a3ef9c2c81 (patch)
treeca138abf4792f58ffa28684f784f201ee1eef6d7 /rest_framework/fields.py
parent7e5b1501b5cede61a9391fb1a751d2ebcdb37031 (diff)
parent4e7805cb24d73e7f706318b5e5a27e3f9ba39d14 (diff)
downloaddjango-rest-framework-9b30dab4f772f67a626e176dc4fae0a3ef9c2c81.tar.bz2
Merge branch 'restframework2' into rest-framework-2-merge2.0.0
Conflicts: .gitignore .travis.yml AUTHORS README.rst djangorestframework/mixins.py djangorestframework/renderers.py djangorestframework/resources.py djangorestframework/serializer.py djangorestframework/templates/djangorestframework/base.html djangorestframework/templates/djangorestframework/login.html djangorestframework/templatetags/add_query_param.py djangorestframework/tests/accept.py djangorestframework/tests/authentication.py djangorestframework/tests/content.py djangorestframework/tests/reverse.py djangorestframework/tests/serializer.py djangorestframework/views.py docs/examples.rst docs/examples/blogpost.rst docs/examples/modelviews.rst docs/examples/objectstore.rst docs/examples/permissions.rst docs/examples/pygments.rst docs/examples/views.rst docs/howto/alternativeframeworks.rst docs/howto/mixin.rst docs/howto/reverse.rst docs/howto/usingurllib2.rst docs/index.rst docs/topics/release-notes.md examples/sandbox/views.py rest_framework/__init__.py rest_framework/compat.py rest_framework/utils/breadcrumbs.py setup.py
Diffstat (limited to 'rest_framework/fields.py')
-rw-r--r--rest_framework/fields.py695
1 files changed, 695 insertions, 0 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
new file mode 100644
index 00000000..1d6d760e
--- /dev/null
+++ b/rest_framework/fields.py
@@ -0,0 +1,695 @@
+import copy
+import datetime
+import inspect
+import warnings
+
+from django.core import validators
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.urlresolvers import resolve, get_script_prefix
+from django.conf import settings
+from django.forms import widgets
+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):
+ """
+ True if the object is a callable that takes no arguments.
+ """
+ return (
+ (inspect.isfunction(obj) and not inspect.getargspec(obj)[0]) or
+ (inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1)
+ )
+
+
+class Field(object):
+ creation_counter = 0
+ empty = ''
+ type_name = None
+
+ def __init__(self, source=None):
+ self.parent = None
+
+ self.creation_counter = Field.creation_counter
+ Field.creation_counter += 1
+
+ self.source = source
+
+ def initialize(self, parent):
+ """
+ Called to set up a field prior to field_to_native or field_from_native.
+
+ parent - The parent serializer.
+ model_field - The model field this field corresponds to, if one exists.
+ """
+ self.parent = parent
+ self.root = parent.root or parent
+ self.context = self.root.context
+
+ def field_from_native(self, data, field_name, into):
+ """
+ Given a dictionary and a field name, updates the dictionary `into`,
+ with the field and it's deserialized value.
+ """
+ return
+
+ def field_to_native(self, obj, field_name):
+ """
+ Given and object and a field name, returns the value that should be
+ serialized for that field.
+ """
+ if obj is None:
+ return self.empty
+
+ if self.source == '*':
+ return self.to_native(obj)
+
+ if self.source:
+ value = obj
+ for component in self.source.split('.'):
+ value = getattr(value, component)
+ if is_simple_callable(value):
+ value = value()
+ else:
+ value = getattr(obj, field_name)
+ return self.to_native(value)
+
+ def to_native(self, value):
+ """
+ Converts the field's value into it's simple representation.
+ """
+ if is_simple_callable(value):
+ value = value()
+
+ if is_protected_type(value):
+ return value
+ elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)):
+ return [self.to_native(item) for item in value]
+ return smart_unicode(value)
+
+ def attributes(self):
+ """
+ Returns a dictionary of attributes to be used when serializing to xml.
+ """
+ if self.type_name:
+ return {'type': self.type_name}
+ return {}
+
+
+class WritableField(Field):
+ """
+ Base for read/write fields.
+ """
+ default_validators = []
+ default_error_messages = {
+ 'required': _('This field is required.'),
+ 'invalid': _('Invalid value.'),
+ }
+ widget = widgets.TextInput
+ default = None
+
+ def __init__(self, source=None, read_only=False, required=None,
+ validators=[], error_messages=None, widget=None,
+ default=None, blank=None):
+
+ super(WritableField, self).__init__(source=source)
+
+ self.read_only = read_only
+ if required is None:
+ self.required = not(read_only)
+ else:
+ assert not read_only, "Cannot set required=True and read_only=True"
+ self.required = required
+
+ messages = {}
+ for c in reversed(self.__class__.__mro__):
+ messages.update(getattr(c, 'default_error_messages', {}))
+ messages.update(error_messages or {})
+ self.error_messages = messages
+
+ self.validators = self.default_validators + validators
+ self.default = default or 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:
+ raise ValidationError(self.error_messages['required'])
+
+ def run_validators(self, value):
+ if value in validators.EMPTY_VALUES:
+ return
+ errors = []
+ for v in self.validators:
+ try:
+ v(value)
+ except ValidationError as e:
+ if hasattr(e, 'code') and e.code in self.error_messages:
+ message = self.error_messages[e.code]
+ if e.params:
+ message = message % e.params
+ errors.append(message)
+ else:
+ errors.extend(e.messages)
+ if errors:
+ raise ValidationError(errors)
+
+ def field_from_native(self, data, field_name, into):
+ """
+ Given a dictionary and a field name, updates the dictionary `into`,
+ with the field and it's deserialized value.
+ """
+ if self.read_only:
+ return
+
+ try:
+ native = data[field_name]
+ except KeyError:
+ 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 == '*':
+ if value:
+ into.update(value)
+ else:
+ self.validate(value)
+ self.run_validators(value)
+ into[self.source or field_name] = value
+
+ def from_native(self, value):
+ """
+ Reverts a simple representation back to the field's value.
+ """
+ return value
+
+
+class ModelField(WritableField):
+ """
+ A generic field that can be used against an arbitrary model field.
+ """
+ def __init__(self, *args, **kwargs):
+ try:
+ self.model_field = kwargs.pop('model_field')
+ except:
+ raise ValueError("ModelField requires 'model_field' kwarg")
+ super(ModelField, self).__init__(*args, **kwargs)
+
+ def from_native(self, value):
+ try:
+ rel = self.model_field.rel
+ except:
+ 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)
+ 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()
+ }
+
+##### Relational fields #####
+
+
+class RelatedField(WritableField):
+ """
+ Base class for related model fields.
+ """
+ def __init__(self, *args, **kwargs):
+ self.queryset = kwargs.pop('queryset', None)
+ super(RelatedField, self).__init__(*args, **kwargs)
+
+ 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)] = self.from_native(value)
+
+
+class ManyRelatedMixin(object):
+ """
+ Mixin to convert a related field to a many related field.
+ """
+ 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)
+ except:
+ # Non-form data
+ value = data.get(self.source or field_name)
+ else:
+ if value == ['']:
+ value = []
+ into[field_name] = [self.from_native(item) for item in value]
+
+
+class ManyRelatedField(ManyRelatedMixin, RelatedField):
+ """
+ Base class for related model managers.
+ """
+ pass
+
+
+### PrimaryKey relationships
+
+class PrimaryKeyRelatedField(RelatedField):
+ """
+ Serializes a related field or related object to a pk value.
+ """
+
+ 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:
+ raise ValidationError('Invalid hyperlink - object does not exist.')
+
+ def field_to_native(self, obj, field_name):
+ try:
+ # Prefer obj.serializable_value for performance reasons
+ pk = obj.serializable_value(self.source or field_name)
+ except AttributeError:
+ # RelatedObject (reverse relationship)
+ obj = getattr(obj, self.source or field_name)
+ return self.to_native(obj.pk)
+ # Forward relationship
+ return self.to_native(pk)
+
+
+class ManyPrimaryKeyRelatedField(ManyRelatedField):
+ """
+ Serializes a to-many related field or related manager to a pk value.
+ """
+ def to_native(self, pk):
+ return pk
+
+ def field_to_native(self, obj, field_name):
+ try:
+ # Prefer obj.serializable_value for performance reasons
+ queryset = obj.serializable_value(self.source or field_name)
+ except AttributeError:
+ # RelatedManager (reverse relationship)
+ queryset = getattr(obj, self.source or field_name)
+ return [self.to_native(item.pk) for item in queryset.all()]
+ # Forward relationship
+ return [self.to_native(item.pk) for item in queryset.all()]
+
+
+### Hyperlinked relationships
+
+class HyperlinkedRelatedField(RelatedField):
+ pk_url_kwarg = 'pk'
+ slug_url_kwarg = 'slug'
+ slug_field = 'slug'
+
+ def __init__(self, *args, **kwargs):
+ try:
+ self.view_name = kwargs.pop('view_name')
+ except:
+ raise ValueError("Hyperlinked field requires 'view_name' kwarg")
+ self.format = kwargs.pop('format', None)
+ super(HyperlinkedRelatedField, self).__init__(*args, **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)
+ 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)
+
+ 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:
+ raise ValidationError('Invalid hyperlink - No URL match')
+
+ if match.url_name != self.view_name:
+ raise ValidationError('Invalid hyperlink - Incorrect URL match')
+
+ pk = match.kwargs.get(self.pk_url_kwarg, None)
+ slug = match.kwargs.get(self.slug_url_kwarg, None)
+
+ # Try explicit primary key.
+ if pk is not None:
+ queryset = self.queryset.filter(pk=pk)
+ # Next, try looking up by slug.
+ elif slug is not None:
+ slug_field = self.get_slug_field()
+ queryset = self.queryset.filter(**{slug_field: slug})
+ # If none of those are defined, it's an error.
+ else:
+ raise ValidationError('Invalid hyperlink')
+
+ try:
+ obj = queryset.get()
+ except ObjectDoesNotExist:
+ raise ValidationError('Invalid hyperlink - object does not exist.')
+ return obj
+
+
+class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
+ pass
+
+
+class HyperlinkedIdentityField(Field):
+ """
+ A field that represents the model's identity using a hyperlink.
+ """
+ def __init__(self, *args, **kwargs):
+ # TODO: Make this mandatory, and have the HyperlinkedModelSerializer
+ # set it on-the-fly
+ self.view_name = kwargs.pop('view_name', None)
+ self.format = kwargs.pop('format', None)
+ 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, format=format)
+
+
+##### 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 ('t', 'True', '1'):
+ return True
+ if value in ('f', 'False', '0'):
+ return False
+ return bool(value)
+
+
+class CharField(WritableField):
+ type_name = 'CharField'
+
+ def __init__(self, max_length=None, min_length=None, *args, **kwargs):
+ self.max_length, self.min_length = max_length, min_length
+ super(CharField, self).__init__(*args, **kwargs)
+ if min_length is not None:
+ self.validators.append(validators.MinLengthValidator(min_length))
+ if max_length is not None:
+ self.validators.append(validators.MaxLengthValidator(max_length))
+
+ def 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'
+
+ default_error_messages = {
+ 'invalid': _('Enter a valid e-mail address.'),
+ }
+ default_validators = [validators.validate_email]
+
+ def from_native(self, value):
+ ret = super(EmailField, self).from_native(value)
+ if ret is None:
+ return None
+ return ret.strip()
+
+ def __deepcopy__(self, memo):
+ result = copy.copy(self)
+ memo[id(self)] = result
+ #result.widget = copy.deepcopy(self.widget, memo)
+ result.validators = self.validators[:]
+ return result
+
+
+class DateField(WritableField):
+ type_name = 'DateField'
+
+ default_error_messages = {
+ 'invalid': _(u"'%s' value has an invalid date format. It must be "
+ u"in YYYY-MM-DD format."),
+ 'invalid_date': _(u"'%s' value has the correct format (YYYY-MM-DD) "
+ u"but it is an invalid date."),
+ }
+ empty = None
+
+ def from_native(self, value):
+ if value in validators.EMPTY_VALUES:
+ return None
+
+ if isinstance(value, datetime.datetime):
+ if timezone and settings.USE_TZ and timezone.is_aware(value):
+ # Convert aware datetimes to the default time zone
+ # before casting them to dates (#17742).
+ default_timezone = timezone.get_default_timezone()
+ value = timezone.make_naive(value, default_timezone)
+ return value.date()
+ if isinstance(value, datetime.date):
+ return value
+
+ try:
+ parsed = parse_date(value)
+ if parsed is not None:
+ return parsed
+ except ValueError:
+ msg = self.error_messages['invalid_date'] % value
+ raise ValidationError(msg)
+
+ msg = self.error_messages['invalid'] % value
+ raise ValidationError(msg)
+
+
+class DateTimeField(WritableField):
+ type_name = 'DateTimeField'
+
+ default_error_messages = {
+ 'invalid': _(u"'%s' value has an invalid format. It must be in "
+ u"YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."),
+ 'invalid_date': _(u"'%s' value has the correct format "
+ u"(YYYY-MM-DD) but it is an invalid date."),
+ 'invalid_datetime': _(u"'%s' value has the correct format "
+ u"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
+ u"but it is an invalid date/time."),
+ }
+ empty = None
+
+ def from_native(self, value):
+ if value in validators.EMPTY_VALUES:
+ return None
+
+ if isinstance(value, datetime.datetime):
+ return value
+ if isinstance(value, datetime.date):
+ value = datetime.datetime(value.year, value.month, value.day)
+ if settings.USE_TZ:
+ # For backwards compatibility, interpret naive datetimes in
+ # local time. This won't work during DST change, but we can't
+ # do much about it, so we let the exceptions percolate up the
+ # call stack.
+ warnings.warn(u"DateTimeField received a naive datetime (%s)"
+ u" while time zone support is active." % value,
+ RuntimeWarning)
+ default_timezone = timezone.get_default_timezone()
+ value = timezone.make_aware(value, default_timezone)
+ return value
+
+ try:
+ parsed = parse_datetime(value)
+ if parsed is not None:
+ return parsed
+ except ValueError:
+ msg = self.error_messages['invalid_datetime'] % value
+ raise ValidationError(msg)
+
+ try:
+ parsed = parse_date(value)
+ if parsed is not None:
+ return datetime.datetime(parsed.year, parsed.month, parsed.day)
+ except ValueError:
+ msg = self.error_messages['invalid_date'] % value
+ raise ValidationError(msg)
+
+ msg = self.error_messages['invalid'] % value
+ raise ValidationError(msg)
+
+
+class IntegerField(WritableField):
+ type_name = 'IntegerField'
+
+ 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'
+
+ default_error_messages = {
+ 'invalid': _("'%s' value must be a float."),
+ }
+
+ def from_native(self, value):
+ if value in validators.EMPTY_VALUES:
+ return None
+
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ msg = self.error_messages['invalid'] % value
+ raise ValidationError(msg)