django-rest-framework/rest_framework/relations.py

592 lines
22 KiB
Python
Raw Normal View History

2013-04-25 15:47:34 +04:00
"""
Serializer fields that deal with relationships.
These fields allow you to specify the style that should be used to represent
model relationships, including hyperlinks, primary keys, or slugs.
"""
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
2012-12-31 12:53:40 +04:00
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
2012-12-31 12:53:40 +04:00
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
2012-12-31 12:53:40 +04:00
from rest_framework.reverse import reverse
from rest_framework.compat import urlparse
from rest_framework.compat import smart_text
2013-01-31 00:33:50 +04:00
import warnings
2012-12-31 12:53:40 +04:00
##### Relational fields #####
# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
"""
Base class for related model fields.
This represents a relationship using the unicode representation of the target.
2012-12-31 12:53:40 +04:00
"""
widget = widgets.Select
many_widget = widgets.SelectMultiple
2013-01-30 16:41:18 +04:00
form_field_class = forms.ChoiceField
many_form_field_class = forms.MultipleChoiceField
2012-12-31 12:53:40 +04:00
cache_choices = False
empty_label = None
2013-01-31 00:33:50 +04:00
read_only = True
many = False
2012-12-31 12:53:40 +04:00
def __init__(self, *args, **kwargs):
queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many)
if self.many:
self.widget = self.many_widget
self.form_field_class = self.many_form_field_class
2012-12-31 12:53:40 +04:00
2013-01-31 00:33:50 +04:00
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
super(RelatedField, self).__init__(*args, **kwargs)
if not self.required:
self.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset
2012-12-31 12:53:40 +04:00
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only:
try:
manager = getattr(self.parent.opts.model, self.source or field_name)
if hasattr(manager, 'related'): # Forward
self.queryset = manager.related.model._default_manager.all()
else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all()
except Exception:
2012-12-31 12:53:40 +04:00
msg = ('Serializer related fields must include a `queryset`' +
' argument or set `read_only=True')
raise Exception(msg)
### We need this stuff to make form choices work...
def prepare_value(self, obj):
return self.to_native(obj)
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))
2012-12-31 12:53:40 +04:00
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
queryset = property(_get_queryset, _set_queryset)
def _get_choices(self):
# If self._choices is set, then somebody must have manually set
# the property self.choices. In this case, just return self._choices.
if hasattr(self, '_choices'):
return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
# time _get_choices() is called (and, thus, each time self.choices is
# accessed) so that we can ensure the QuerySet has not been consumed. This
# construct might look complicated but it allows for lazy evaluation of
# the queryset.
return ModelChoiceIterator(self)
def _set_choices(self, value):
# Setting choices also sets the choices on the widget.
# choices can be any iterable, but we call list() on it because
# it will be consumed more than once.
self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
### Regular serializer stuff...
def field_to_native(self, obj, field_name):
try:
if self.source == '*':
return self.to_native(obj)
source = self.source or field_name
value = obj
for component in source.split('.'):
if value is None:
break
value = get_component(value, component)
except ObjectDoesNotExist:
return None
if value is None:
return None
if self.many:
if is_simple_callable(getattr(value, 'all', None)):
return [self.to_native(item) for item in value.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item) for item in value]
2012-12-31 12:53:40 +04:00
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
try:
if self.many:
try:
# Form data
value = data.getlist(field_name)
2013-01-30 18:21:18 +04:00
if value == [''] or value == []:
raise KeyError
except AttributeError:
# Non-form data
value = data[field_name]
else:
value = data[field_name]
2012-12-31 12:53:40 +04:00
except KeyError:
if self.partial:
return
value = [] if self.many else None
2012-12-31 12:53:40 +04:00
if value in (None, '') and self.required:
raise ValidationError(self.error_messages['required'])
elif value in (None, ''):
2012-12-31 12:53:40 +04:00
into[(self.source or field_name)] = None
elif self.many:
into[(self.source or field_name)] = [self.from_native(item) for item in value]
2012-12-31 12:53:40 +04:00
else:
into[(self.source or field_name)] = self.from_native(value)
### PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField):
"""
Represents a relationship as a pk value.
2012-12-31 12:53:40 +04:00
"""
2013-01-31 00:33:50 +04:00
read_only = False
2012-12-31 12:53:40 +04:00
default_error_messages = {
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
}
2012-12-31 12:53:40 +04:00
# TODO: Remove these field hacks...
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))
2012-12-31 12:53:40 +04:00
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
# TODO: Possibly change this to just take `obj`, through prob less performant
def to_native(self, pk):
return 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
2012-12-31 12:53:40 +04:00
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
2013-01-30 16:41:18 +04:00
if self.many:
# To-many relationship
queryset = None
if not self.source:
2013-01-30 16:41:18 +04:00
# Prefer obj.serializable_value for performance reasons
try:
queryset = obj.serializable_value(field_name)
except AttributeError:
pass
if queryset is None:
2013-01-30 16:41:18 +04:00
# RelatedManager (reverse relationship)
source = self.source or field_name
queryset = obj
for component in source.split('.'):
if queryset is None:
return []
queryset = get_component(queryset, component)
2013-01-30 16:41:18 +04:00
# Forward relationship
if is_simple_callable(getattr(queryset, 'all', None)):
return [self.to_native(item.pk) for item in queryset.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item.pk) for item in queryset]
2013-01-30 16:41:18 +04:00
# To-one relationship
2012-12-31 12:53:40 +04:00
try:
# Prefer obj.serializable_value for performance reasons
pk = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedObject (reverse relationship)
try:
2013-01-30 16:41:18 +04:00
pk = getattr(obj, self.source or field_name).pk
except (ObjectDoesNotExist, AttributeError):
return None
2012-12-31 12:53:40 +04:00
# Forward relationship
2013-01-30 16:41:18 +04:00
return self.to_native(pk)
2012-12-31 12:53:40 +04:00
### Slug relationships
class SlugRelatedField(RelatedField):
"""
Represents a relationship using a unique field on the target.
"""
2013-01-31 00:33:50 +04:00
read_only = False
2012-12-31 12:53:40 +04:00
default_error_messages = {
'does_not_exist': _("Object with %s=%s does not exist."),
'invalid': _('Invalid value.'),
}
2012-12-31 12:53:40 +04:00
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
assert self.slug_field, 'slug_field is required'
super(SlugRelatedField, self).__init__(*args, **kwargs)
def to_native(self, obj):
return getattr(obj, self.slug_field)
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(**{self.slug_field: data})
except ObjectDoesNotExist:
raise ValidationError(self.error_messages['does_not_exist'] %
2013-02-01 18:03:28 +04:00
(self.slug_field, smart_text(data)))
except (TypeError, ValueError):
msg = self.error_messages['invalid']
raise ValidationError(msg)
2012-12-31 12:53:40 +04:00
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
Represents a relationship using hyperlinking.
2012-12-31 12:53:40 +04:00
"""
2013-01-31 00:33:50 +04:00
read_only = False
lookup_field = 'pk'
2012-12-31 12:53:40 +04:00
default_error_messages = {
'no_match': _('Invalid hyperlink - No URL match'),
'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
'configuration_error': _('Invalid hyperlink due to configuration error'),
'does_not_exist': _("Invalid hyperlink - object does not exist."),
'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
}
2013-06-27 23:36:14 +04:00
# These are all deprecated
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
2012-12-31 12:53:40 +04:00
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except KeyError:
2012-12-31 12:53:40 +04:00
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.format = kwargs.pop('format', None)
2013-06-27 23:36:14 +04:00
# These are deprecated
if 'pk_url_kwarg' in kwargs:
2013-06-27 23:36:14 +04:00
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_url_kwarg' in kwargs:
2013-06-27 23:36:14 +04:00
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_field' in kwargs:
2013-06-27 23:36:14 +04:00
msg = 'slug_field is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
2012-12-31 12:53:40 +04:00
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def get_url(self, obj, view_name, request, format):
2012-12-31 12:53:40 +04:00
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
lookup_field = getattr(obj, self.lookup_field)
kwargs = {self.lookup_field: lookup_field}
2012-12-31 12:53:40 +04:00
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
2012-12-31 12:53:40 +04:00
pass
if self.pk_url_kwarg != 'pk':
# Only try pk if it has been explicitly set.
# Otherwise, the default `lookup_field = 'pk'` has us covered.
pk = obj.pk
kwargs = {self.pk_url_kwarg: pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
2012-12-31 12:53:40 +04:00
slug = getattr(obj, self.slug_field, None)
if slug is not None:
# Only try slug if it corresponds to an attribute on the object.
kwargs = {self.slug_url_kwarg: slug}
try:
2013-05-05 19:47:45 +04:00
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
# If the lookup succeeds using the default slug params,
# then `slug_field` is being used implicitly, and we
# we need to warn about the pending deprecation.
2013-06-27 23:36:14 +04:00
msg = 'Implicit slug field hyperlinked fields are deprecated.' \
2013-05-05 19:47:45 +04:00
'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
2013-06-27 23:36:14 +04:00
warnings.warn(msg, DeprecationWarning, stacklevel=2)
2013-05-05 19:47:45 +04:00
return ret
except NoReverseMatch:
pass
2012-12-31 12:53:40 +04:00
raise NoReverseMatch()
2012-12-31 12:53:40 +04:00
def get_object(self, queryset, view_name, view_args, view_kwargs):
"""
Return the object corresponding to a matched URL.
2012-12-31 12:53:40 +04:00
Takes the matched URL conf arguments, and the queryset, and should
return an object instance, or raise an `ObjectDoesNotExist` exception.
"""
lookup = view_kwargs.get(self.lookup_field, None)
pk = view_kwargs.get(self.pk_url_kwarg, None)
slug = view_kwargs.get(self.slug_url_kwarg, None)
if lookup is not None:
filter_kwargs = {self.lookup_field: lookup}
elif pk is not None:
filter_kwargs = {'pk': pk}
elif slug is not None:
filter_kwargs = {self.slug_field: slug}
else:
raise ObjectDoesNotExist()
2012-12-31 12:53:40 +04:00
return queryset.get(**filter_kwargs)
2012-12-31 12:53:40 +04:00
def to_native(self, obj):
view_name = self.view_name
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
2013-06-27 23:29:52 +04:00
assert request is not None, (
"`HyperlinkedRelatedField` requires the request in the serializer "
"context. Add `context={'request': request}` when instantiating "
"the serializer."
)
# If the object has not yet been saved then we cannot hyperlink to it.
if getattr(obj, 'pk', None) is None:
return
2012-12-31 12:53:40 +04:00
# Return the hyperlink, or error if incorrectly configured.
2012-12-31 12:53:40 +04:00
try:
return self.get_url(obj, view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
'view name "%s". You may have failed to include the related '
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
raise Exception(msg % view_name)
2012-12-31 12:53:40 +04:00
def from_native(self, value):
# Convert URL -> model instance pk
# TODO: Use values_list
queryset = self.queryset
if queryset is None:
2012-12-31 12:53:40 +04:00
raise Exception('Writable related fields must include a `queryset` argument')
try:
2013-05-18 18:09:24 +04:00
http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
msg = self.error_messages['incorrect_type']
raise ValidationError(msg % type(value).__name__)
if http_prefix:
2012-12-31 12:53:40 +04:00
# If needed convert absolute URLs to relative path
value = urlparse.urlparse(value).path
2012-12-31 12:53:40 +04:00
prefix = get_script_prefix()
if value.startswith(prefix):
value = '/' + value[len(prefix):]
try:
match = resolve(value)
except Exception:
raise ValidationError(self.error_messages['no_match'])
2012-12-31 12:53:40 +04:00
if match.view_name != self.view_name:
raise ValidationError(self.error_messages['incorrect_match'])
2012-12-31 12:53:40 +04:00
try:
return self.get_object(queryset, match.view_name,
match.args, match.kwargs)
except (ObjectDoesNotExist, TypeError, ValueError):
raise ValidationError(self.error_messages['does_not_exist'])
2012-12-31 12:53:40 +04:00
class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
"""
lookup_field = 'pk'
read_only = True
2013-06-27 23:36:14 +04:00
# These are all deprecated
2012-12-31 12:53:40 +04:00
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except KeyError:
msg = "HyperlinkedIdentityField requires 'view_name' argument"
raise ValueError(msg)
2012-12-31 12:53:40 +04:00
self.format = kwargs.pop('format', None)
lookup_field = kwargs.pop('lookup_field', None)
self.lookup_field = lookup_field or self.lookup_field
2013-06-27 23:36:14 +04:00
# These are deprecated
if 'pk_url_kwarg' in kwargs:
2013-06-27 23:36:14 +04:00
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_url_kwarg' in kwargs:
2013-06-27 23:36:14 +04:00
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_field' in kwargs:
2013-06-27 23:36:14 +04:00
msg = 'slug_field is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
2012-12-31 12:53:40 +04:00
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
format = self.context.get('format', None)
view_name = self.view_name
2013-06-27 23:29:52 +04:00
assert request is not None, (
"`HyperlinkedIdentityField` requires the request in the serializer"
" context. Add `context={'request': request}` when instantiating "
"the serializer."
)
# By default use whatever format is given for the current context
# unless the target is a different type to the source.
#
# Eg. Consider a HyperlinkedIdentityField pointing from a json
# representation to an html property of that representation...
#
# '/snippets/1/' should link to '/snippets/1/highlight/'
# ...but...
# '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
if format and self.format and self.format != format:
format = self.format
# Return the hyperlink, or error if incorrectly configured.
try:
return self.get_url(obj, view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
'view name "%s". You may have failed to include the related '
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
raise Exception(msg % view_name)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
lookup_field = getattr(obj, self.lookup_field, None)
kwargs = {self.lookup_field: lookup_field}
# Handle unsaved object case
if lookup_field is None:
return None
2012-12-31 12:53:40 +04:00
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
2012-12-31 12:53:40 +04:00
pass
if self.pk_url_kwarg != 'pk':
# Only try pk lookup if it has been explicitly set.
# Otherwise, the default `lookup_field = 'pk'` has us covered.
kwargs = {self.pk_url_kwarg: obj.pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
2012-12-31 12:53:40 +04:00
slug = getattr(obj, self.slug_field, None)
if slug:
# Only use slug lookup if a slug field exists on the model
kwargs = {self.slug_url_kwarg: slug}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
2012-12-31 12:53:40 +04:00
raise NoReverseMatch()