mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-02 20:54:42 +03:00
lookup_field on hyperlinked fields, and overriddable hyperlinked fields. Closes #688
This commit is contained in:
parent
22af28d146
commit
35f99cddc4
|
@ -123,9 +123,9 @@ Would serialize to a representation like this:
|
||||||
'album_name': 'Graceland',
|
'album_name': 'Graceland',
|
||||||
'artist': 'Paul Simon'
|
'artist': 'Paul Simon'
|
||||||
'tracks': [
|
'tracks': [
|
||||||
'http://www.example.com/api/tracks/45',
|
'http://www.example.com/api/tracks/45/',
|
||||||
'http://www.example.com/api/tracks/46',
|
'http://www.example.com/api/tracks/46/',
|
||||||
'http://www.example.com/api/tracks/47',
|
'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`.
|
* `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.
|
* `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`.
|
* `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'`.
|
* `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'`.
|
||||||
* `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`.
|
|
||||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||||
|
|
||||||
## SlugRelatedField
|
## SlugRelatedField
|
||||||
|
@ -196,7 +194,7 @@ Would serialize to a representation like this:
|
||||||
{
|
{
|
||||||
'album_name': 'The Eraser',
|
'album_name': 'The Eraser',
|
||||||
'artist': 'Thom Yorke'
|
'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.
|
This field is always read-only.
|
||||||
|
@ -291,32 +289,23 @@ This custom field would then serialize to the following representation.
|
||||||
|
|
||||||
## Reverse relations
|
## 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.
|
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:
|
||||||
|
|
||||||
**The following will not work:**
|
|
||||||
|
|
||||||
class AlbumSerializer(serializers.ModelSerializer):
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('tracks', ...)
|
fields = ('tracks', ...)
|
||||||
|
|
||||||
Instead, you must explicitly add it to the serializer. For example:
|
|
||||||
|
|
||||||
class AlbumSerializer(serializers.ModelSerializer):
|
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:
|
||||||
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.
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
class Track(models.Model):
|
class Track(models.Model):
|
||||||
album = models.ForeignKey(Album, related_name='tracks')
|
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):
|
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.
|
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].
|
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
|
## Deprecated APIs
|
||||||
|
|
|
@ -288,10 +288,8 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
"""
|
"""
|
||||||
Represents a relationship using hyperlinking.
|
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
|
read_only = False
|
||||||
|
lookup_field = 'pk'
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'no_match': _('Invalid hyperlink - No URL match'),
|
'no_match': _('Invalid hyperlink - No URL match'),
|
||||||
|
@ -301,25 +299,84 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
|
'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):
|
def __init__(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
self.view_name = kwargs.pop('view_name')
|
self.view_name = kwargs.pop('view_name')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
|
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)
|
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||||
default_slug_kwarg = self.slug_url_kwarg or 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.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
|
||||||
|
|
||||||
self.format = kwargs.pop('format', None)
|
|
||||||
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
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.
|
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.
|
||||||
"""
|
"""
|
||||||
return self.slug_field
|
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
|
||||||
|
|
||||||
|
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):
|
def to_native(self, obj):
|
||||||
view_name = self.view_name
|
view_name = self.view_name
|
||||||
|
@ -327,43 +384,35 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
format = self.format or self.context.get('format', None)
|
format = self.format or self.context.get('format', None)
|
||||||
|
|
||||||
if request is None:
|
if request is None:
|
||||||
warnings.warn("Using `HyperlinkedRelatedField` without including the "
|
msg = (
|
||||||
"request in the serializer context is deprecated. "
|
"Using `HyperlinkedRelatedField` without including the request "
|
||||||
"Add `context={'request': request}` when instantiating the serializer.",
|
"in the serializer context is deprecated. "
|
||||||
DeprecationWarning, stacklevel=4)
|
"Add `context={'request': request}` when instantiating "
|
||||||
|
"the serializer."
|
||||||
|
)
|
||||||
|
warnings.warn(msg, DeprecationWarning, stacklevel=4)
|
||||||
|
|
||||||
pk = getattr(obj, 'pk', None)
|
# If the object has not yet been saved then we cannot hyperlink to it.
|
||||||
if pk is None:
|
if getattr(obj, 'pk', None) is None:
|
||||||
return
|
return
|
||||||
kwargs = {self.pk_url_kwarg: pk}
|
|
||||||
|
# Return the hyperlink, or error if incorrectly configured.
|
||||||
try:
|
try:
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return self.get_url(obj, view_name, request, format)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
pass
|
msg = (
|
||||||
|
'Could not resolve URL for hyperlinked relationship using '
|
||||||
slug = getattr(obj, self.slug_field, None)
|
'view name "%s". You may have failed to include the related '
|
||||||
|
'model in your API, or incorrectly configured the '
|
||||||
if not slug:
|
'`lookup_field` attribute on this field.'
|
||||||
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
|
)
|
||||||
|
raise Exception(msg % view_name)
|
||||||
kwargs = {self.slug_url_kwarg: slug}
|
|
||||||
try:
|
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
|
||||||
try:
|
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
|
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
# Convert URL -> model instance pk
|
# Convert URL -> model instance pk
|
||||||
# TODO: Use values_list
|
# 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')
|
raise Exception('Writable related fields must include a `queryset` argument')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -387,29 +436,11 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
if match.view_name != self.view_name:
|
if match.view_name != self.view_name:
|
||||||
raise ValidationError(self.error_messages['incorrect_match'])
|
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:
|
try:
|
||||||
obj = queryset.get()
|
return self.get_object(queryset, match.view_name,
|
||||||
except ObjectDoesNotExist:
|
match.args, match.kwargs)
|
||||||
|
except (ObjectDoesNotExist, TypeError, ValueError):
|
||||||
raise ValidationError(self.error_messages['does_not_exist'])
|
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):
|
class HyperlinkedIdentityField(Field):
|
||||||
|
|
|
@ -836,6 +836,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
_options_class = HyperlinkedModelSerializerOptions
|
_options_class = HyperlinkedModelSerializerOptions
|
||||||
_default_view_name = '%(model_name)s-detail'
|
_default_view_name = '%(model_name)s-detail'
|
||||||
|
_hyperlink_field_class = HyperlinkedRelatedField
|
||||||
|
|
||||||
url = HyperlinkedIdentityField()
|
url = HyperlinkedIdentityField()
|
||||||
|
|
||||||
|
@ -874,7 +875,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
if model_field:
|
if model_field:
|
||||||
kwargs['required'] = not(model_field.null or model_field.blank)
|
kwargs['required'] = not(model_field.null or model_field.blank)
|
||||||
|
|
||||||
return HyperlinkedRelatedField(**kwargs)
|
return self._hyperlink_field_class(**kwargs)
|
||||||
|
|
||||||
def get_identity(self, data):
|
def get_identity(self, data):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user