aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api-guide/relations.md67
-rw-r--r--rest_framework/relations.py147
-rw-r--r--rest_framework/serializers.py3
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):
"""