aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--rest_framework/fields.py38
-rw-r--r--rest_framework/relations.py235
-rw-r--r--rest_framework/renderers.py1
-rw-r--r--rest_framework/serializers.py88
-rw-r--r--rest_framework/tests/relations_hyperlink.py2
-rw-r--r--rest_framework/tests/relations_slug.py2
-rw-r--r--rest_framework/tests/serializer.py4
7 files changed, 190 insertions, 180 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index a66e1d7c..d6e8539d 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -35,7 +35,8 @@ class Field(object):
creation_counter = 0
empty = ''
type_name = None
- _use_files = None
+ partial = False
+ use_files = False
form_field_class = forms.CharField
def __init__(self, source=None):
@@ -56,7 +57,8 @@ class Field(object):
self.parent = parent
self.root = parent.root or parent
self.context = self.root.context
- if self.root.partial:
+ self.partial = self.root.partial
+ if self.partial:
self.required = False
def field_from_native(self, data, files, field_name, into):
@@ -127,6 +129,13 @@ class WritableField(Field):
validators=[], error_messages=None, widget=None,
default=None, blank=None):
+ # 'blank' is to be deprecated in favor of 'required'
+ if blank is not None:
+ warnings.warn('The `blank` keyword argument is due to deprecated. '
+ 'Use the `required` keyword argument instead.',
+ PendingDeprecationWarning, stacklevel=2)
+ required = not(blank)
+
super(WritableField, self).__init__(source=source)
self.read_only = read_only
@@ -144,7 +153,6 @@ class WritableField(Field):
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
@@ -183,13 +191,13 @@ class WritableField(Field):
return
try:
- if self._use_files:
+ if self.use_files:
files = files or {}
native = files[field_name]
else:
native = data[field_name]
except KeyError:
- if self.default is not None and not self.root.partial:
+ if self.default is not None and not self.partial:
# Note: partial updates shouldn't set defaults
native = self.default
else:
@@ -290,16 +298,6 @@ 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, six.string_types) or value is None:
return value
@@ -328,7 +326,8 @@ class ChoiceField(WritableField):
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.'),
+ 'invalid_choice': _('Select a valid choice. %(value)s is not one of '
+ 'the available choices.'),
}
def __init__(self, choices=(), *args, **kwargs):
@@ -567,7 +566,7 @@ class FloatField(WritableField):
class FileField(WritableField):
- _use_files = True
+ use_files = True
type_name = 'FileField'
form_field_class = forms.FileField
widget = widgets.FileInput
@@ -611,11 +610,12 @@ class FileField(WritableField):
class ImageField(FileField):
- _use_files = True
+ use_files = True
form_field_class = forms.ImageField
default_error_messages = {
- 'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
+ 'invalid_image': _("Upload a valid image. The file you uploaded was "
+ "either not an image or a corrupted image."),
}
def from_native(self, data):
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index dfa80fb7..93f19362 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -9,8 +9,11 @@ from django.forms.models import ModelChoiceIterator
from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import Field, WritableField
from rest_framework.reverse import reverse
+from urlparse import urlparse
from rest_framework.compat import urlparse
from rest_framework.compat import smart_text
+import warnings
+
##### Relational fields #####
@@ -20,19 +23,35 @@ 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.
+ 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
+
cache_choices = False
empty_label = None
- default_read_only = True # TODO: Remove this
+ read_only = True
+ many = False
def __init__(self, *args, **kwargs):
+
+ # 'null' is to be deprecated in favor of 'required'
+ if 'null' in kwargs:
+ warnings.warn('The `null` keyword argument is due to be deprecated. '
+ 'Use the `required` keyword argument instead.',
+ PendingDeprecationWarning, stacklevel=2)
+ kwargs['required'] = not kwargs.pop('null')
+
self.queryset = kwargs.pop('queryset', None)
- self.null = kwargs.pop('null', False)
+ 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)
- self.read_only = kwargs.pop('read_only', self.default_read_only)
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
@@ -51,11 +70,6 @@ class RelatedField(WritableField):
### 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)
@@ -111,6 +125,9 @@ class RelatedField(WritableField):
if value is None:
return None
+
+ if self.many:
+ return [self.to_native(item) for item in value.all()]
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
@@ -118,65 +135,39 @@ class RelatedField(WritableField):
return
try:
- value = data[field_name]
+ 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.required:
- raise ValidationError(self.error_messages['required'])
- return
+ if self.partial:
+ return
+ value = [] if self.many else None
- if value in (None, '') and not self.null:
- raise ValidationError('Value may not be null')
- elif value in (None, '') and self.null:
+ if value in (None, '') and self.required:
+ raise ValidationError(self.error_messages['required'])
+ elif value in (None, ''):
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)
-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, files, 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.
-
- If not overridden, this represents a to-many relationship, using the unicode
- representations of the target, and is read-only.
- """
- pass
-
-
### PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField):
"""
- Represents a to-one relationship as a pk value.
+ Represents a relationship as a pk value.
"""
- default_read_only = False
- form_field_class = forms.ChoiceField
+ read_only = False
default_error_messages = {
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
@@ -216,79 +207,42 @@ class PrimaryKeyRelatedField(RelatedField):
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
+ if self.many:
+ # To-many relationship
+ 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)
+
+ # Forward relationship
+ return [self.to_native(item.pk) for item in queryset.all()]
+
+ # 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:
- obj = getattr(obj, self.source or field_name)
+ pk = getattr(obj, self.source or field_name).pk
except ObjectDoesNotExist:
return None
return self.to_native(obj.pk)
- # Forward relationship
- return self.to_native(pk)
-
-
-class ManyPrimaryKeyRelatedField(ManyRelatedField):
- """
- Represents a to-many relationship as a pk value.
- """
- default_read_only = False
- form_field_class = forms.MultipleChoiceField
- default_error_messages = {
- 'does_not_exist': _("Invalid pk '%s' - object does not exist."),
- 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
- }
-
- 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)
-
- 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()]
+ return self.to_native(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)
### Slug relationships
class SlugRelatedField(RelatedField):
- default_read_only = False
- form_field_class = forms.ChoiceField
+ """
+ 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."),
@@ -317,21 +271,16 @@ class SlugRelatedField(RelatedField):
raise ValidationError(msg)
-class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
- form_field_class = forms.MultipleChoiceField
-
-
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
- Represents a to-one relationship, using hyperlinking.
+ Represents a relationship using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
- default_read_only = False
- form_field_class = forms.ChoiceField
+ read_only = False
default_error_messages = {
'no_match': _('Invalid hyperlink - No URL match'),
@@ -445,13 +394,6 @@ class HyperlinkedRelatedField(RelatedField):
return obj
-class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
- """
- Represents a to-many relationship, using hyperlinking.
- """
- form_field_class = forms.MultipleChoiceField
-
-
class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
@@ -459,6 +401,7 @@ class HyperlinkedIdentityField(Field):
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
+ read_only = True
def __init__(self, *args, **kwargs):
# TODO: Make view_name mandatory, and have the
@@ -515,3 +458,41 @@ class HyperlinkedIdentityField(Field):
pass
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
+
+
+### Old-style many classes for backwards compat
+
+class ManyRelatedField(RelatedField):
+ def __init__(self, *args, **kwargs):
+ warnings.warn('`ManyRelatedField()` is due to be deprecated. '
+ 'Use `RelatedField(many=True)` instead.',
+ PendingDeprecationWarning, stacklevel=2)
+ kwargs['many'] = True
+ super(ManyRelatedField, self).__init__(*args, **kwargs)
+
+
+class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField):
+ def __init__(self, *args, **kwargs):
+ warnings.warn('`ManyPrimaryKeyRelatedField()` is due to be deprecated. '
+ 'Use `PrimaryKeyRelatedField(many=True)` instead.',
+ PendingDeprecationWarning, stacklevel=2)
+ kwargs['many'] = True
+ super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs)
+
+
+class ManySlugRelatedField(SlugRelatedField):
+ def __init__(self, *args, **kwargs):
+ warnings.warn('`ManySlugRelatedField()` is due to be deprecated. '
+ 'Use `SlugRelatedField(many=True)` instead.',
+ PendingDeprecationWarning, stacklevel=2)
+ kwargs['many'] = True
+ super(ManySlugRelatedField, self).__init__(*args, **kwargs)
+
+
+class ManyHyperlinkedRelatedField(HyperlinkedRelatedField):
+ def __init__(self, *args, **kwargs):
+ warnings.warn('`ManyHyperlinkedRelatedField()` is due to be deprecated. '
+ 'Use `HyperlinkedRelatedField(many=True)` instead.',
+ PendingDeprecationWarning, stacklevel=2)
+ kwargs['many'] = True
+ super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 7eb6068a..74c7e2c9 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -335,6 +335,7 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs['label'] = k
fields[k] = v.form_field_class(**kwargs)
+
return fields
def get_form(self, view, method, request):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index b154fcad..c5b3494c 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -96,19 +96,24 @@ class SerializerOptions(object):
class BaseSerializer(Field):
+ """
+ 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 # Set to unsorted dict for backwards compatibility with unsorted implementations.
+ _dict_class = SortedDictWithMetadata
def __init__(self, instance=None, data=None, files=None,
- context=None, partial=False, **kwargs):
- super(BaseSerializer, self).__init__(**kwargs)
+ context=None, partial=False, many=None, source=None):
+ super(BaseSerializer, self).__init__(source=source)
self.opts = self._options_class(self.Meta)
self.parent = None
self.root = None
self.partial = partial
+ self.many = many
self.context = context or {}
@@ -188,22 +193,6 @@ class BaseSerializer(Field):
"""
return field_name
- def convert_object(self, obj):
- """
- Core of serialization.
- Convert an object into a dictionary of serialized field values.
- """
- ret = self._dict_class()
- ret.fields = {}
-
- for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- key = self.get_field_key(field_name)
- value = field.field_to_native(obj, field_name)
- ret[key] = value
- ret.fields[key] = field
- return ret
-
def restore_fields(self, data, files):
"""
Core of deserialization, together with `restore_object`.
@@ -275,13 +264,16 @@ class BaseSerializer(Field):
"""
Serialize objects -> primitives.
"""
- # Note: At the moment we have an ugly hack to determine if we should
- # walk over iterables. At some point, serializers will require an
- # explicit `many=True` in order to iterate over a set, and this hack
- # will disappear.
- if hasattr(obj, '__iter__') and not isinstance(obj, Page):
- return [self.convert_object(item) for item in obj]
- return self.convert_object(obj)
+ ret = self._dict_class()
+ ret.fields = {}
+
+ for field_name, field in self.fields.items():
+ field.initialize(parent=self, field_name=field_name)
+ key = self.get_field_key(field_name)
+ value = field.field_to_native(obj, field_name)
+ ret[key] = value
+ ret.fields[key] = field
+ return ret
def from_native(self, data, files):
"""
@@ -329,6 +321,13 @@ class BaseSerializer(Field):
if obj is None:
return None
+ if self.many is not None:
+ many = self.many
+ else:
+ many = hasattr(obj, '__iter__') and not isinstance(obj, Page)
+
+ if many:
+ return [self.to_native(item) for item in obj]
return self.to_native(obj)
@property
@@ -338,9 +337,20 @@ class BaseSerializer(Field):
setting self.object if no errors occurred.
"""
if self._errors is None:
- obj = self.from_native(self.init_data, self.init_files)
+ data, files = self.init_data, self.init_files
+
+ if self.many is not None:
+ many = self.many
+ else:
+ many = hasattr(data, '__iter__') and not isinstance(data, dict)
+
+ # TODO: error data when deserializing lists
+ if many:
+ ret = [self.from_native(item, None) for item in data]
+ ret = self.from_native(data, files)
+
if not self._errors:
- self.object = obj
+ self.object = ret
return self._errors
def is_valid(self):
@@ -348,8 +358,22 @@ class BaseSerializer(Field):
@property
def data(self):
+ """
+ Returns the serialized data on the serializer.
+ """
if self._data is None:
- self._data = self.to_native(self.object)
+ obj = self.object
+
+ if self.many is not None:
+ many = self.many
+ else:
+ many = hasattr(obj, '__iter__') and not isinstance(obj, Page)
+
+ if many:
+ self._data = [self.to_native(item) for item in obj]
+ else:
+ self._data = self.to_native(obj)
+
return self._data
def save(self):
@@ -444,7 +468,7 @@ class ModelSerializer(Serializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
kwargs = {
- 'null': model_field.null or model_field.blank,
+ 'required': not(model_field.null or model_field.blank),
'queryset': model_field.rel.to._default_manager
}
@@ -607,6 +631,8 @@ 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'
@@ -640,7 +666,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
# .using(db).complex_filter(self.rel.limit_choices_to)
rel = model_field.rel.to
kwargs = {
- 'null': model_field.null,
+ 'required': not(model_field.null or model_field.blank),
'queryset': rel._default_manager,
'view_name': self._get_default_view_name(rel)
}
diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py
index f2957abf..76e31476 100644
--- a/rest_framework/tests/relations_hyperlink.py
+++ b/rest_framework/tests/relations_hyperlink.py
@@ -293,7 +293,7 @@ class HyperlinkedForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEquals(serializer.errors, {'target': ['Value may not be null']})
+ self.assertEquals(serializer.errors, {'target': ['This field is required.']})
class HyperlinkedNullableForeignKeyTests(TestCase):
diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py
index b4c2cb5f..c5558ec5 100644
--- a/rest_framework/tests/relations_slug.py
+++ b/rest_framework/tests/relations_slug.py
@@ -149,7 +149,7 @@ class PKForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEquals(serializer.errors, {'target': ['Value may not be null']})
+ self.assertEquals(serializer.errors, {'target': ['This field is required.']})
class SlugNullableForeignKeyTests(TestCase):
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 9697889d..1f46cfc7 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import datetime
import pickle
+from django.utils.datastructures import MultiValueDict
from django.test import TestCase
from rest_framework import serializers
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
@@ -538,7 +539,8 @@ class ManyToManyTests(TestCase):
containing no items, using a representation that does not support
lists (eg form data).
"""
- data = {'rel': ''}
+ data = MultiValueDict()
+ data.setlist('rel', [''])
serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True)
instance = serializer.save()