aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorTom Christie2013-01-30 13:41:56 +0000
committerTom Christie2013-01-30 13:41:56 +0000
commitbe6df3ae3ce18bf4b55ae065ebd34198885e48df (patch)
tree3a96bb6a5075584add7e28c6d8d7f251ad785b4e /rest_framework
parent9a4d01d687d57601d37f9a930d37039cb9f6a6f2 (diff)
parent8021bb5d5089955b171173e60dcc0968e13d29ea (diff)
downloaddjango-rest-framework-be6df3ae3ce18bf4b55ae065ebd34198885e48df.tar.bz2
Merge branch 'master' into many-fields
Conflicts: rest_framework/relations.py
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/fields.py6
-rw-r--r--rest_framework/relations.py34
-rw-r--r--rest_framework/renderers.py5
-rw-r--r--rest_framework/serializers.py36
-rw-r--r--rest_framework/templates/rest_framework/login.html8
-rw-r--r--rest_framework/tests/relations.py14
-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.py27
9 files changed, 97 insertions, 37 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index a788ecf2..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:
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index aee43206..221c72fb 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -17,8 +17,7 @@ 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
@@ -31,13 +30,18 @@ class RelatedField(WritableField):
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)
@@ -56,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)
@@ -138,13 +137,13 @@ class RelatedField(WritableField):
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]
@@ -156,7 +155,7 @@ class RelatedField(WritableField):
class PrimaryKeyRelatedField(RelatedField):
"""
- Represents a to-one relationship as a pk value.
+ Represents a relationship as a pk value.
"""
default_read_only = False
@@ -229,6 +228,9 @@ class PrimaryKeyRelatedField(RelatedField):
class SlugRelatedField(RelatedField):
+ """
+ Represents a relationship using a unique field on the target.
+ """
default_read_only = False
default_error_messages = {
@@ -262,7 +264,7 @@ class SlugRelatedField(RelatedField):
class HyperlinkedRelatedField(RelatedField):
"""
- Represents a to-one relationship, using hyperlinking.
+ Represents a relationship using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 1f6e615f..ed11551b 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -332,10 +332,7 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs['label'] = k
- if getattr(v, 'many', None):
- fields[k] = v.many_form_field_class(**kwargs)
- else:
- fields[k] = v.form_field_class(**kwargs)
+ fields[k] = v.form_field_class(**kwargs)
return fields
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 6ecc7b45..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
}
@@ -469,7 +469,7 @@ class ModelSerializer(Serializer):
kwargs['required'] = False
kwargs['default'] = model_field.get_default()
- if model_field.__class__ == models.TextField:
+ if issubclass(model_field.__class__, models.TextField):
kwargs['widget'] = widgets.Textarea
# TODO: TypedChoiceField?
@@ -513,6 +513,22 @@ class ModelSerializer(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())
+ except ValidationError, err:
+ self._errors = err.message_dict
+ return None
+ return instance
+
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
@@ -544,14 +560,16 @@ class ModelSerializer(Serializer):
else:
instance = self.opts.model(**attrs)
- try:
- instance.full_clean(exclude=self.get_validation_exclusions())
- except ValidationError, err:
- self._errors = err.message_dict
- return None
-
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 instance:
+ return self.full_clean(instance)
+
def save(self):
"""
Save the deserialized object and return it.
@@ -615,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/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html
index 6e2bd8d4..e10ce20f 100644
--- a/rest_framework/templates/rest_framework/login.html
+++ b/rest_framework/templates/rest_framework/login.html
@@ -25,14 +25,14 @@
<form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
{% csrf_token %}
<div id="div_id_username" class="clearfix control-group">
- <div class="controls" style="height: 30px">
- <Label class="span4" style="margin-top: 3px">Username:</label>
+ <div class="controls">
+ <Label class="span4">Username:</label>
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
</div>
</div>
<div id="div_id_password" class="clearfix control-group">
- <div class="controls" style="height: 30px">
- <Label class="span4" style="margin-top: 3px">Password:</label>
+ <div class="controls">
+ <Label class="span4">Password:</label>
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
</div>
</div>
diff --git a/rest_framework/tests/relations.py b/rest_framework/tests/relations.py
index 91daea8a..edc85f9e 100644
--- a/rest_framework/tests/relations.py
+++ b/rest_framework/tests/relations.py
@@ -31,3 +31,17 @@ class FieldTests(TestCase):
field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
self.assertRaises(serializers.ValidationError, field.from_native, '')
self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+
+class TestManyRelateMixin(TestCase):
+ def test_missing_many_to_many_related_field(self):
+ '''
+ Regression test for #632
+
+ https://github.com/tomchristie/django-rest-framework/pull/632
+ '''
+ field = serializers.ManyRelatedField(read_only=False)
+
+ into = {}
+ field.field_from_native({}, None, 'field_name', into)
+ self.assertEqual(into['field_name'], [])
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 4724348e..2cbd0c1a 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -55,6 +55,19 @@ class ActionItemSerializer(serializers.ModelSerializer):
model = ActionItem
+class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
+
+ class Meta:
+ model = ActionItem
+
+ def restore_object(self, data, instance=None):
+ if instance is None:
+ return ActionItem(**data)
+ for key, val in data.items():
+ setattr(instance, key, val)
+ return instance
+
+
class PersonSerializer(serializers.ModelSerializer):
info = serializers.Field(source='info')
@@ -274,6 +287,20 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.is_valid(), False)
self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
+ def test_modelserializer_max_length_exceeded_with_custom_restore(self):
+ """
+ When overriding ModelSerializer.restore_object, validation tests should still apply.
+ Regression test for #623.
+
+ https://github.com/tomchristie/django-rest-framework/pull/623
+ """
+ data = {
+ 'title': 'x' * 201,
+ }
+ serializer = ActionItemSerializerCustomRestore(data=data)
+ self.assertEquals(serializer.is_valid(), False)
+ self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
+
def test_default_modelfield_max_length_exceeded(self):
data = {
'title': 'Testing "info" field...',