diff options
| author | Tom Christie | 2013-02-06 00:53:41 -0800 |
|---|---|---|
| committer | Tom Christie | 2013-02-06 00:53:41 -0800 |
| commit | 72fe686623f63b79fa2b7f7de177c6d98bc1da3f (patch) | |
| tree | 5d0834d4eb2caf21ed54a4dcba9a4a3a40e6fa99 /rest_framework/relations.py | |
| parent | 221e77d3575c182eb49d50546f844f392a5f7ba6 (diff) | |
| parent | bdc97c561147130b59c8cd8cc6bc735eab8b223d (diff) | |
| download | django-rest-framework-72fe686623f63b79fa2b7f7de177c6d98bc1da3f.tar.bz2 | |
Merge pull request #636 from tomchristie/2.2
2.2 Release
Diffstat (limited to 'rest_framework/relations.py')
| -rw-r--r-- | rest_framework/relations.py | 253 |
1 files changed, 117 insertions, 136 deletions
diff --git a/rest_framework/relations.py b/rest_framework/relations.py index dc0a73e6..ae4ef6b3 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,13 +1,16 @@ +from __future__ import unicode_literals from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve, get_script_prefix from django import forms from django.forms import widgets from django.forms.models import ModelChoiceIterator -from django.utils.encoding import smart_unicode 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 ##### @@ -17,19 +20,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) @@ -48,11 +67,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) @@ -60,8 +74,8 @@ class RelatedField(WritableField): """ Return a readable representation for use with eg. select widgets. """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj)) + desc = smart_text(obj) + ident = smart_text(self.to_native(obj)) if desc == ident: return desc return "%s - %s" % (desc, ident) @@ -108,6 +122,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 +132,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."), @@ -188,8 +179,8 @@ class PrimaryKeyRelatedField(RelatedField): """ Return a readable representation for use with eg. select widgets. """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj.pk)) + desc = smart_text(obj) + ident = smart_text(self.to_native(obj.pk)) if desc == ident: return desc return "%s - %s" % (desc, ident) @@ -205,7 +196,7 @@ class PrimaryKeyRelatedField(RelatedField): try: return self.queryset.get(pk=data) except ObjectDoesNotExist: - msg = self.error_messages['does_not_exist'] % smart_unicode(data) + msg = self.error_messages['does_not_exist'] % smart_text(data) raise ValidationError(msg) except (TypeError, ValueError): received = type(data).__name__ @@ -213,79 +204,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_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()] - - def from_native(self, data): - if self.queryset is None: - raise Exception('Writable related fields must include a `queryset` argument') + return self.to_native(pk) - 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): - 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."), @@ -308,27 +262,22 @@ class SlugRelatedField(RelatedField): return self.queryset.get(**{self.slug_field: data}) except ObjectDoesNotExist: raise ValidationError(self.error_messages['does_not_exist'] % - (self.slug_field, unicode(data))) + (self.slug_field, smart_text(data))) except (TypeError, ValueError): msg = self.error_messages['invalid'] 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'), @@ -404,7 +353,7 @@ class HyperlinkedRelatedField(RelatedField): if http_prefix: # If needed convert absolute URLs to relative path - value = urlparse(value).path + value = urlparse.urlparse(value).path prefix = get_script_prefix() if value.startswith(prefix): value = '/' + value[len(prefix):] @@ -442,13 +391,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. @@ -456,6 +398,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 @@ -512,3 +455,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) |
