From 82c81ef33db4541eb5c065962859f6f02ec52e88 Mon Sep 17 00:00:00 2001 From: "santiago.avendano" Date: Wed, 1 Feb 2017 11:04:22 -0300 Subject: [PATCH 1/6] Change fake-factory (deprecated) requirement to Faker --- requirements-development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-development.txt b/requirements-development.txt index 1ac48b58..9bafab94 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -2,6 +2,6 @@ pytest>=2.9.0,<3.0 pytest-django pytest-factoryboy -fake-factory +Faker tox mock From ab27cd120ce8994bbd5df7ba6f3c19a8116d4c6d Mon Sep 17 00:00:00 2001 From: "santiago.avendano" Date: Wed, 1 Feb 2017 11:26:07 -0300 Subject: [PATCH 2/6] Added support for GenericRelation --- rest_framework_json_api/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 3f247da8..d30aae1d 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -12,6 +12,7 @@ import django from django.conf import settings +from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor from django.db.models import Manager from django.utils import encoding, six from django.utils.module_loading import import_string as import_class_from_dotted_path @@ -203,17 +204,20 @@ 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: + relation_model = parent_model_relation.rel.model else: return get_related_resource_type(parent_model_relation) From cfd094151a78fd7191f868852582d640fe9b1f3e Mon Sep 17 00:00:00 2001 From: "santiago.avendano" Date: Wed, 1 Feb 2017 11:57:07 -0300 Subject: [PATCH 3/6] Added GenericRelation example. Fix tests --- example/migrations/0002_taggeditem.py | 31 +++++++++++++++++++ example/models.py | 15 +++++++++ example/serializers.py | 22 +++++++++++-- example/tests/integration/test_meta.py | 10 ++++++ .../test_non_paginated_responses.py | 6 ++++ example/tests/integration/test_pagination.py | 3 ++ 6 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 example/migrations/0002_taggeditem.py 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..ea77d2fa 100644 --- a/example/models.py +++ b/example/models.py @@ -3,6 +3,9 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericRelation class BaseModel(models.Model): @@ -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 e20a7c5f..19283a4f 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',) diff --git a/example/tests/integration/test_meta.py b/example/tests/integration/test_meta.py index ce8b4fab..cf38b806 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 }, @@ -51,6 +56,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 d0a9adb0..50ae7e2d 100644 --- a/example/tests/integration/test_non_paginated_responses.py +++ b/example/tests/integration/test_non_paginated_responses.py @@ -48,6 +48,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": [] } } }, @@ -83,6 +86,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 482ac460..1f04c2df 100644 --- a/example/tests/integration/test_pagination.py +++ b/example/tests/integration/test_pagination.py @@ -42,6 +42,9 @@ def test_pagination_with_single_entry(single_entry, client): "related": "http://testserver/entries/1/suggested/", "self": "http://testserver/entries/1/relationships/suggested" } + }, + "tags": { + "data": [] } } }], From cd83e5c9628f25d5b43673246a404dc9b25672ba Mon Sep 17 00:00:00 2001 From: "santiago.avendano" Date: Wed, 1 Feb 2017 12:23:47 -0300 Subject: [PATCH 4/6] Added GenericRelations tests --- example/factories/__init__.py | 10 +++++++++- example/tests/conftest.py | 7 +++++-- example/tests/integration/test_pagination.py | 7 ++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/example/factories/__init__.py b/example/factories/__init__.py index 0119f925..e37072da 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 EntryTaggedItemFactory(factory.django.DjangoModelFactory): + + class Meta: + model = TaggedItem + + content_object = factory.SubFactory(EntryFactory) + tag = factory.LazyAttribute(lambda x: faker.word()) diff --git a/example/tests/conftest.py b/example/tests/conftest.py index 8a96cfdb..161139e5 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, \ + EntryTaggedItemFactory register(BlogFactory) register(AuthorFactory) register(AuthorBioFactory) register(EntryFactory) register(CommentFactory) +register(EntryTaggedItemFactory) @pytest.fixture -def single_entry(blog, author, entry_factory, comment_factory): +def single_entry(blog, author, entry_factory, comment_factory, entry_tagged_item_factory): entry = entry_factory(blog=blog, authors=(author,)) comment_factory(entry=entry) + entry_tagged_item_factory(content_object=entry) return entry diff --git a/example/tests/integration/test_pagination.py b/example/tests/integration/test_pagination.py index 1f04c2df..d1e9f1c1 100644 --- a/example/tests/integration/test_pagination.py +++ b/example/tests/integration/test_pagination.py @@ -44,7 +44,12 @@ def test_pagination_with_single_entry(single_entry, client): } }, "tags": { - "data": [] + "data": [ + { + "id": "1", + "type": "taggedItems" + } + ] } } }], From 0fd45e4896046b687800c62edf02cfbd3031d363 Mon Sep 17 00:00:00 2001 From: "santiago.avendano" Date: Wed, 1 Feb 2017 14:55:53 -0300 Subject: [PATCH 5/6] Added support for GenericRelations on django 1.8 --- rest_framework_json_api/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index d30aae1d..6bfc60a3 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -12,7 +12,6 @@ import django from django.conf import settings -from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor from django.db.models import Manager from django.utils import encoding, six from django.utils.module_loading import import_string as import_class_from_dotted_path @@ -31,11 +30,14 @@ 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 + ReverseGenericRelatedObjectsDescriptor = type(None) 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 + ReverseGenericManyToOneDescriptor = type(None) def get_resource_name(context): """ @@ -218,6 +220,8 @@ def get_related_resource_type(relation): relation_model = parent_model_relation.field.related.model elif parent_model_relation_type is ReverseGenericManyToOneDescriptor: relation_model = parent_model_relation.rel.model + elif parent_model_relation_type is ReverseGenericRelatedObjectsDescriptor: + relation_model = parent_model_relation.field.related_model else: return get_related_resource_type(parent_model_relation) From f9bfcc96f99e0d4b2bf6ad7b338f33d716c6fb37 Mon Sep 17 00:00:00 2001 From: "santiago.avendano" Date: Thu, 23 Feb 2017 09:50:17 -0300 Subject: [PATCH 6/6] implement requested changes --- example/factories/__init__.py | 2 +- example/models.py | 4 ++-- example/tests/conftest.py | 8 ++++---- rest_framework_json_api/utils.py | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/example/factories/__init__.py b/example/factories/__init__.py index e37072da..bd689cf7 100644 --- a/example/factories/__init__.py +++ b/example/factories/__init__.py @@ -59,7 +59,7 @@ class Meta: author = factory.SubFactory(AuthorFactory) -class EntryTaggedItemFactory(factory.django.DjangoModelFactory): +class TaggedItemFactory(factory.django.DjangoModelFactory): class Meta: model = TaggedItem diff --git a/example/models.py b/example/models.py index ea77d2fa..7ded4ebf 100644 --- a/example/models.py +++ b/example/models.py @@ -1,11 +1,11 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals -from django.db import models -from django.utils.encoding import python_2_unicode_compatible 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 class BaseModel(models.Model): diff --git a/example/tests/conftest.py b/example/tests/conftest.py index 161139e5..8e3c8f40 100644 --- a/example/tests/conftest.py +++ b/example/tests/conftest.py @@ -2,22 +2,22 @@ from pytest_factoryboy import register from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory, \ - EntryTaggedItemFactory + TaggedItemFactory register(BlogFactory) register(AuthorFactory) register(AuthorBioFactory) register(EntryFactory) register(CommentFactory) -register(EntryTaggedItemFactory) +register(TaggedItemFactory) @pytest.fixture -def single_entry(blog, author, entry_factory, comment_factory, entry_tagged_item_factory): +def single_entry(blog, author, entry_factory, comment_factory, tagged_item_factory): entry = entry_factory(blog=blog, authors=(author,)) comment_factory(entry=entry) - entry_tagged_item_factory(content_object=entry) + tagged_item_factory(content_object=entry) return entry diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 55f709f6..26079c0c 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -31,13 +31,12 @@ from django.db.models.fields.related_descriptors import ManyToManyDescriptor, ReverseManyToOneDescriptor ReverseManyRelatedObjectsDescriptor = type(None) from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor - ReverseGenericRelatedObjectsDescriptor = type(None) 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 - ReverseGenericManyToOneDescriptor = type(None) + from django.contrib.contenttypes.fields import ReverseGenericRelatedObjectsDescriptor as ReverseGenericManyToOneDescriptor + def get_resource_name(context): """ @@ -226,9 +225,10 @@ def get_related_resource_type(relation): elif parent_model_relation_type is ReverseManyRelatedObjectsDescriptor: relation_model = parent_model_relation.field.related.model elif parent_model_relation_type is ReverseGenericManyToOneDescriptor: - relation_model = parent_model_relation.rel.model - elif parent_model_relation_type is ReverseGenericRelatedObjectsDescriptor: - relation_model = parent_model_relation.field.related_model + 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)