aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--rest_framework/fields.py12
-rw-r--r--rest_framework/relations.py206
-rw-r--r--rest_framework/renderers.py1
-rw-r--r--rest_framework/serializers.py4
-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, 99 insertions, 132 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 998911e1..d6689c4e 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -32,6 +32,7 @@ class Field(object):
creation_counter = 0
empty = ''
type_name = None
+ partial = False
_use_files = None
form_field_class = forms.CharField
@@ -53,7 +54,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):
@@ -186,7 +188,7 @@ class WritableField(Field):
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:
@@ -325,7 +327,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):
@@ -612,7 +615,8 @@ class ImageField(FileField):
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 dc0a73e6..221c72fb 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -17,19 +17,31 @@ 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
+ many = False
def __init__(self, *args, **kwargs):
+
+ # 'null' will be deprecated in favor of 'required'
+ if 'null' in kwargs:
+ 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)
super(RelatedField, self).__init__(*args, **kwargs)
self.read_only = kwargs.pop('read_only', self.default_read_only)
+ if self.many:
+ self.widget = self.many_widget
+ self.form_field_class = self.many_form_field_class
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
@@ -48,11 +60,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)
@@ -108,6 +115,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):
@@ -115,65 +125,39 @@ class RelatedField(WritableField):
return
try:
- value = data[field_name]
+ if self.many:
+ try:
+ # Form data
+ value = data.getlist(field_name)
+ if value == ['']:
+ value = []
+ 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
default_error_messages = {
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
@@ -213,79 +197,41 @@ 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_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
-
- 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_unicode(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):
+ """
+ Represents a relationship using a unique field on the target.
+ """
default_read_only = False
- form_field_class = forms.ChoiceField
default_error_messages = {
'does_not_exist': _("Object with %s=%s does not exist."),
@@ -314,21 +260,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
default_error_messages = {
'no_match': _('Invalid hyperlink - No URL match'),
@@ -442,13 +383,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.
@@ -512,3 +446,29 @@ 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):
+ kwargs['many'] = True
+ super(ManyRelatedField, self).__init__(*args, **kwargs)
+
+
+class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField):
+ def __init__(self, *args, **kwargs):
+ kwargs['many'] = True
+ super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs)
+
+
+class ManySlugRelatedField(SlugRelatedField):
+ def __init__(self, *args, **kwargs):
+ kwargs['many'] = True
+ super(ManySlugRelatedField, self).__init__(*args, **kwargs)
+
+
+class ManyHyperlinkedRelatedField(HyperlinkedRelatedField):
+ def __init__(self, *args, **kwargs):
+ kwargs['many'] = True
+ super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 0a34abaa..ed11551b 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -333,6 +333,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 4fb802a7..d02e1ada 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -443,7 +443,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
}
@@ -633,7 +633,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 6d137f68..7bc36dee 100644
--- a/rest_framework/tests/relations_hyperlink.py
+++ b/rest_framework/tests/relations_hyperlink.py
@@ -291,7 +291,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': [u'Value may not be null']})
+ self.assertEquals(serializer.errors, {'target': [u'This field is required.']})
class HyperlinkedNullableForeignKeyTests(TestCase):
diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py
index 37ccc75e..34d1f82a 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': [u'Value may not be null']})
+ self.assertEquals(serializer.errors, {'target': [u'This field is required.']})
class SlugNullableForeignKeyTests(TestCase):
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 48b4f1ab..2cbd0c1a 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,5 +1,6 @@
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,
@@ -536,7 +537,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()