diff options
| -rw-r--r-- | docs/api-guide/relations.md | 67 | ||||
| -rw-r--r-- | rest_framework/relations.py | 147 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 3 | 
3 files changed, 136 insertions, 81 deletions
| diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 623fe1a9..756e1562 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -123,9 +123,9 @@ Would serialize to a representation like this:          'album_name': 'Graceland',          'artist': 'Paul Simon'          'tracks': [ -            'http://www.example.com/api/tracks/45', -            'http://www.example.com/api/tracks/46', -            'http://www.example.com/api/tracks/47', +            'http://www.example.com/api/tracks/45/', +            'http://www.example.com/api/tracks/46/', +            'http://www.example.com/api/tracks/47/',              ...          ]      } @@ -138,9 +138,7 @@ By default this field is read-write, although you can change this behavior using  * `many` - If applied to a to-many relationship, you should set this argument to `True`.  * `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.  * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship.  `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. -* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. -* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. -* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. +* `lookup_field` - The field on the target that should be used for the lookup.  Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.  * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.  ## SlugRelatedField @@ -196,7 +194,7 @@ Would serialize to a representation like this:      {          'album_name': 'The Eraser',          'artist': 'Thom Yorke' -        'track_listing': 'http://www.example.com/api/track_list/12', +        'track_listing': 'http://www.example.com/api/track_list/12/',      }  This field is always read-only. @@ -291,32 +289,23 @@ This custom field would then serialize to the following representation.  ## Reverse relations -Note that reverse relationships are not automatically generated by the `ModelSerializer` and `HyperlinkedModelSerializer` classes.  To include a reverse relationship, you cannot simply add it to the fields list. - -**The following will not work:** +Note that reverse relationships are not automatically included by the `ModelSerializer` and `HyperlinkedModelSerializer` classes.  To include a reverse relationship, you must explicitly add it to the fields list.  For example:      class AlbumSerializer(serializers.ModelSerializer):          class Meta: -            fields = ('tracks', ...)  -            -Instead, you must explicitly add it to the serializer.  For example: - -    class AlbumSerializer(serializers.ModelSerializer): -        tracks = serializers.PrimaryKeyRelatedField(many=True) -        ... - -By default, the field will uses the same accessor as it's field name to retrieve the relationship, so in this example, `Album` instances would need to have the `tracks` attribute for this relationship to work. +            fields = ('tracks', ...) -The best way to ensure this is typically to make sure that the relationship on the model definition has it's `related_name` argument properly set.  For example: +You'll normally want to ensure that you've set an appropriate `related_name` argument on the relationship, that you can use as the field name.  For example:      class Track(models.Model):          album = models.ForeignKey(Album, related_name='tracks')          ... -Alternatively, you can use the `source` argument on the serializer field, to use a different accessor attribute than the field name.  For example. +If you have not set a related name for the reverse relationship, you'll need to use the automatically generated related name in the `fields` argument.  For example:      class AlbumSerializer(serializers.ModelSerializer): -        tracks = serializers.PrimaryKeyRelatedField(many=True, source='track_set') +        class Meta: +            fields = ('track_set', ...)   See the Django documentation on [reverse relationships][reverse-relationships] for more details. @@ -394,6 +383,40 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can  For more information see [the Django documentation on generic relations][generic-relations]. +## Advanced Hyperlinked fields + +If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.  + +There are two methods you'll need to override. + +#### get_url(self, obj, view_name, request, format) + +This method should return the URL that corresponds to the given object. + +May raise a `NoReverseMatch` if the `view_name` and `lookup_field` +attributes are not configured to correctly match the URL conf. + +#### get_object(self, queryset, view_name, view_args, view_kwargs) + + +This method should the object that corresponds to the matched URL conf arguments. + +May raise an `ObjectDoesNotExist` exception. + +### Example + +For example, if all your object URLs used both a account and a slug in the the URL to reference the object, you might create a custom field like this:  + +    class CustomHyperlinkedField(serializers.HyperlinkedRelatedField): +        def get_url(self, obj, view_name, request, format): +            kwargs = {'account': obj.account, 'slug': obj.slug} +            return reverse(view_name, kwargs=kwargs, request=request, format=format) + +        def get_object(self, queryset, view_name, view_args, view_kwargs): +            account = view_kwargs['account'] +            slug = view_kwargs['slug'] +            return queryset.get(account=account, slug=sug) +  ---  ## Deprecated APIs diff --git a/rest_framework/relations.py b/rest_framework/relations.py index abe5203b..6d8deec1 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -288,10 +288,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'), @@ -301,69 +299,120 @@ 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 +        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): +    def get_url(self, obj, view_name, request, format):          """ -        Get the name of a slug field to be used to look up by slug. -        """ -        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) - -        if request is None: -            warnings.warn("Using `HyperlinkedRelatedField` without including the " -                          "request in the serializer context is deprecated. " -                          "Add `context={'request': request}` when instantiating the serializer.", -                          DeprecationWarning, stacklevel=4) +        Given an object, return the URL that hyperlinks to the object. -        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: +                return reverse(view_name, kwargs=kwargs, request=request, format=format) +            except NoReverseMatch: +                pass -        if not slug: -            raise Exception('Could not resolve URL for field using view name "%s"' % view_name) +        raise NoReverseMatch() -        kwargs = {self.slug_url_kwarg: slug} -        try: -            return reverse(view_name, kwargs=kwargs, request=request, format=format) -        except NoReverseMatch: -            pass +    def get_object(self, queryset, view_name, view_args, view_kwargs): +        """ +        Return the object corresponding to a matched URL. -        kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} +        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) + +        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: @@ -387,29 +436,11 @@ 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): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b589eca8..d4b34c01 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -836,6 +836,7 @@ class HyperlinkedModelSerializer(ModelSerializer):      """      _options_class = HyperlinkedModelSerializerOptions      _default_view_name = '%(model_name)s-detail' +    _hyperlink_field_class = HyperlinkedRelatedField      url = HyperlinkedIdentityField() @@ -874,7 +875,7 @@ class HyperlinkedModelSerializer(ModelSerializer):          if model_field:              kwargs['required'] = not(model_field.null or model_field.blank) -        return HyperlinkedRelatedField(**kwargs) +        return self._hyperlink_field_class(**kwargs)      def get_identity(self, data):          """ | 
