diff options
| author | Tom Christie | 2013-01-30 13:41:56 +0000 |
|---|---|---|
| committer | Tom Christie | 2013-01-30 13:41:56 +0000 |
| commit | be6df3ae3ce18bf4b55ae065ebd34198885e48df (patch) | |
| tree | 3a96bb6a5075584add7e28c6d8d7f251ad785b4e /rest_framework | |
| parent | 9a4d01d687d57601d37f9a930d37039cb9f6a6f2 (diff) | |
| parent | 8021bb5d5089955b171173e60dcc0968e13d29ea (diff) | |
| download | django-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.py | 6 | ||||
| -rw-r--r-- | rest_framework/relations.py | 34 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 5 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 36 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/login.html | 8 | ||||
| -rw-r--r-- | rest_framework/tests/relations.py | 14 | ||||
| -rw-r--r-- | rest_framework/tests/relations_hyperlink.py | 2 | ||||
| -rw-r--r-- | rest_framework/tests/relations_slug.py | 2 | ||||
| -rw-r--r-- | rest_framework/tests/serializer.py | 27 |
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...', |
