Introducing implementation and tests

This commit is contained in:
lukasbuenger 2013-03-26 01:02:09 +01:00
parent dca24cd914
commit 7957591910
2 changed files with 491 additions and 0 deletions

View 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)

View 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)