mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-26 03:23:59 +03:00
596 lines
22 KiB
Python
596 lines
22 KiB
Python
"""
|
|
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
|
|
from django import forms
|
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
|
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
|
|
from rest_framework.reverse import reverse
|
|
from rest_framework.compat import urlparse
|
|
from rest_framework.compat import smart_text
|
|
import warnings
|
|
|
|
|
|
# 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.
|
|
"""
|
|
widget = widgets.Select
|
|
many_widget = widgets.SelectMultiple
|
|
form_field_class = forms.ChoiceField
|
|
many_form_field_class = forms.MultipleChoiceField
|
|
null_values = (None, '', 'None')
|
|
|
|
cache_choices = False
|
|
empty_label = None
|
|
read_only = True
|
|
many = False
|
|
|
|
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
|
|
|
|
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
|
|
super(RelatedField, self).__init__(*args, **kwargs)
|
|
|
|
if not self.required:
|
|
# Accessed in ModelChoiceIterator django/forms/models.py:1034
|
|
# If set adds empty choice.
|
|
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
|
|
|
self.queryset = queryset
|
|
|
|
def initialize(self, parent, field_name):
|
|
super(RelatedField, self).initialize(parent, field_name)
|
|
if self.queryset is None and not self.read_only:
|
|
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()
|
|
|
|
# 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))
|
|
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)
|
|
|
|
# Default value handling
|
|
|
|
def get_default_value(self):
|
|
default = super(RelatedField, self).get_default_value()
|
|
if self.many and default is None:
|
|
return []
|
|
return default
|
|
|
|
# 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]
|
|
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)
|
|
if value == [''] or value == []:
|
|
raise KeyError
|
|
except AttributeError:
|
|
# Non-form data
|
|
value = data[field_name]
|
|
else:
|
|
value = data[field_name]
|
|
except KeyError:
|
|
if self.partial:
|
|
return
|
|
value = self.get_default_value()
|
|
|
|
if value in self.null_values:
|
|
if self.required:
|
|
raise ValidationError(self.error_messages['required'])
|
|
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)
|
|
|
|
|
|
# PrimaryKey relationships
|
|
|
|
class PrimaryKeyRelatedField(RelatedField):
|
|
"""
|
|
Represents a relationship as a pk value.
|
|
"""
|
|
read_only = False
|
|
|
|
default_error_messages = {
|
|
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
|
|
'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
|
|
}
|
|
|
|
# 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))
|
|
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
|
|
raise ValidationError(msg)
|
|
|
|
def field_to_native(self, obj, field_name):
|
|
if self.many:
|
|
# To-many relationship
|
|
|
|
queryset = None
|
|
if not self.source:
|
|
# Prefer obj.serializable_value for performance reasons
|
|
try:
|
|
queryset = obj.serializable_value(field_name)
|
|
except AttributeError:
|
|
pass
|
|
if queryset is None:
|
|
# 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)
|
|
|
|
# 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]
|
|
|
|
# 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:
|
|
pk = getattr(obj, self.source or field_name).pk
|
|
except (ObjectDoesNotExist, AttributeError):
|
|
return None
|
|
|
|
# Forward relationship
|
|
return self.to_native(pk)
|
|
|
|
|
|
# Slug relationships
|
|
|
|
class SlugRelatedField(RelatedField):
|
|
"""
|
|
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."),
|
|
'invalid': _('Invalid value.'),
|
|
}
|
|
|
|
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'] %
|
|
(self.slug_field, smart_text(data)))
|
|
except (TypeError, ValueError):
|
|
msg = self.error_messages['invalid']
|
|
raise ValidationError(msg)
|
|
|
|
|
|
# Hyperlinked relationships
|
|
|
|
class HyperlinkedRelatedField(RelatedField):
|
|
"""
|
|
Represents a relationship using hyperlinking.
|
|
"""
|
|
read_only = False
|
|
lookup_field = 'pk'
|
|
|
|
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.'),
|
|
}
|
|
|
|
# These are all deprecated
|
|
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:
|
|
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
|
|
|
|
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
|
|
self.format = kwargs.pop('format', None)
|
|
|
|
# These are deprecated
|
|
if 'pk_url_kwarg' in kwargs:
|
|
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
if 'slug_url_kwarg' in kwargs:
|
|
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
if 'slug_field' in kwargs:
|
|
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)
|
|
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):
|
|
"""
|
|
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}
|
|
try:
|
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
except NoReverseMatch:
|
|
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
|
|
|
|
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:
|
|
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.
|
|
msg = 'Implicit slug field hyperlinked fields are deprecated.' \
|
|
'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
return ret
|
|
except NoReverseMatch:
|
|
pass
|
|
|
|
raise NoReverseMatch()
|
|
|
|
def get_object(self, queryset, view_name, view_args, view_kwargs):
|
|
"""
|
|
Return the object corresponding to a matched URL.
|
|
|
|
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()
|
|
|
|
return queryset.get(**filter_kwargs)
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
# 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 from_native(self, value):
|
|
# Convert URL -> model instance pk
|
|
# TODO: Use values_list
|
|
queryset = self.queryset
|
|
if queryset is None:
|
|
raise Exception('Writable related fields must include a `queryset` argument')
|
|
|
|
try:
|
|
http_prefix = value.startswith(('http:', 'https:'))
|
|
except AttributeError:
|
|
msg = self.error_messages['incorrect_type']
|
|
raise ValidationError(msg % type(value).__name__)
|
|
|
|
if http_prefix:
|
|
# If needed convert absolute URLs to relative path
|
|
value = urlparse.urlparse(value).path
|
|
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'])
|
|
|
|
if match.view_name != self.view_name:
|
|
raise ValidationError(self.error_messages['incorrect_match'])
|
|
|
|
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'])
|
|
|
|
|
|
class HyperlinkedIdentityField(Field):
|
|
"""
|
|
Represents the instance, or a property on the instance, using hyperlinking.
|
|
"""
|
|
lookup_field = 'pk'
|
|
read_only = True
|
|
|
|
# These are all deprecated
|
|
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)
|
|
|
|
self.format = kwargs.pop('format', None)
|
|
lookup_field = kwargs.pop('lookup_field', None)
|
|
self.lookup_field = lookup_field or self.lookup_field
|
|
|
|
# These are deprecated
|
|
if 'pk_url_kwarg' in kwargs:
|
|
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
if 'slug_url_kwarg' in kwargs:
|
|
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
if 'slug_field' in kwargs:
|
|
msg = 'slug_field is deprecated. Use lookup_field instead.'
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
|
|
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
|
|
|
|
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
|
|
|
|
try:
|
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
except NoReverseMatch:
|
|
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
|
|
|
|
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
|
|
|
|
raise NoReverseMatch()
|