Skip to content

Commit fbe49a1

Browse files
santiavenda2mblayman
authored andcommitted
Add support for GenericRelations (#319)
* Change fake-factory (deprecated) requirement to Faker * Added support for GenericRelation * Added GenericRelation example. Fix tests * Added GenericRelations tests * Added support for GenericRelations on django 1.8 * implement requested changes
1 parent a19f445 commit fbe49a1

File tree

10 files changed

+115
-10
lines changed

10 files changed

+115
-10
lines changed

‎example/factories/__init__.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import factory
44
from faker import Factory as FakerFactory
5-
from example.models import Blog, Author, AuthorBio, Entry, Comment
5+
from example.models import Blog, Author, AuthorBio, Entry, Comment, TaggedItem
66

77
faker = FakerFactory.create()
88
faker.seed(983843)
@@ -58,3 +58,11 @@ class Meta:
5858
body = factory.LazyAttribute(lambda x: faker.text())
5959
author = factory.SubFactory(AuthorFactory)
6060

61+
62+
class TaggedItemFactory(factory.django.DjangoModelFactory):
63+
64+
class Meta:
65+
model = TaggedItem
66+
67+
content_object = factory.SubFactory(EntryFactory)
68+
tag = factory.LazyAttribute(lambda x: faker.word())

‎example/migrations/0002_taggeditem.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.10.5 on 2017-02-01 08:34
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('contenttypes', '0002_remove_content_type_name'),
13+
('example', '0001_initial'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='TaggedItem',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('created_at', models.DateTimeField(auto_now_add=True)),
22+
('modified_at', models.DateTimeField(auto_now=True)),
23+
('tag', models.SlugField()),
24+
('object_id', models.PositiveIntegerField()),
25+
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
26+
],
27+
options={
28+
'abstract': False,
29+
},
30+
),
31+
]

‎example/models.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# -*- encoding: utf-8 -*-
22
from __future__ import unicode_literals
33

4+
from django.contrib.contenttypes.models import ContentType
5+
from django.contrib.contenttypes.fields import GenericForeignKey
6+
from django.contrib.contenttypes.fields import GenericRelation
47
from django.db import models
58
from django.utils.encoding import python_2_unicode_compatible
69

@@ -16,10 +19,21 @@ class Meta:
1619
abstract = True
1720

1821

22+
class TaggedItem(BaseModel):
23+
tag = models.SlugField()
24+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
25+
object_id = models.PositiveIntegerField()
26+
content_object = GenericForeignKey('content_type', 'object_id')
27+
28+
def __str__(self):
29+
return self.tag
30+
31+
1932
@python_2_unicode_compatible
2033
class Blog(BaseModel):
2134
name = models.CharField(max_length=100)
2235
tagline = models.TextField()
36+
tags = GenericRelation(TaggedItem)
2337

2438
def __str__(self):
2539
return self.name
@@ -54,6 +68,7 @@ class Entry(BaseModel):
5468
n_comments = models.IntegerField(default=0)
5569
n_pingbacks = models.IntegerField(default=0)
5670
rating = models.IntegerField(default=0)
71+
tags = GenericRelation(TaggedItem)
5772

5873
def __str__(self):
5974
return self.headline

‎example/serializers.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
from datetime import datetime
22
from rest_framework_json_api import serializers, relations
3-
from example.models import Blog, Entry, Author, AuthorBio, Comment
3+
from example.models import Blog, Entry, Author, AuthorBio, Comment, TaggedItem
4+
5+
6+
class TaggedItemSerializer(serializers.ModelSerializer):
7+
8+
class Meta:
9+
model = TaggedItem
10+
fields = ('tag', )
411

512

613
class BlogSerializer(serializers.ModelSerializer):
714

815
copyright = serializers.SerializerMethodField()
16+
tags = TaggedItemSerializer(many=True, read_only=True)
17+
18+
include_serializers = {
19+
'tags': 'example.serializers.TaggedItemSerializer',
20+
}
921

1022
def get_copyright(self, resource):
1123
return datetime.now().year
@@ -17,7 +29,8 @@ def get_root_meta(self, resource, many):
1729

1830
class Meta:
1931
model = Blog
20-
fields = ('name', 'url',)
32+
fields = ('name', 'url', 'tags')
33+
read_only_fields = ('tags', )
2134
meta_fields = ('copyright',)
2235

2336

@@ -36,6 +49,7 @@ def __init__(self, *args, **kwargs):
3649
'comments': 'example.serializers.CommentSerializer',
3750
'featured': 'example.serializers.EntrySerializer',
3851
'suggested': 'example.serializers.EntrySerializer',
52+
'tags': 'example.serializers.TaggedItemSerializer',
3953
}
4054

4155
body_format = serializers.SerializerMethodField()
@@ -52,6 +66,7 @@ def __init__(self, *args, **kwargs):
5266
# single related from serializer
5367
featured = relations.SerializerMethodResourceRelatedField(
5468
source='get_featured', model=Entry, read_only=True)
69+
tags = TaggedItemSerializer(many=True, read_only=True)
5570

5671
def get_suggested(self, obj):
5772
return Entry.objects.exclude(pk=obj.pk)
@@ -65,7 +80,8 @@ def get_body_format(self, obj):
6580
class Meta:
6681
model = Entry
6782
fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date',
68-
'authors', 'comments', 'featured', 'suggested',)
83+
'authors', 'comments', 'featured', 'suggested', 'tags')
84+
read_only_fields = ('tags', )
6985
meta_fields = ('body_format',)
7086

7187
class JSONAPIMeta:

‎example/tests/conftest.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import pytest
22
from pytest_factoryboy import register
33

4-
from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory
4+
from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory, \
5+
TaggedItemFactory
56

67
register(BlogFactory)
78
register(AuthorFactory)
89
register(AuthorBioFactory)
910
register(EntryFactory)
1011
register(CommentFactory)
12+
register(TaggedItemFactory)
1113

1214

1315
@pytest.fixture
14-
def single_entry(blog, author, entry_factory, comment_factory):
16+
def single_entry(blog, author, entry_factory, comment_factory, tagged_item_factory):
1517

1618
entry = entry_factory(blog=blog, authors=(author,))
1719
comment_factory(entry=entry)
20+
tagged_item_factory(content_object=entry)
1821
return entry
1922

2023

‎example/tests/integration/test_meta.py

+10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ def test_top_level_meta_for_list_view(blog, client):
1919
"links": {
2020
"self": 'http://testserver/blogs/1'
2121
},
22+
"relationships": {
23+
"tags": {
24+
"data": []
25+
}
26+
},
2227
"meta": {
2328
"copyright": datetime.now().year
2429
},
@@ -50,6 +55,11 @@ def test_top_level_meta_for_detail_view(blog, client):
5055
"attributes": {
5156
"name": blog.name
5257
},
58+
"relationships": {
59+
"tags": {
60+
"data": []
61+
}
62+
},
5363
"links": {
5464
"self": "http://testserver/blogs/1"
5565
},

‎example/tests/integration/test_non_paginated_responses.py

+6
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf):
5757
"related": "http://testserver/entries/1/suggested/",
5858
"self": "http://testserver/entries/1/relationships/suggested"
5959
}
60+
},
61+
"tags": {
62+
"data": []
6063
}
6164
}
6265
},
@@ -92,6 +95,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf):
9295
"related": "http://testserver/entries/2/suggested/",
9396
"self": "http://testserver/entries/2/relationships/suggested"
9497
}
98+
},
99+
"tags": {
100+
"data": []
95101
}
96102
}
97103
},

‎example/tests/integration/test_pagination.py

+8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ def test_pagination_with_single_entry(single_entry, client):
5050
"related": "http://testserver/entries/1/suggested/",
5151
"self": "http://testserver/entries/1/relationships/suggested"
5252
}
53+
},
54+
"tags": {
55+
"data": [
56+
{
57+
"id": "1",
58+
"type": "taggedItems"
59+
}
60+
]
5361
}
5462
}
5563
}],

‎requirements-development.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pytest>=2.9.0,<3.0
33
pytest-django
44
pytest-factoryboy
5-
fake-factory
5+
Faker
66
recommonmark
77
Sphinx
88
sphinx_rtd_theme

‎rest_framework_json_api/utils.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@
3030
if django.VERSION >= (1, 9):
3131
from django.db.models.fields.related_descriptors import ManyToManyDescriptor, ReverseManyToOneDescriptor
3232
ReverseManyRelatedObjectsDescriptor = type(None)
33+
from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor
3334
else:
3435
from django.db.models.fields.related import ManyRelatedObjectsDescriptor as ManyToManyDescriptor
3536
from django.db.models.fields.related import ForeignRelatedObjectsDescriptor as ReverseManyToOneDescriptor
3637
from django.db.models.fields.related import ReverseManyRelatedObjectsDescriptor
38+
from django.contrib.contenttypes.fields import ReverseGenericRelatedObjectsDescriptor as ReverseGenericManyToOneDescriptor
3739

3840

3941
def get_resource_name(context):
@@ -210,17 +212,23 @@ def get_related_resource_type(relation):
210212
else:
211213
parent_model_relation = getattr(parent_model, parent_serializer.field_name)
212214

213-
if type(parent_model_relation) is ReverseManyToOneDescriptor:
215+
parent_model_relation_type = type(parent_model_relation)
216+
if parent_model_relation_type is ReverseManyToOneDescriptor:
214217
if django.VERSION >= (1, 9):
215218
relation_model = parent_model_relation.rel.related_model
216219
elif django.VERSION >= (1, 8):
217220
relation_model = parent_model_relation.related.related_model
218221
else:
219222
relation_model = parent_model_relation.related.model
220-
elif type(parent_model_relation) is ManyToManyDescriptor:
223+
elif parent_model_relation_type is ManyToManyDescriptor:
221224
relation_model = parent_model_relation.field.remote_field.model
222-
elif type(parent_model_relation) is ReverseManyRelatedObjectsDescriptor:
225+
elif parent_model_relation_type is ReverseManyRelatedObjectsDescriptor:
223226
relation_model = parent_model_relation.field.related.model
227+
elif parent_model_relation_type is ReverseGenericManyToOneDescriptor:
228+
if django.VERSION >= (1, 9):
229+
relation_model = parent_model_relation.rel.model
230+
else:
231+
relation_model = parent_model_relation.field.related_model
224232
else:
225233
return get_related_resource_type(parent_model_relation)
226234

0 commit comments

Comments
 (0)