mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-29 17:39:48 +03:00
refactored GenericRelatedField with dict argument as discussed here: https://github.com/tomchristie/django-rest-framework/pull/755
This commit is contained in:
parent
850e696608
commit
b894b3c15d
|
@ -1,197 +1,50 @@
|
||||||
"""
|
|
||||||
# Generic relations
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
This is an attempt to implement generic relation foreign keys as provided by Django's
|
|
||||||
[contenttype framework](https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/) and I tried to follow the
|
|
||||||
suggestions and requirements described here:
|
|
||||||
* https://github.com/tomchristie/django-rest-framework/issues/606
|
|
||||||
* http://stackoverflow.com/a/14454629/1326146
|
|
||||||
|
|
||||||
The `GenericRelatedField` field enables you to read and write `GenericForeignKey`s within the scope of Django REST
|
|
||||||
Framework. While you can decide whether to output `GenericForeignKey`s as nested objects (`ModelSerializer`) or valid
|
|
||||||
resource URLs (`HyperlinkedRelatedField`), the input is restricted to resource URLs mainly because of two reasons:
|
|
||||||
* The sheer impossibility of deriving a valid `Model` class from a simple data dictionary.
|
|
||||||
* My inner storm of indignation when thinking of exposing the actual `ContentType`s to the public scope.
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
Although I'm pretty experienced with Django and REST etc, please note that this piece of code is also my first
|
|
||||||
experience with Django REST framework at all. I am the maintainer of a pretty large REST service application based on
|
|
||||||
[tastypie](tastypieapi.org), but Daniel's recent
|
|
||||||
[blog post](http://toastdriven.com/blog/2013/feb/05/committers-needed-tastypie-haystack/) made me feel a little
|
|
||||||
uncomfortable. As much I'd like to spend more time contributing, I fear that my current work-life balance doesn't allow
|
|
||||||
me to do so at the level I'd expect from myself. So I thought it would be a good thing to reach out for other solutions.
|
|
||||||
It's a good thing anyway, I guess. But a sane and approved way of representing `GenericForeignKeys` is just essential to
|
|
||||||
the data structure I'm working with. So that's basically why I got in the mix.
|
|
||||||
|
|
||||||
|
|
||||||
## Minimalist example
|
|
||||||
|
|
||||||
This is based on the models mentioned in
|
|
||||||
[this article](http://django-rest-framework.org/api-guide/relations.html#generic-relationships). The examples are also
|
|
||||||
based on working URL patterns for each model fitting the pattern `<model_lowercase>-detail`.
|
|
||||||
|
|
||||||
A minimalist but still working example of a `TagSerializer` with `GenericRelatedField` would look like this:
|
|
||||||
|
|
||||||
class TagSerializer(serializers.ModelSerializer):
|
|
||||||
tagged_item = GenericRelatedField([
|
|
||||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
|
||||||
GenericRelationOption(Note, 'note-detail'),
|
|
||||||
], source='tagged_item')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Tag
|
|
||||||
exclude = ('id', 'content_type', 'object_id', )
|
|
||||||
|
|
||||||
## ``GenericRelationOption``
|
|
||||||
|
|
||||||
Constructor:
|
|
||||||
|
|
||||||
GenericRelationOption(model, view_name, as_hyperlink=True, related_field=None, serializer=None)
|
|
||||||
|
|
||||||
**`model`**
|
|
||||||
The model class.
|
|
||||||
|
|
||||||
**`view_name`**
|
|
||||||
The view name as used in url patterns.
|
|
||||||
|
|
||||||
**`as_hyperlink`**
|
|
||||||
Decides whether the output of the `GenericForeignKey` should be as end point or nested object. In the case of the
|
|
||||||
latter a generic serializer will be used unless you pass a specific one.
|
|
||||||
|
|
||||||
**`related_field`**
|
|
||||||
A specific subclass of `HyperlinkedRelatedField` that should be used for resolving input data and resolving output data
|
|
||||||
in case of `as_hyperlink` is `True`.
|
|
||||||
|
|
||||||
**`serializer`**
|
|
||||||
A specific subclass of `ModelSerializer` that should be used for resolving output data
|
|
||||||
in case of `as_hyperlink` is `True`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import get_script_prefix, resolve
|
from django.core.urlresolvers import get_script_prefix, resolve
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from rest_framework.compat import urlparse
|
from rest_framework.compat import urlparse
|
||||||
|
from rest_framework import six
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.exceptions import ConfigurationError
|
||||||
|
|
||||||
|
|
||||||
class GenericRelationOption(object):
|
|
||||||
"""
|
|
||||||
This object is responsible for setting up the components needed for providing a generic relation with a given model.
|
|
||||||
"""
|
|
||||||
|
|
||||||
#TODO: Far more strict evaluation of custom related_field and serializer objects
|
|
||||||
|
|
||||||
# Trying to be inline with common practices
|
|
||||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
|
||||||
|
|
||||||
def __init__(self, model, view_name, as_hyperlink=True, related_field=None, serializer=None):
|
|
||||||
self.model = model
|
|
||||||
self.view_name = view_name
|
|
||||||
self.as_hyperlink = as_hyperlink
|
|
||||||
self.related_field = related_field or self.get_default_related_field()
|
|
||||||
self.serializer = serializer or self.get_default_serializer()
|
|
||||||
|
|
||||||
def get_output_resolver(self):
|
|
||||||
"""
|
|
||||||
Should return a class that implements the `to_native` method, i.e. `HyperlinkedRelatedField` or `ModelSerializer`.
|
|
||||||
"""
|
|
||||||
if self.as_hyperlink:
|
|
||||||
return self.get_prepared_related_field()
|
|
||||||
else:
|
|
||||||
return self.serializer
|
|
||||||
|
|
||||||
def get_input_resolver(self):
|
|
||||||
"""
|
|
||||||
Should return a class that implements the `from_native` method that can handle URL values,
|
|
||||||
i.e. `HyperlinkedRelatedField`.
|
|
||||||
"""
|
|
||||||
return self.get_prepared_related_field()
|
|
||||||
|
|
||||||
def get_prepared_related_field(self):
|
|
||||||
"""
|
|
||||||
Provides the related field with a queryset if not present, based on `self.model`.
|
|
||||||
"""
|
|
||||||
if self.related_field.queryset is None:
|
|
||||||
self.related_field.queryset = self.model.objects.all()
|
|
||||||
return self.related_field
|
|
||||||
|
|
||||||
def get_default_related_field(self):
|
|
||||||
"""
|
|
||||||
Creates and returns a minimalist ``HyperlinkedRelatedField` instance if none has been passed to the constructor.
|
|
||||||
"""
|
|
||||||
return serializers.HyperlinkedRelatedField(view_name=self.view_name)
|
|
||||||
|
|
||||||
def get_default_serializer(self):
|
|
||||||
"""
|
|
||||||
Creates and returns a minimalist ``ModelSerializer` instance if none has been passed to the constructor.
|
|
||||||
"""
|
|
||||||
class DefaultSerializer(self.model_serializer_class):
|
|
||||||
class Meta:
|
|
||||||
model = self.model
|
|
||||||
return DefaultSerializer()
|
|
||||||
|
|
||||||
|
|
||||||
class GenericRelatedField(serializers.WritableField):
|
class GenericRelatedField(serializers.WritableField):
|
||||||
"""
|
"""
|
||||||
Represents a generic relation foreign key.
|
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.
|
||||||
It's actually more of a wrapper, that delegates the logic to registered fields / serializers based on some
|
|
||||||
contenttype framework criteria.
|
|
||||||
"""
|
"""
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'no_model_match': _('Invalid model - model not available.'),
|
'no_model_match': _('Invalid model - model not available.'),
|
||||||
'no_match': _('Invalid hyperlink - No URL match'),
|
'no_url_match': _('Invalid hyperlink - No URL match'),
|
||||||
'incorrect_match': _('Invalid hyperlink - view name not available'),
|
'incorrect_url_match': _('Invalid hyperlink - view name not available'),
|
||||||
}
|
}
|
||||||
|
|
||||||
form_field_class = forms.URLField
|
form_field_class = forms.URLField
|
||||||
|
|
||||||
def __init__(self, options, *args, **kwargs):
|
def __init__(self, serializers, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Needs an extra parameter ``options`` which has to be a list of `GenericRelationOption` objects.
|
Needs an extra parameter `serializers` which has to be a dict key: value being `Model`: serializer.
|
||||||
"""
|
"""
|
||||||
super(GenericRelatedField, self).__init__(*args, **kwargs)
|
super(GenericRelatedField, self).__init__(*args, **kwargs)
|
||||||
|
self.serializers = serializers
|
||||||
# Map for option identifying based on a `Model` class (deserialization cycle)
|
for model, serializer in six.iteritems(self.serializers):
|
||||||
self._model_map = dict()
|
# We have to do it, because the serializer can't access a explicit manager through the
|
||||||
# Map for option identifying based on a `view_name` (serialization cycle)
|
# GenericForeignKey field on the model.
|
||||||
self._view_name_map = dict()
|
if hasattr(serializer, 'queryset') and serializer.queryset is None:
|
||||||
|
serializer.queryset = model._default_manager.all()
|
||||||
# Adding the options to the maps.
|
|
||||||
for option in options:
|
|
||||||
self._model_map[option.model] = option
|
|
||||||
self._view_name_map[option.view_name] = option
|
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
"""
|
"""
|
||||||
Identifies the option object that is responsible for this `value.__class__` (a model) object and returns
|
Delegates to the `to_native` method of the serializer registered under obj.__class__
|
||||||
its output serializer's `to_native` method.
|
|
||||||
"""
|
"""
|
||||||
value = super(GenericRelatedField, self).field_to_native(obj, field_name)
|
value = super(GenericRelatedField, self).field_to_native(obj, field_name)
|
||||||
|
serializer = self.determine_deserializer_for_data(value)
|
||||||
# Retrieving the model class.
|
|
||||||
model = value.__class__
|
|
||||||
|
|
||||||
try:
|
|
||||||
option = self._model_map[model]
|
|
||||||
except KeyError:
|
|
||||||
raise ValidationError(self.error_messages['no_model_match'])
|
|
||||||
|
|
||||||
# Get the serializer responsible for output formatting
|
|
||||||
serializer = option.get_output_resolver()
|
|
||||||
|
|
||||||
# Necessary because of context, field resolving etc.
|
# Necessary because of context, field resolving etc.
|
||||||
serializer.initialize(self.parent, field_name)
|
serializer.initialize(self.parent, field_name)
|
||||||
|
|
||||||
return serializer.to_native(value)
|
return serializer.to_native(value)
|
||||||
|
|
||||||
def to_native(self, value):
|
def to_native(self, value):
|
||||||
|
@ -199,6 +52,24 @@ class GenericRelatedField(serializers.WritableField):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
|
# Get the serializer responsible for input resolving
|
||||||
|
serializer = self.determine_serializer_for_data(value)
|
||||||
|
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):
|
||||||
|
for serializer in six.itervalues(self.serializers):
|
||||||
|
if not isinstance(serializer, serializers.HyperlinkedRelatedField):
|
||||||
|
raise ConfigurationError('Please use HyperlinkedRelatedField as serializers on GenericRelatedField \
|
||||||
|
instances with read_only=False or set read_only=True.')
|
||||||
|
|
||||||
# This excerpt is an exact copy of ``rest_framework.relations.HyperlinkedRelatedField``, Line 363
|
# This excerpt is an exact copy of ``rest_framework.relations.HyperlinkedRelatedField``, Line 363
|
||||||
# From here until ...
|
# From here until ...
|
||||||
|
@ -217,20 +88,15 @@ class GenericRelatedField(serializers.WritableField):
|
||||||
try:
|
try:
|
||||||
match = resolve(value)
|
match = resolve(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ValidationError(self.error_messages['no_match'])
|
raise ValidationError(self.error_messages['no_url_match'])
|
||||||
|
|
||||||
# ... here. Thinking about putting that in ``rest_framework.utils.py``. Of course With more appropriate exceptions.
|
# ... here
|
||||||
|
|
||||||
# Try to find the derived `view_name` in the map.
|
matched_serializer = None
|
||||||
try:
|
for serializer in six.itervalues(self.serializers):
|
||||||
view_name = match.url_name
|
if serializer.view_name == match.url_name:
|
||||||
option = self._view_name_map[view_name]
|
matched_serializer = serializer
|
||||||
except KeyError:
|
|
||||||
raise ValidationError(self.error_messages['incorrect_match'])
|
|
||||||
|
|
||||||
# Get the serializer responsible for input resolving
|
if matched_serializer is None:
|
||||||
serializer = option.get_input_resolver()
|
raise ValidationError(self.error_messages['incorrect_url_match'])
|
||||||
|
return matched_serializer
|
||||||
# Necessary because of context, field resolving etc.
|
|
||||||
serializer.initialize(self.parent, self.source)
|
|
||||||
return serializer.from_native(value)
|
|
|
@ -1,12 +1,14 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
|
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.compat import patterns, url
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.genericrelations import GenericRelationOption, GenericRelatedField
|
from rest_framework.exceptions import ConfigurationError
|
||||||
|
from rest_framework.genericrelations import GenericRelatedField
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
@ -58,75 +60,19 @@ class Note(models.Model):
|
||||||
return 'Note: %s' % self.text
|
return 'Note: %s' % self.text
|
||||||
|
|
||||||
|
|
||||||
class Contact(models.Model):
|
class BookmarkSerializer(serializers.ModelSerializer):
|
||||||
"""
|
class Meta:
|
||||||
A textual note that may have multiple tags attached.
|
model = Bookmark
|
||||||
"""
|
exclude = ('id', )
|
||||||
name = models.TextField()
|
|
||||||
slug = models.SlugField()
|
|
||||||
tags = GenericRelation(Tag)
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return 'Contact: %s' % self.name
|
|
||||||
|
|
||||||
|
|
||||||
class TestGenericRelationOptions(TestCase):
|
class NoteSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
def test_default_related_field(self):
|
model = Note
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
exclude = ('id', )
|
||||||
self.assertIsInstance(option.related_field, serializers.HyperlinkedRelatedField)
|
|
||||||
|
|
||||||
def test_default_related_field_view_name(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
|
||||||
self.assertEqual(option.related_field.view_name, 'bookmark-detail')
|
|
||||||
|
|
||||||
def test_default_serializer(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
|
||||||
self.assertIsInstance(option.serializer, serializers.ModelSerializer)
|
|
||||||
|
|
||||||
def test_default_serializer_meta_model(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
|
||||||
self.assertEqual(option.serializer.Meta.model, Bookmark)
|
|
||||||
|
|
||||||
def test_get_url_output_resolver(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
|
||||||
self.assertIsInstance(option.get_output_resolver(), serializers.HyperlinkedRelatedField)
|
|
||||||
|
|
||||||
def test_get_url_output_resolver_with_queryset(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
|
||||||
self.assertIsNotNone(option.get_output_resolver().queryset)
|
|
||||||
|
|
||||||
def test_get_input_resolver(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
|
||||||
self.assertIsInstance(option.get_input_resolver(), serializers.HyperlinkedRelatedField)
|
|
||||||
|
|
||||||
def test_get_input_resolver_with_queryset(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
|
||||||
self.assertIsNotNone(option.get_output_resolver().queryset)
|
|
||||||
|
|
||||||
def test_get_serializer_resolver(self):
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail', as_hyperlink=False)
|
|
||||||
self.assertIsInstance(option.get_output_resolver(), serializers.ModelSerializer)
|
|
||||||
|
|
||||||
def test_custom_related_field(self):
|
|
||||||
related_field = serializers.HyperlinkedRelatedField(view_name='bookmark-detail', format='xml')
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail',
|
|
||||||
related_field=related_field)
|
|
||||||
self.assertEqual(option.related_field.format, 'xml')
|
|
||||||
|
|
||||||
def test_custom_serializer(self):
|
|
||||||
|
|
||||||
class BookmarkSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Bookmark
|
|
||||||
exclude = ('id', )
|
|
||||||
|
|
||||||
serializer = BookmarkSerializer()
|
|
||||||
option = GenericRelationOption(Bookmark, 'bookmark-detail', as_hyperlink=False, serializer=serializer)
|
|
||||||
self.assertIn('id', option.get_output_resolver().Meta.exclude)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGenericRelatedFieldToNative(TestCase):
|
class TestGenericRelatedFieldDeserialization(TestCase):
|
||||||
|
|
||||||
urls = 'rest_framework.tests.relations_generic'
|
urls = 'rest_framework.tests.relations_generic'
|
||||||
|
|
||||||
|
@ -140,10 +86,10 @@ class TestGenericRelatedFieldToNative(TestCase):
|
||||||
def test_relations_as_hyperlinks(self):
|
def test_relations_as_hyperlinks(self):
|
||||||
|
|
||||||
class TagSerializer(serializers.ModelSerializer):
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
tagged_item = GenericRelatedField([
|
tagged_item = GenericRelatedField({
|
||||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||||
GenericRelationOption(Note, 'note-detail'),
|
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||||
], source='tagged_item')
|
}, source='tagged_item', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
|
@ -169,10 +115,10 @@ class TestGenericRelatedFieldToNative(TestCase):
|
||||||
def test_relations_as_nested(self):
|
def test_relations_as_nested(self):
|
||||||
|
|
||||||
class TagSerializer(serializers.ModelSerializer):
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
tagged_item = GenericRelatedField([
|
tagged_item = GenericRelatedField({
|
||||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
Bookmark: BookmarkSerializer(),
|
||||||
GenericRelationOption(Note, 'note-detail', as_hyperlink=False),
|
Note: NoteSerializer(),
|
||||||
], source='tagged_item')
|
}, source='tagged_item', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
|
@ -181,16 +127,19 @@ class TestGenericRelatedFieldToNative(TestCase):
|
||||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
'tagged_item': '/bookmark/1/',
|
'tagged_item': {
|
||||||
|
'url': 'https://www.djangoproject.com/'
|
||||||
|
},
|
||||||
'tag': 'django'
|
'tag': 'django'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tagged_item': '/bookmark/1/',
|
'tagged_item': {
|
||||||
|
'url': 'https://www.djangoproject.com/'
|
||||||
|
},
|
||||||
'tag': 'python'
|
'tag': 'python'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tagged_item': {
|
'tagged_item': {
|
||||||
'id': 1,
|
|
||||||
'text': 'Remember the milk',
|
'text': 'Remember the milk',
|
||||||
},
|
},
|
||||||
'tag': 'reminder'
|
'tag': 'reminder'
|
||||||
|
@ -198,19 +147,12 @@ class TestGenericRelatedFieldToNative(TestCase):
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
def test_custom_related_field(self):
|
def test_mixed_serializers(self):
|
||||||
contact = Contact.objects.create(name='Lukas Buenger', slug='lukas-buenger')
|
|
||||||
Tag.objects.create(tagged_item=contact, tag='developer')
|
|
||||||
|
|
||||||
contact_related_field = serializers.HyperlinkedRelatedField(view_name='contact-detail',
|
|
||||||
slug_url_kwarg='my_own_slug')
|
|
||||||
|
|
||||||
class TagSerializer(serializers.ModelSerializer):
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
tagged_item = GenericRelatedField([
|
tagged_item = GenericRelatedField({
|
||||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
Bookmark: BookmarkSerializer(),
|
||||||
GenericRelationOption(Note, 'note-detail', as_hyperlink=False),
|
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||||
GenericRelationOption(Contact, 'contact-detail', related_field=contact_related_field),
|
}, source='tagged_item', read_only=True)
|
||||||
], source='tagged_item')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
|
@ -219,80 +161,42 @@ class TestGenericRelatedFieldToNative(TestCase):
|
||||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
'tagged_item': '/bookmark/1/',
|
'tagged_item': {
|
||||||
|
'url': 'https://www.djangoproject.com/'
|
||||||
|
},
|
||||||
'tag': 'django'
|
'tag': 'django'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tagged_item': '/bookmark/1/',
|
'tagged_item': {
|
||||||
|
'url': 'https://www.djangoproject.com/'
|
||||||
|
},
|
||||||
'tag': 'python'
|
'tag': 'python'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tagged_item': {
|
'tagged_item': '/note/1/',
|
||||||
'id': 1,
|
|
||||||
'text': 'Remember the milk',
|
|
||||||
},
|
|
||||||
'tag': 'reminder'
|
'tag': 'reminder'
|
||||||
},
|
|
||||||
{
|
|
||||||
'tagged_item': '/contact/lukas-buenger/',
|
|
||||||
'tag': 'developer'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
def test_custom_serializer(self):
|
def test_invalid_model(self):
|
||||||
contact = Contact.objects.create(name='Lukas Buenger', slug='lukas-buenger')
|
# Leaving out the Note model should result in a ValidationError
|
||||||
Tag.objects.create(tagged_item=contact, tag='developer')
|
|
||||||
|
|
||||||
contact_related_field = serializers.HyperlinkedRelatedField(view_name='contact-detail',
|
|
||||||
slug_url_kwarg='my_own_slug')
|
|
||||||
|
|
||||||
class ContactSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Contact
|
|
||||||
exclude = ('id', 'slug', )
|
|
||||||
|
|
||||||
|
|
||||||
class TagSerializer(serializers.ModelSerializer):
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
tagged_item = GenericRelatedField([
|
tagged_item = GenericRelatedField({
|
||||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
Bookmark: BookmarkSerializer(),
|
||||||
GenericRelationOption(Note, 'note-detail', as_hyperlink=False),
|
}, source='tagged_item', read_only=True)
|
||||||
GenericRelationOption(Contact, 'contact-detail', as_hyperlink=False, related_field=contact_related_field,
|
|
||||||
serializer=ContactSerializer()),
|
|
||||||
], source='tagged_item')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
exclude = ('id', 'content_type', 'object_id', )
|
exclude = ('id', 'content_type', 'object_id', )
|
||||||
|
|
||||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||||
expected = [
|
|
||||||
{
|
def call_data():
|
||||||
'tagged_item': '/bookmark/1/',
|
return serializer.data
|
||||||
'tag': 'django'
|
self.assertRaises(ValidationError, call_data)
|
||||||
},
|
|
||||||
{
|
|
||||||
'tagged_item': '/bookmark/1/',
|
|
||||||
'tag': 'python'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tagged_item': {
|
|
||||||
'id': 1,
|
|
||||||
'text': 'Remember the milk',
|
|
||||||
},
|
|
||||||
'tag': 'reminder'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tagged_item': {
|
|
||||||
'name': 'Lukas Buenger'
|
|
||||||
},
|
|
||||||
'tag': 'developer'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
self.assertEqual(serializer.data, expected)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGenericRelatedFieldFromNative(TestCase):
|
class TestGenericRelatedFieldSerialization(TestCase):
|
||||||
|
|
||||||
urls = 'rest_framework.tests.relations_generic'
|
urls = 'rest_framework.tests.relations_generic'
|
||||||
|
|
||||||
|
@ -302,13 +206,12 @@ class TestGenericRelatedFieldFromNative(TestCase):
|
||||||
Tag.objects.create(tagged_item=self.bookmark, tag='python')
|
Tag.objects.create(tagged_item=self.bookmark, tag='python')
|
||||||
self.note = Note.objects.create(text='Remember the milk')
|
self.note = Note.objects.create(text='Remember the milk')
|
||||||
|
|
||||||
def test_default(self):
|
def test_hyperlink_serialization(self):
|
||||||
|
|
||||||
class TagSerializer(serializers.ModelSerializer):
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
tagged_item = GenericRelatedField([
|
tagged_item = GenericRelatedField({
|
||||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
Bookmark: serializers.HyperlinkedRelatedField(view_name='bookmark-detail'),
|
||||||
GenericRelationOption(Note, 'note-detail'),
|
Note: serializers.HyperlinkedRelatedField(view_name='note-detail'),
|
||||||
], source='tagged_item')
|
}, source='tagged_item', read_only=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
|
@ -318,10 +221,82 @@ class TestGenericRelatedFieldFromNative(TestCase):
|
||||||
'tag': 'reminder',
|
'tag': 'reminder',
|
||||||
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
|
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
|
||||||
})
|
})
|
||||||
|
|
||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
expected = {
|
expected = {
|
||||||
'tagged_item': '/note/1/',
|
'tagged_item': '/note/1/',
|
||||||
'tag': 'reminder'
|
'tag': 'reminder'
|
||||||
}
|
}
|
||||||
self.assertEqual(serializer.data, expected)
|
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': reverse('note-detail', kwargs={'pk': self.note.pk})
|
||||||
|
})
|
||||||
|
self.assertRaises(ConfigurationError, serializer.is_valid)
|
||||||
|
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
|
||||||
|
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