mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-03 12:00:12 +03:00
Merge 0705671731
into 8bd5e7e612
This commit is contained in:
commit
93b549730b
|
@ -308,7 +308,7 @@ See the Django documentation on [reverse relationships][reverse-relationships] f
|
|||
|
||||
## Generic relationships
|
||||
|
||||
If you want to serialize a generic foreign key, you need to define a custom field, to determine explicitly how you want serialize the targets of the relationship.
|
||||
If you want to serialize a generic foreign key, you need to define a `GenericRelatedField` with a configuration dictionary as first argument, that describes the representation of each model you possibly want to connect to the generic foreign key.
|
||||
|
||||
For example, given the following model for a tag, which has a generic relationship with other arbitrary models:
|
||||
|
||||
|
@ -343,40 +343,112 @@ And the following two models, which may be have associated tags:
|
|||
text = models.CharField(max_length=1000)
|
||||
tags = GenericRelation(TaggedItem)
|
||||
|
||||
We could define a custom field that could be used to serialize tagged instances, using the type of each instance to determine how it should be serialized.
|
||||
Now we define serializers for each model that may get associated with tags.
|
||||
|
||||
class TaggedObjectRelatedField(serializers.RelatedField):
|
||||
class BookmarkSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
A custom field to use for the `tagged_object` generic relationship.
|
||||
A simple `ModelSerializer` subclass for serializing `Bookmark` objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
exclude = ('id', )
|
||||
|
||||
def to_native(self, value):
|
||||
|
||||
class NoteSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
A simple `ModelSerializer` subclass for serializing `Note` objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = Note
|
||||
exclude = ('id', )
|
||||
|
||||
The model serializer for the `Tag` model could look like this:
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
A `Tag` serializer with a `GenericRelatedField` mapping all possible
|
||||
models to their respective serializers.
|
||||
"""
|
||||
tagged_object = serializers.GenericRelatedField({
|
||||
Bookmark: BookmarkSerializer(),
|
||||
Note: NoteSerializer()
|
||||
}, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', )
|
||||
|
||||
The JSON representation of a `Tag` object with `name='django'` and its generic foreign key pointing at a `Bookmark` object with `url='https://www.djangoproject.com/'` would look like this:
|
||||
|
||||
{
|
||||
'tagged_object': {
|
||||
'url': 'https://www.djangoproject.com/'
|
||||
},
|
||||
'tag_name': 'django'
|
||||
}
|
||||
|
||||
If you want to have your generic foreign key represented as hyperlink, simply use `HyperlinkedRelatedField` objects:
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
A `Tag` serializer with a `GenericRelatedField` mapping all possible
|
||||
models to properly set up `HyperlinkedRelatedField`s.
|
||||
"""
|
||||
tagged_object = serializers.GenericRelatedField({
|
||||
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||
}, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', )
|
||||
|
||||
The JSON representation of the same `Tag` example object could now look something like this:
|
||||
|
||||
{
|
||||
'tagged_object': '/bookmark/1/',
|
||||
'tag_name': 'django'
|
||||
}
|
||||
|
||||
These examples cover the default behavior of generic foreign key representation. However, you may also want to write to generic foreign key fields through your API.
|
||||
|
||||
By default, a `GenericRelatedField` iterates over its nested serializers and returns the value of the first serializer, that is actually able to perform `from_native`` on the input value without any errors.
|
||||
Note, that (at the moment) only `HyperlinkedRelatedField` is able to serialize model objects out of the box.
|
||||
|
||||
This `Tag` serializer is able to write to it's generic foreign key field:
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize tagged objects to a simple textual representation.
|
||||
"""
|
||||
if isinstance(value, Bookmark):
|
||||
return 'Bookmark: ' + value.url
|
||||
elif isinstance(value, Note):
|
||||
return 'Note: ' + value.text
|
||||
raise Exception('Unexpected type of tagged object')
|
||||
|
||||
If you need the target of the relationship to have a nested representation, you can use the required serializers inside the `.to_native()` method:
|
||||
|
||||
def to_native(self, value):
|
||||
A `Tag` serializer with a `GenericRelatedField` mapping all possible
|
||||
models to properly set up `HyperlinkedRelatedField`s.
|
||||
"""
|
||||
Serialize bookmark instances using a bookmark serializer,
|
||||
and note instances using a note serializer.
|
||||
"""
|
||||
if isinstance(value, Bookmark):
|
||||
serializer = BookmarkSerializer(value)
|
||||
elif isinstance(value, Note):
|
||||
serializer = NoteSerializer(value)
|
||||
else:
|
||||
raise Exception('Unexpected type of tagged object')
|
||||
tagged_object = serializers.GenericRelatedField({
|
||||
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||
}, read_only=False)
|
||||
|
||||
return serializer.data
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', )
|
||||
|
||||
Note that reverse generic keys, expressed using the `GenericRelation` field, can be serialized using the regular relational field types, since the type of the target in the relationship is always known.
|
||||
The following operations would create a `Tag` object with it's `tagged_object` property pointing at the `Bookmark` object found at the given detail end point.
|
||||
|
||||
tag_serializer = TagSerializer(data={
|
||||
'tag_name': 'python'
|
||||
'tagged_object': '/bookmark/1/'
|
||||
})
|
||||
|
||||
tag_serializer.is_valid()
|
||||
tag_serializer.save()
|
||||
|
||||
If you feel that this default behavior doesn't suit your needs, you can subclass `GenericRelatedField` and override its `determine_deserializer_for_data` or `determine_serializer_for_data` respectively to implement your own way of decision-making.
|
||||
|
||||
A few things you should note:
|
||||
|
||||
* Although `GenericForeignKey` fields can be set to any model object, the `GenericRelatedField` only handles models explicitly defined in its configuration dictionary.
|
||||
* Reverse generic keys, expressed using the `GenericRelation` field, can be serialized using the regular relational field types, since the type of the target in the relationship is always known.
|
||||
* Please take into account that the order in which you register serializers matters as far as write operations are concerned.
|
||||
* Unless you provide custom serializer determination, only `HyperlinkedRelatedFields` provide write access to generic model relations.
|
||||
|
||||
For more information see [the Django documentation on generic relations][generic-relations].
|
||||
|
||||
|
|
87
rest_framework/genericrelations.py
Normal file
87
rest_framework/genericrelations.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django import forms
|
||||
|
||||
from rest_framework import six
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ConfigurationError
|
||||
|
||||
|
||||
class GenericRelatedField(serializers.WritableField):
|
||||
"""
|
||||
Represents a generic relation foreign key.
|
||||
It's actually more of a wrapper, that delegates the logic to registered serializers based on the `Model` class.
|
||||
"""
|
||||
default_error_messages = {
|
||||
'no_model_match': _('Invalid model - model not available.'),
|
||||
'no_url_match': _('Invalid hyperlink - No URL match'),
|
||||
'incorrect_url_match': _('Invalid hyperlink - view name not available'),
|
||||
}
|
||||
|
||||
form_field_class = forms.URLField
|
||||
|
||||
def __init__(self, serializers, *args, **kwargs):
|
||||
"""
|
||||
Needs an extra parameter `serializers` which has to be a dict key: value being `Model`: serializer.
|
||||
"""
|
||||
super(GenericRelatedField, self).__init__(*args, **kwargs)
|
||||
self.serializers = serializers
|
||||
for model, serializer in six.iteritems(self.serializers):
|
||||
# We have to do it, because the serializer can't access a explicit manager through the
|
||||
# GenericForeignKey field on the model.
|
||||
if hasattr(serializer, 'queryset') and serializer.queryset is None:
|
||||
serializer.queryset = model._default_manager.all()
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
"""
|
||||
Delegates to the `to_native` method of the serializer registered under obj.__class__
|
||||
"""
|
||||
value = super(GenericRelatedField, self).field_to_native(obj, field_name)
|
||||
serializer = self.determine_deserializer_for_data(value)
|
||||
|
||||
# Necessary because of context, field resolving etc.
|
||||
serializer.initialize(self.parent, field_name)
|
||||
return serializer.to_native(value)
|
||||
|
||||
def to_native(self, value):
|
||||
# Override to prevent the simplifying process of value as present in `WritableField.to_native`.
|
||||
return value
|
||||
|
||||
def from_native(self, value):
|
||||
# Get the serializer responsible for input resolving
|
||||
try:
|
||||
serializer = self.determine_serializer_for_data(value)
|
||||
except ConfigurationError as e:
|
||||
raise ValidationError(e)
|
||||
serializer.initialize(self.parent, self.source)
|
||||
return serializer.from_native(value)
|
||||
|
||||
def determine_deserializer_for_data(self, value):
|
||||
try:
|
||||
model = value.__class__
|
||||
serializer = self.serializers[model]
|
||||
except KeyError:
|
||||
raise ValidationError(self.error_messages['no_model_match'])
|
||||
return serializer
|
||||
|
||||
def determine_serializer_for_data(self, value):
|
||||
# While one could easily execute the "try" block within from_native and reduce operations, I consider the
|
||||
# concept of serializing is already very naive and vague, that's why I'd go for stringency with the deserialization
|
||||
# process here.
|
||||
serializers = []
|
||||
for serializer in six.itervalues(self.serializers):
|
||||
try:
|
||||
serializer.from_native(value)
|
||||
# Collects all serializers that can handle the input data.
|
||||
serializers.append(serializer)
|
||||
except:
|
||||
pass
|
||||
# If no serializer found, raise error.
|
||||
l = len(serializers)
|
||||
if l < 1:
|
||||
raise ConfigurationError('Could not determine a valid serializer for value %r.' % value)
|
||||
elif l > 1:
|
||||
raise ConfigurationError('There were multiple serializers found for value %r.' % value)
|
||||
return serializers[0]
|
311
rest_framework/tests/relations_generic.py
Normal file
311
rest_framework/tests/relations_generic.py
Normal file
|
@ -0,0 +1,311 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.exceptions import ConfigurationError
|
||||
from rest_framework.genericrelations import GenericRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.get('/') # Just to ensure we have a request in the serializer context
|
||||
|
||||
def dummy_view(request, pk):
|
||||
pass
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^bookmark/(?P<pk>[0-9]+)/$', dummy_view, name='bookmark-detail'),
|
||||
url(r'^note/(?P<pk>[0-9]+)/$', dummy_view, name='note-detail'),
|
||||
url(r'^tag/(?P<pk>[0-9]+)/$', dummy_view, name='tag-detail'),
|
||||
url(r'^contact/(?P<my_own_slug>[-\w]+)/$', dummy_view, name='contact-detail'),
|
||||
)
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
"""
|
||||
Tags have a descriptive slug, and are attached to an arbitrary object.
|
||||
"""
|
||||
tag = models.SlugField()
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
tagged_item = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
class Bookmark(models.Model):
|
||||
"""
|
||||
A URL bookmark that may have multiple tags attached.
|
||||
"""
|
||||
url = models.URLField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __unicode__(self):
|
||||
return 'Bookmark: %s' % self.url
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
"""
|
||||
A textual note that may have multiple tags attached.
|
||||
"""
|
||||
text = models.TextField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __unicode__(self):
|
||||
return 'Note: %s' % self.text
|
||||
|
||||
|
||||
class BookmarkSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class NoteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Note
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class TestGenericRelatedFieldDeserialization(TestCase):
|
||||
|
||||
urls = 'rest_framework.tests.relations_generic'
|
||||
|
||||
def setUp(self):
|
||||
self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/')
|
||||
Tag.objects.create(tagged_item=self.bookmark, tag='django')
|
||||
Tag.objects.create(tagged_item=self.bookmark, tag='python')
|
||||
self.note = Note.objects.create(text='Remember the milk')
|
||||
Tag.objects.create(tagged_item=self.note, tag='reminder')
|
||||
|
||||
def test_relations_as_hyperlinks(self):
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||
}, source='tagged_item', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
expected = [
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': 'django',
|
||||
},
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': 'python',
|
||||
},
|
||||
{
|
||||
'tagged_item': '/note/1/',
|
||||
'tag': 'reminder'
|
||||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_relations_as_nested(self):
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: BookmarkSerializer(),
|
||||
Note: NoteSerializer(),
|
||||
}, source='tagged_item', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
expected = [
|
||||
{
|
||||
'tagged_item': {
|
||||
'url': 'https://www.djangoproject.com/'
|
||||
},
|
||||
'tag': 'django'
|
||||
},
|
||||
{
|
||||
'tagged_item': {
|
||||
'url': 'https://www.djangoproject.com/'
|
||||
},
|
||||
'tag': 'python'
|
||||
},
|
||||
{
|
||||
'tagged_item': {
|
||||
'text': 'Remember the milk',
|
||||
},
|
||||
'tag': 'reminder'
|
||||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_mixed_serializers(self):
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: BookmarkSerializer(),
|
||||
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||
}, source='tagged_item', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
expected = [
|
||||
{
|
||||
'tagged_item': {
|
||||
'url': 'https://www.djangoproject.com/'
|
||||
},
|
||||
'tag': 'django'
|
||||
},
|
||||
{
|
||||
'tagged_item': {
|
||||
'url': 'https://www.djangoproject.com/'
|
||||
},
|
||||
'tag': 'python'
|
||||
},
|
||||
{
|
||||
'tagged_item': '/note/1/',
|
||||
'tag': 'reminder'
|
||||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_invalid_model(self):
|
||||
# Leaving out the Note model should result in a ValidationError
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: BookmarkSerializer(),
|
||||
}, source='tagged_item', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
|
||||
def call_data():
|
||||
return serializer.data
|
||||
self.assertRaises(ValidationError, call_data)
|
||||
|
||||
|
||||
class TestGenericRelatedFieldSerialization(TestCase):
|
||||
|
||||
urls = 'rest_framework.tests.relations_generic'
|
||||
|
||||
def setUp(self):
|
||||
self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/')
|
||||
Tag.objects.create(tagged_item=self.bookmark, tag='django')
|
||||
Tag.objects.create(tagged_item=self.bookmark, tag='python')
|
||||
self.note = Note.objects.create(text='Remember the milk')
|
||||
|
||||
def test_hyperlink_serialization(self):
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||
}, source='tagged_item', read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(data={
|
||||
'tag': 'reminder',
|
||||
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
|
||||
})
|
||||
serializer.is_valid()
|
||||
expected = {
|
||||
'tagged_item': '/note/1/',
|
||||
'tag': 'reminder'
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_configuration_error(self):
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: BookmarkSerializer(),
|
||||
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||
}, source='tagged_item', read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(data={
|
||||
'tag': 'reminder',
|
||||
'tagged_item': 'just a string'
|
||||
})
|
||||
|
||||
with self.assertRaises(ConfigurationError):
|
||||
serializer.fields['tagged_item'].determine_serializer_for_data('just a string')
|
||||
|
||||
def test_not_registered_view_name(self):
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||
}, source='tagged_item', read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(data={
|
||||
'tag': 'reminder',
|
||||
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
|
||||
def test_invalid_url(self):
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||
}, source='tagged_item', read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(data={
|
||||
'tag': 'reminder',
|
||||
'tagged_item': 'foo-bar'
|
||||
})
|
||||
|
||||
expected = {
|
||||
'tagged_item': ['Could not determine a valid serializer for value %r.' % 'foo-bar']
|
||||
}
|
||||
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(expected, serializer.errors)
|
||||
|
||||
def test_serializer_save(self):
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField({
|
||||
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||
}, source='tagged_item', read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(data={
|
||||
'tag': 'reminder',
|
||||
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
|
||||
})
|
||||
serializer.is_valid()
|
||||
expected = {
|
||||
'tagged_item': '/note/1/',
|
||||
'tag': 'reminder'
|
||||
}
|
||||
serializer.save()
|
||||
tag = Tag.objects.get(pk=3)
|
||||
self.assertEqual(tag.tagged_item, self.note)
|
Loading…
Reference in New Issue
Block a user