diff --git a/example/factories/__init__.py b/example/factories/__init__.py index 0119f925..bd689cf7 100644 --- a/example/factories/__init__.py +++ b/example/factories/__init__.py @@ -2,7 +2,7 @@ import factory from faker import Factory as FakerFactory -from example.models import Blog, Author, AuthorBio, Entry, Comment +from example.models import Blog, Author, AuthorBio, Entry, Comment, TaggedItem faker = FakerFactory.create() faker.seed(983843) @@ -58,3 +58,11 @@ class Meta: body = factory.LazyAttribute(lambda x: faker.text()) author = factory.SubFactory(AuthorFactory) + +class TaggedItemFactory(factory.django.DjangoModelFactory): + + class Meta: + model = TaggedItem + + content_object = factory.SubFactory(EntryFactory) + tag = factory.LazyAttribute(lambda x: faker.word()) diff --git a/example/migrations/0002_taggeditem.py b/example/migrations/0002_taggeditem.py new file mode 100644 index 00000000..46a79de9 --- /dev/null +++ b/example/migrations/0002_taggeditem.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-01 08:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('example', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='TaggedItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('tag', models.SlugField()), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/example/models.py b/example/models.py index 7895722a..7ded4ebf 100644 --- a/example/models.py +++ b/example/models.py @@ -1,6 +1,9 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -16,10 +19,21 @@ class Meta: abstract = True +class TaggedItem(BaseModel): + tag = models.SlugField() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + def __str__(self): + return self.tag + + @python_2_unicode_compatible class Blog(BaseModel): name = models.CharField(max_length=100) tagline = models.TextField() + tags = GenericRelation(TaggedItem) def __str__(self): return self.name @@ -54,6 +68,7 @@ class Entry(BaseModel): n_comments = models.IntegerField(default=0) n_pingbacks = models.IntegerField(default=0) rating = models.IntegerField(default=0) + tags = GenericRelation(TaggedItem) def __str__(self): return self.headline diff --git a/example/serializers.py b/example/serializers.py index 1454f72a..c5eb1a18 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,11 +1,23 @@ from datetime import datetime from rest_framework_json_api import serializers, relations -from example.models import Blog, Entry, Author, AuthorBio, Comment +from example.models import Blog, Entry, Author, AuthorBio, Comment, TaggedItem + + +class TaggedItemSerializer(serializers.ModelSerializer): + + class Meta: + model = TaggedItem + fields = ('tag', ) class BlogSerializer(serializers.ModelSerializer): copyright = serializers.SerializerMethodField() + tags = TaggedItemSerializer(many=True, read_only=True) + + include_serializers = { + 'tags': 'example.serializers.TaggedItemSerializer', + } def get_copyright(self, resource): return datetime.now().year @@ -17,7 +29,8 @@ def get_root_meta(self, resource, many): class Meta: model = Blog - fields = ('name', 'url',) + fields = ('name', 'url', 'tags') + read_only_fields = ('tags', ) meta_fields = ('copyright',) @@ -36,6 +49,7 @@ def __init__(self, *args, **kwargs): 'comments': 'example.serializers.CommentSerializer', 'featured': 'example.serializers.EntrySerializer', 'suggested': 'example.serializers.EntrySerializer', + 'tags': 'example.serializers.TaggedItemSerializer', } body_format = serializers.SerializerMethodField() @@ -52,6 +66,7 @@ def __init__(self, *args, **kwargs): # single related from serializer featured = relations.SerializerMethodResourceRelatedField( source='get_featured', model=Entry, read_only=True) + tags = TaggedItemSerializer(many=True, read_only=True) def get_suggested(self, obj): return Entry.objects.exclude(pk=obj.pk) @@ -65,7 +80,8 @@ def get_body_format(self, obj): class Meta: model = Entry fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date', - 'authors', 'comments', 'featured', 'suggested',) + 'authors', 'comments', 'featured', 'suggested', 'tags') + read_only_fields = ('tags', ) meta_fields = ('body_format',) class JSONAPIMeta: diff --git a/example/tests/conftest.py b/example/tests/conftest.py index 8a96cfdb..8e3c8f40 100644 --- a/example/tests/conftest.py +++ b/example/tests/conftest.py @@ -1,20 +1,23 @@ import pytest from pytest_factoryboy import register -from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory +from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory, \ + TaggedItemFactory register(BlogFactory) register(AuthorFactory) register(AuthorBioFactory) register(EntryFactory) register(CommentFactory) +register(TaggedItemFactory) @pytest.fixture -def single_entry(blog, author, entry_factory, comment_factory): +def single_entry(blog, author, entry_factory, comment_factory, tagged_item_factory): entry = entry_factory(blog=blog, authors=(author,)) comment_factory(entry=entry) + tagged_item_factory(content_object=entry) return entry diff --git a/example/tests/integration/test_meta.py b/example/tests/integration/test_meta.py index 1ebe6689..8365f095 100644 --- a/example/tests/integration/test_meta.py +++ b/example/tests/integration/test_meta.py @@ -19,6 +19,11 @@ def test_top_level_meta_for_list_view(blog, client): "links": { "self": 'http://testserver/blogs/1' }, + "relationships": { + "tags": { + "data": [] + } + }, "meta": { "copyright": datetime.now().year }, @@ -50,6 +55,11 @@ def test_top_level_meta_for_detail_view(blog, client): "attributes": { "name": blog.name }, + "relationships": { + "tags": { + "data": [] + } + }, "links": { "self": "http://testserver/blogs/1" }, diff --git a/example/tests/integration/test_non_paginated_responses.py b/example/tests/integration/test_non_paginated_responses.py index 28799dda..8473a077 100644 --- a/example/tests/integration/test_non_paginated_responses.py +++ b/example/tests/integration/test_non_paginated_responses.py @@ -57,6 +57,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf): "related": "http://testserver/entries/1/suggested/", "self": "http://testserver/entries/1/relationships/suggested" } + }, + "tags": { + "data": [] } } }, @@ -92,6 +95,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf): "related": "http://testserver/entries/2/suggested/", "self": "http://testserver/entries/2/relationships/suggested" } + }, + "tags": { + "data": [] } } }, diff --git a/example/tests/integration/test_pagination.py b/example/tests/integration/test_pagination.py index 2be33150..656ea709 100644 --- a/example/tests/integration/test_pagination.py +++ b/example/tests/integration/test_pagination.py @@ -50,6 +50,14 @@ def test_pagination_with_single_entry(single_entry, client): "related": "http://testserver/entries/1/suggested/", "self": "http://testserver/entries/1/relationships/suggested" } + }, + "tags": { + "data": [ + { + "id": "1", + "type": "taggedItems" + } + ] } } }], diff --git a/requirements-development.txt b/requirements-development.txt index 347619e2..30ae754a 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -2,7 +2,7 @@ pytest>=2.9.0,<3.0 pytest-django pytest-factoryboy -fake-factory +Faker recommonmark Sphinx sphinx_rtd_theme diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index ae41f536..26079c0c 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -30,10 +30,12 @@ if django.VERSION >= (1, 9): from django.db.models.fields.related_descriptors import ManyToManyDescriptor, ReverseManyToOneDescriptor ReverseManyRelatedObjectsDescriptor = type(None) + from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor else: from django.db.models.fields.related import ManyRelatedObjectsDescriptor as ManyToManyDescriptor from django.db.models.fields.related import ForeignRelatedObjectsDescriptor as ReverseManyToOneDescriptor from django.db.models.fields.related import ReverseManyRelatedObjectsDescriptor + from django.contrib.contenttypes.fields import ReverseGenericRelatedObjectsDescriptor as ReverseGenericManyToOneDescriptor def get_resource_name(context): @@ -210,17 +212,23 @@ def get_related_resource_type(relation): else: parent_model_relation = getattr(parent_model, parent_serializer.field_name) - if type(parent_model_relation) is ReverseManyToOneDescriptor: + parent_model_relation_type = type(parent_model_relation) + if parent_model_relation_type is ReverseManyToOneDescriptor: if django.VERSION >= (1, 9): relation_model = parent_model_relation.rel.related_model elif django.VERSION >= (1, 8): relation_model = parent_model_relation.related.related_model else: relation_model = parent_model_relation.related.model - elif type(parent_model_relation) is ManyToManyDescriptor: + elif parent_model_relation_type is ManyToManyDescriptor: relation_model = parent_model_relation.field.remote_field.model - elif type(parent_model_relation) is ReverseManyRelatedObjectsDescriptor: + elif parent_model_relation_type is ReverseManyRelatedObjectsDescriptor: relation_model = parent_model_relation.field.related.model + elif parent_model_relation_type is ReverseGenericManyToOneDescriptor: + if django.VERSION >= (1, 9): + relation_model = parent_model_relation.rel.model + else: + relation_model = parent_model_relation.field.related_model else: return get_related_resource_type(parent_model_relation)