aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/relations.py
diff options
context:
space:
mode:
authorTom Christie2013-02-06 00:53:41 -0800
committerTom Christie2013-02-06 00:53:41 -0800
commit72fe686623f63b79fa2b7f7de177c6d98bc1da3f (patch)
tree5d0834d4eb2caf21ed54a4dcba9a4a3a40e6fa99 /rest_framework/relations.py
parent221e77d3575c182eb49d50546f844f392a5f7ba6 (diff)
parentbdc97c561147130b59c8cd8cc6bc735eab8b223d (diff)
downloaddjango-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.py253
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)