mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-30 01:49:50 +03:00
Introducing implementation and tests
This commit is contained in:
parent
dca24cd914
commit
7957591910
164
rest_framework/genericrelations.py
Normal file
164
rest_framework/genericrelations.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import urlparse
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import get_script_prefix, resolve
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django import forms
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Represents a generic relation foreign key.
|
||||
|
||||
It's actually more of a wrapper, that delegates the logic to registered fields / serializers based on some
|
||||
contenttype framework criteria.
|
||||
"""
|
||||
default_error_messages = {
|
||||
'no_model_match': _('Invalid model - model not available.'),
|
||||
'no_match': _('Invalid hyperlink - No URL match'),
|
||||
'incorrect_match': _('Invalid hyperlink - view name not available'),
|
||||
}
|
||||
|
||||
form_field_class = forms.URLField
|
||||
|
||||
def __init__(self, options, *args, **kwargs):
|
||||
"""
|
||||
Needs an extra parameter ``options`` which has to be a list of `GenericRelationOption` objects.
|
||||
"""
|
||||
super(GenericRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
# Map for option identifying based on a `Model` class (deserialization cycle)
|
||||
self._model_map = dict()
|
||||
# Map for option identifying based on a `view_name` (serialization cycle)
|
||||
self._view_name_map = dict()
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Identifies the option object that is responsible for this `value.__class__` (a model) object and returns
|
||||
its output serializer's `to_native` method.
|
||||
"""
|
||||
value = super(GenericRelatedField, self).field_to_native(obj, field_name)
|
||||
|
||||
# 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.
|
||||
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):
|
||||
|
||||
# This excerpt is an exact copy of ``rest_framework.relations.HyperlinkedRelatedField``, Line 363
|
||||
# From here until ...
|
||||
try:
|
||||
http_prefix = value.startswith('http:') or value.startswith('https:')
|
||||
except AttributeError:
|
||||
msg = self.error_messages['incorrect_type']
|
||||
raise ValidationError(msg % type(value).__name__)
|
||||
|
||||
if http_prefix:
|
||||
# If needed convert absolute URLs to relative path
|
||||
value = urlparse.urlparse(value).path
|
||||
prefix = get_script_prefix()
|
||||
if value.startswith(prefix):
|
||||
value = '/' + value[len(prefix):]
|
||||
try:
|
||||
match = resolve(value)
|
||||
except Exception:
|
||||
raise ValidationError(self.error_messages['no_match'])
|
||||
|
||||
# ... here. Thinking about putting that in ``rest_framework.utils.py``. Of course With more appropriate exceptions.
|
||||
|
||||
# Try to find the derived `view_name` in the map.
|
||||
try:
|
||||
view_name = match.url_name
|
||||
option = self._view_name_map[view_name]
|
||||
except KeyError:
|
||||
raise ValidationError(self.error_messages['incorrect_match'])
|
||||
|
||||
# Get the serializer responsible for input resolving
|
||||
serializer = option.get_input_resolver()
|
||||
|
||||
# Necessary because of context, field resolving etc.
|
||||
serializer.initialize(self.parent, self.source)
|
||||
return serializer.from_native(value)
|
327
rest_framework/tests/relations_generic.py
Normal file
327
rest_framework/tests/relations_generic.py
Normal file
|
@ -0,0 +1,327 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
|
||||
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.genericrelations import GenericRelationOption, 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 Contact(models.Model):
|
||||
"""
|
||||
A textual note that may have multiple tags attached.
|
||||
"""
|
||||
name = models.TextField()
|
||||
slug = models.SlugField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __unicode__(self):
|
||||
return 'Contact: %s' % self.name
|
||||
|
||||
|
||||
class TestGenericRelationOptions(TestCase):
|
||||
|
||||
def test_default_related_field(self):
|
||||
option = GenericRelationOption(Bookmark, 'bookmark-detail')
|
||||
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):
|
||||
|
||||
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([
|
||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
||||
GenericRelationOption(Note, 'note-detail'),
|
||||
], source='tagged_item')
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
expected = [
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'django'
|
||||
},
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'python'
|
||||
},
|
||||
{
|
||||
'tagged_item': '/note/1/',
|
||||
'tag': u'reminder'
|
||||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_relations_as_nested(self):
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField([
|
||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
||||
GenericRelationOption(Note, 'note-detail', as_hyperlink=False),
|
||||
], source='tagged_item')
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
expected = [
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'django'
|
||||
},
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'python'
|
||||
},
|
||||
{
|
||||
'tagged_item': {
|
||||
'id': 1,
|
||||
'text': 'Remember the milk',
|
||||
},
|
||||
'tag': u'reminder'
|
||||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_custom_related_field(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):
|
||||
tagged_item = GenericRelatedField([
|
||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
||||
GenericRelationOption(Note, 'note-detail', as_hyperlink=False),
|
||||
GenericRelationOption(Contact, 'contact-detail', related_field=contact_related_field),
|
||||
], source='tagged_item')
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
expected = [
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'django'
|
||||
},
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'python'
|
||||
},
|
||||
{
|
||||
'tagged_item': {
|
||||
'id': 1,
|
||||
'text': 'Remember the milk',
|
||||
},
|
||||
'tag': u'reminder'
|
||||
},
|
||||
{
|
||||
'tagged_item': '/contact/lukas-buenger/',
|
||||
'tag': u'developer'
|
||||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_custom_serializer(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 ContactSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Contact
|
||||
exclude = ('id', 'slug', )
|
||||
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
tagged_item = GenericRelatedField([
|
||||
GenericRelationOption(Bookmark, 'bookmark-detail'),
|
||||
GenericRelationOption(Note, 'note-detail', as_hyperlink=False),
|
||||
GenericRelationOption(Contact, 'contact-detail', as_hyperlink=False, related_field=contact_related_field,
|
||||
serializer=ContactSerializer()),
|
||||
], source='tagged_item')
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('id', 'content_type', 'object_id', )
|
||||
|
||||
serializer = TagSerializer(Tag.objects.all(), many=True)
|
||||
expected = [
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'django'
|
||||
},
|
||||
{
|
||||
'tagged_item': '/bookmark/1/',
|
||||
'tag': u'python'
|
||||
},
|
||||
{
|
||||
'tagged_item': {
|
||||
'id': 1,
|
||||
'text': 'Remember the milk',
|
||||
},
|
||||
'tag': u'reminder'
|
||||
},
|
||||
{
|
||||
'tagged_item': {
|
||||
'name': 'Lukas Buenger'
|
||||
},
|
||||
'tag': u'developer'
|
||||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
class TestGenericRelatedFieldFromNative(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_default(self):
|
||||
|
||||
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', )
|
||||
|
||||
serializer = TagSerializer(data={
|
||||
'tag': 'reminder',
|
||||
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
|
||||
})
|
||||
|
||||
serializer.is_valid()
|
||||
expected = {
|
||||
'tagged_item': '/note/1/',
|
||||
'tag': u'reminder'
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
Loading…
Reference in New Issue
Block a user