diff options
| -rw-r--r-- | rest_framework/fields.py | 38 | ||||
| -rw-r--r-- | rest_framework/relations.py | 235 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 1 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 88 | ||||
| -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 | 4 |
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() |
