diff options
Diffstat (limited to 'rest_framework/relations.py')
| -rw-r--r-- | rest_framework/relations.py | 217 |
1 files changed, 144 insertions, 73 deletions
diff --git a/rest_framework/relations.py b/rest_framework/relations.py index ef465b3c..884b954c 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,3 +1,9 @@ +""" +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 @@ -36,9 +42,9 @@ class RelatedField(WritableField): # 'null' is to be deprecated in favor of 'required' if 'null' in kwargs: - warnings.warn('The `null` keyword argument is due to be deprecated. ' + warnings.warn('The `null` keyword argument is deprecated. ' 'Use the `required` keyword argument instead.', - PendingDeprecationWarning, stacklevel=2) + DeprecationWarning, stacklevel=2) kwargs['required'] = not kwargs.pop('null') self.queryset = kwargs.pop('queryset', None) @@ -243,7 +249,6 @@ class PrimaryKeyRelatedField(RelatedField): 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) @@ -291,10 +296,8 @@ class HyperlinkedRelatedField(RelatedField): """ Represents a relationship using hyperlinking. """ - pk_url_kwarg = 'pk' - slug_field = 'slug' - slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden read_only = False + lookup_field = 'pk' default_error_messages = { 'no_match': _('Invalid hyperlink - No URL match'), @@ -304,69 +307,138 @@ class HyperlinkedRelatedField(RelatedField): 'incorrect_type': _('Incorrect type. Expected url string, received %s.'), } + # These are all pending deprecation + 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 pending deprecation + if 'pk_url_kwarg' in kwargs: + msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' + warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + if 'slug_url_kwarg' in kwargs: + msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' + warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + if 'slug_field' in kwargs: + msg = 'slug_field is pending deprecation. Use lookup_field instead.' + warnings.warn(msg, PendingDeprecationWarning, 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.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) - self.format = kwargs.pop('format', None) super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) - def get_slug_field(self): - """ - Get the name of a slug field to be used to look up by slug. + def get_url(self, obj, view_name, request, format): """ - return self.slug_field - - 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) + Given an object, return the URL that hyperlinks to the object. - if request is None: - warnings.warn("Using `HyperlinkedRelatedField` without including the " - "request in the serializer context is due to be deprecated. " - "Add `context={'request': request}` when instantiating the serializer.", - PendingDeprecationWarning, stacklevel=4) - - pk = getattr(obj, 'pk', None) - if pk is None: - return - kwargs = {self.pk_url_kwarg: pk} + 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 pending deprecation.' \ + 'You should set `lookup_field=slug` on the HyperlinkedRelatedField.' + warnings.warn(msg, PendingDeprecationWarning, 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. - if not slug: - raise Exception('Could not resolve URL for field using view name "%s"' % view_name) + 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() - kwargs = {self.slug_url_kwarg: slug} - try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) - except NoReverseMatch: - pass + return queryset.get(**filter_kwargs) - kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} + 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) + + if request is None: + msg = ( + "Using `HyperlinkedRelatedField` without including the request " + "in the serializer context is deprecated. " + "Add `context={'request': request}` when instantiating " + "the serializer." + ) + warnings.warn(msg, DeprecationWarning, stacklevel=4) + + # 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 reverse(view_name, kwargs=kwargs, request=request, format=format) + return self.get_url(obj, view_name, request, format) except NoReverseMatch: - pass - - raise Exception('Could not resolve URL for field using view name "%s"' % view_name) + 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 - if self.queryset is None: + queryset = self.queryset + if queryset is None: raise Exception('Writable related fields must include a `queryset` argument') try: @@ -390,39 +462,24 @@ class HyperlinkedRelatedField(RelatedField): if match.view_name != self.view_name: raise ValidationError(self.error_messages['incorrect_match']) - pk = match.kwargs.get(self.pk_url_kwarg, None) - slug = match.kwargs.get(self.slug_url_kwarg, None) - - # Try explicit primary key. - if pk is not None: - queryset = self.queryset.filter(pk=pk) - # Next, try looking up by slug. - elif slug is not None: - slug_field = self.get_slug_field() - queryset = self.queryset.filter(**{slug_field: slug}) - # If none of those are defined, it's probably a configuation error. - else: - raise ValidationError(self.error_messages['configuration_error']) - try: - obj = queryset.get() - except ObjectDoesNotExist: + return self.get_object(queryset, match.view_name, + match.args, match.kwargs) + except (ObjectDoesNotExist, TypeError, ValueError): raise ValidationError(self.error_messages['does_not_exist']) - except (TypeError, ValueError): - msg = self.error_messages['incorrect_type'] - raise ValidationError(msg % type(value).__name__) - - return obj class HyperlinkedIdentityField(Field): """ Represents the instance, or a property on the instance, using hyperlinking. """ + lookup_field = 'pk' + read_only = True + + # These are all pending deprecation 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 @@ -431,6 +488,19 @@ class HyperlinkedIdentityField(Field): # Optionally the format of the target hyperlink may be specified self.format = kwargs.pop('format', None) + self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) + + # These are pending deprecation + if 'pk_url_kwarg' in kwargs: + msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' + warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + if 'slug_url_kwarg' in kwargs: + msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' + warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + if 'slug_field' in kwargs: + msg = 'slug_field is pending deprecation. Use lookup_field instead.' + warnings.warn(msg, PendingDeprecationWarning, 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) @@ -442,13 +512,14 @@ class HyperlinkedIdentityField(Field): request = self.context.get('request', None) format = self.context.get('format', None) view_name = self.view_name or self.parent.opts.view_name - kwargs = {self.pk_url_kwarg: obj.pk} + lookup_field = getattr(obj, self.lookup_field) + kwargs = {self.lookup_field: lookup_field} if request is None: warnings.warn("Using `HyperlinkedIdentityField` without including the " - "request in the serializer context is due to be deprecated. " + "request in the serializer context is deprecated. " "Add `context={'request': request}` when instantiating the serializer.", - PendingDeprecationWarning, stacklevel=4) + DeprecationWarning, stacklevel=4) # By default use whatever format is given for the current context # unless the target is a different type to the source. @@ -491,35 +562,35 @@ class HyperlinkedIdentityField(Field): class ManyRelatedField(RelatedField): def __init__(self, *args, **kwargs): - warnings.warn('`ManyRelatedField()` is due to be deprecated. ' + warnings.warn('`ManyRelatedField()` is deprecated. ' 'Use `RelatedField(many=True)` instead.', - PendingDeprecationWarning, stacklevel=2) + DeprecationWarning, 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. ' + warnings.warn('`ManyPrimaryKeyRelatedField()` is deprecated. ' 'Use `PrimaryKeyRelatedField(many=True)` instead.', - PendingDeprecationWarning, stacklevel=2) + DeprecationWarning, 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. ' + warnings.warn('`ManySlugRelatedField()` is deprecated. ' 'Use `SlugRelatedField(many=True)` instead.', - PendingDeprecationWarning, stacklevel=2) + DeprecationWarning, 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. ' + warnings.warn('`ManyHyperlinkedRelatedField()` is deprecated. ' 'Use `HyperlinkedRelatedField(many=True)` instead.', - PendingDeprecationWarning, stacklevel=2) + DeprecationWarning, stacklevel=2) kwargs['many'] = True super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs) |
