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',
|
||||
'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:
|
||||
fields = ('tracks', ...)
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
|
|
@ -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,25 +299,84 @@ 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.
|
||||
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):
|
||||
view_name = self.view_name
|
||||
|
@ -327,43 +384,35 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
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)
|
||||
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)
|
||||
|
||||
pk = getattr(obj, 'pk', None)
|
||||
if pk is None:
|
||||
# If the object has not yet been saved then we cannot hyperlink to it.
|
||||
if getattr(obj, 'pk', None) is None:
|
||||
return
|
||||
kwargs = {self.pk_url_kwarg: pk}
|
||||
|
||||
# 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
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
|
||||
if not slug:
|
||||
raise Exception('Could not resolve URL for field using view name "%s"' % 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)
|
||||
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):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue
Block a user