Skip to content

Commit 1ad3f65

Browse files
committed
Support polymorphic models (from django-polymorphic and django-typed-models)
1 parent 697adf1 commit 1ad3f65

File tree

13 files changed

+207
-30
lines changed

13 files changed

+207
-30
lines changed

example/factories/__init__.py

+39-6
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,39 @@
33

44
import factory
55
from faker import Factory as FakerFactory
6-
from example.models import Blog, Author, AuthorBio, Entry, Comment
6+
from example import models
7+
78

89
faker = FakerFactory.create()
910
faker.seed(983843)
1011

12+
1113
class BlogFactory(factory.django.DjangoModelFactory):
1214
class Meta:
13-
model = Blog
15+
model = models.Blog
1416

1517
name = factory.LazyAttribute(lambda x: faker.name())
1618

1719

1820
class AuthorFactory(factory.django.DjangoModelFactory):
1921
class Meta:
20-
model = Author
22+
model = models.Author
2123

2224
name = factory.LazyAttribute(lambda x: faker.name())
2325
email = factory.LazyAttribute(lambda x: faker.email())
2426

2527

2628
class AuthorBioFactory(factory.django.DjangoModelFactory):
2729
class Meta:
28-
model = AuthorBio
30+
model = models.AuthorBio
2931

3032
author = factory.SubFactory(AuthorFactory)
3133
body = factory.LazyAttribute(lambda x: faker.text())
3234

3335

3436
class EntryFactory(factory.django.DjangoModelFactory):
3537
class Meta:
36-
model = Entry
38+
model = models.Entry
3739

3840
headline = factory.LazyAttribute(lambda x: faker.sentence(nb_words=4))
3941
body_text = factory.LazyAttribute(lambda x: faker.text())
@@ -52,9 +54,40 @@ def authors(self, create, extracted, **kwargs):
5254

5355
class CommentFactory(factory.django.DjangoModelFactory):
5456
class Meta:
55-
model = Comment
57+
model = models.Comment
5658

5759
entry = factory.SubFactory(EntryFactory)
5860
body = factory.LazyAttribute(lambda x: faker.text())
5961
author = factory.SubFactory(AuthorFactory)
6062

63+
64+
class ArtProjectFactory(factory.django.DjangoModelFactory):
65+
class Meta:
66+
model = models.ArtProject
67+
68+
topic = factory.LazyAttribute(lambda x: faker.catch_phrase())
69+
artist = factory.LazyAttribute(lambda x: faker.name())
70+
71+
72+
class ResearchProjectFactory(factory.django.DjangoModelFactory):
73+
class Meta:
74+
model = models.ResearchProject
75+
76+
topic = factory.LazyAttribute(lambda x: faker.catch_phrase())
77+
supervisor = factory.LazyAttribute(lambda x: faker.name())
78+
79+
80+
class CompanyFactory(factory.django.DjangoModelFactory):
81+
class Meta:
82+
model = models.Company
83+
84+
name = factory.LazyAttribute(lambda x: faker.company())
85+
current_project = factory.SubFactory(ArtProjectFactory)
86+
87+
@factory.post_generation
88+
def future_projects(self, create, extracted, **kwargs):
89+
if not create:
90+
return
91+
if extracted:
92+
for project in extracted:
93+
self.future_projects.add(project)

example/models.py

+22
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.db import models
55
from django.utils.encoding import python_2_unicode_compatible
6+
from polymorphic.models import PolymorphicModel
67

78

89
class BaseModel(models.Model):
@@ -72,3 +73,24 @@ class Comment(BaseModel):
7273
def __str__(self):
7374
return self.body
7475

76+
77+
class Project(PolymorphicModel):
78+
topic = models.CharField(max_length=30)
79+
80+
81+
class ArtProject(Project):
82+
artist = models.CharField(max_length=30)
83+
84+
85+
class ResearchProject(Project):
86+
supervisor = models.CharField(max_length=30)
87+
88+
89+
@python_2_unicode_compatible
90+
class Company(models.Model):
91+
name = models.CharField(max_length=100)
92+
current_project = models.ForeignKey(Project, related_name='companies')
93+
future_projects = models.ManyToManyField(Project)
94+
95+
def __str__(self):
96+
return self.name

example/serializers.py

+49-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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 import models
44

55

66
class BlogSerializer(serializers.ModelSerializer):
@@ -12,11 +12,11 @@ def get_copyright(self, obj):
1212

1313
def get_root_meta(self, obj):
1414
return {
15-
'api_docs': '/docs/api/blogs'
15+
'api_docs': '/docs/api/blogs'
1616
}
1717

1818
class Meta:
19-
model = Blog
19+
model = models.Blog
2020
fields = ('name', )
2121
meta_fields = ('copyright',)
2222

@@ -38,18 +38,18 @@ def __init__(self, *args, **kwargs):
3838

3939
body_format = serializers.SerializerMethodField()
4040
comments = relations.ResourceRelatedField(
41-
source='comment_set', many=True, read_only=True)
41+
source='comment_set', many=True, read_only=True)
4242
suggested = relations.SerializerMethodResourceRelatedField(
43-
source='get_suggested', model=Entry, read_only=True)
43+
source='get_suggested', model=models.Entry, read_only=True)
4444

4545
def get_suggested(self, obj):
46-
return Entry.objects.exclude(pk=obj.pk).first()
46+
return models.Entry.objects.exclude(pk=obj.pk).first()
4747

4848
def get_body_format(self, obj):
4949
return 'text'
5050

5151
class Meta:
52-
model = Entry
52+
model = models.Entry
5353
fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date',
5454
'authors', 'comments', 'suggested',)
5555
meta_fields = ('body_format',)
@@ -58,7 +58,7 @@ class Meta:
5858
class AuthorBioSerializer(serializers.ModelSerializer):
5959

6060
class Meta:
61-
model = AuthorBio
61+
model = models.AuthorBio
6262
fields = ('author', 'body',)
6363

6464

@@ -68,13 +68,52 @@ class AuthorSerializer(serializers.ModelSerializer):
6868
}
6969

7070
class Meta:
71-
model = Author
71+
model = models.Author
7272
fields = ('name', 'email', 'bio')
7373

7474

7575
class CommentSerializer(serializers.ModelSerializer):
7676

7777
class Meta:
78-
model = Comment
78+
model = models.Comment
7979
exclude = ('created_at', 'modified_at',)
8080
# fields = ('entry', 'body', 'author',)
81+
82+
83+
class ArtProjectSerializer(serializers.ModelSerializer):
84+
class Meta:
85+
model = models.ArtProject
86+
exclude = ('polymorphic_ctype',)
87+
88+
89+
class ResearchProjectSerializer(serializers.ModelSerializer):
90+
class Meta:
91+
model = models.ResearchProject
92+
exclude = ('polymorphic_ctype',)
93+
94+
95+
class ProjectSerializer(serializers.ModelSerializer):
96+
97+
class Meta:
98+
model = models.Project
99+
exclude = ('polymorphic_ctype',)
100+
101+
def to_representation(self, instance):
102+
# Handle polymorphism
103+
if isinstance(instance, models.ArtProject):
104+
return ArtProjectSerializer(
105+
instance, context=self.context).to_representation(instance)
106+
elif isinstance(instance, models.ResearchProject):
107+
return ResearchProjectSerializer(
108+
instance, context=self.context).to_representation(instance)
109+
return super(ProjectSerializer, self).to_representation(instance)
110+
111+
112+
class CompanySerializer(serializers.ModelSerializer):
113+
included_serializers = {
114+
'current_project': ProjectSerializer,
115+
'future_projects': ProjectSerializer,
116+
}
117+
118+
class Meta:
119+
model = models.Company

example/settings/dev.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
'django.contrib.auth',
2424
'django.contrib.admin',
2525
'rest_framework',
26+
'polymorphic',
2627
'example',
2728
]
2829

example/tests/conftest.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import pytest
22
from pytest_factoryboy import register
33

4-
from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory
4+
from example import factories
55

6-
register(BlogFactory)
7-
register(AuthorFactory)
8-
register(AuthorBioFactory)
9-
register(EntryFactory)
10-
register(CommentFactory)
6+
register(factories.BlogFactory)
7+
register(factories.AuthorFactory)
8+
register(factories.AuthorBioFactory)
9+
register(factories.EntryFactory)
10+
register(factories.CommentFactory)
11+
register(factories.ArtProjectFactory)
12+
register(factories.ResearchProjectFactory)
13+
register(factories.CompanyFactory)
1114

1215

1316
@pytest.fixture
@@ -29,3 +32,13 @@ def multiple_entries(blog_factory, author_factory, entry_factory, comment_factor
2932
comment_factory(entry=entries[1])
3033
return entries
3134

35+
36+
@pytest.fixture
37+
def single_company(art_project_factory, research_project_factory, company_factory):
38+
company = company_factory(future_projects=(research_project_factory(), art_project_factory()))
39+
return company
40+
41+
42+
@pytest.fixture
43+
def single_art_project(art_project_factory):
44+
return art_project_factory()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pytest
2+
from django.core.urlresolvers import reverse
3+
4+
from example.tests.utils import load_json
5+
6+
pytestmark = pytest.mark.django_db
7+
8+
9+
def test_polymorphism_on_detail(single_art_project, client):
10+
response = client.get(reverse("project-detail", kwargs={'pk': single_art_project.pk}))
11+
content = load_json(response.content)
12+
assert content["data"]["type"] == "artProjects"
13+
14+
15+
def test_polymorphism_on_detail_relations(single_company, client):
16+
response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
17+
content = load_json(response.content)
18+
assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
19+
assert [rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]] == [
20+
"researchProjects", "artProjects"]
21+
22+
23+
def test_polymorphism_on_included_relations(single_company, client):
24+
response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}) +
25+
'?include=current_project,future_projects')
26+
content = load_json(response.content)
27+
assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
28+
assert [rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]] == [
29+
"researchProjects", "artProjects"]
30+
assert [x.get('type') for x in content.get('included')] == ['artProjects', 'artProjects', 'researchProjects'], \
31+
'Detail included types are incorrect'

example/urls.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from django.conf.urls import include, url
22
from rest_framework import routers
33

4-
from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet
4+
from example.views import (
5+
BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet, CompanyViewset, ProjectViewset)
56

67
router = routers.DefaultRouter(trailing_slash=False)
78

89
router.register(r'blogs', BlogViewSet)
910
router.register(r'entries', EntryViewSet)
1011
router.register(r'authors', AuthorViewSet)
1112
router.register(r'comments', CommentViewSet)
13+
router.register(r'companies', CompanyViewset)
14+
router.register(r'projects', ProjectViewset)
1215

1316
urlpatterns = [
1417
url(r'^', include(router.urls)),

example/urls_test.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from django.conf.urls import include, url
22
from rest_framework import routers
33

4-
from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet, EntryRelationshipView, BlogRelationshipView, \
5-
CommentRelationshipView, AuthorRelationshipView
4+
from example.views import (
5+
BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet, CompanyViewset, ProjectViewset,
6+
EntryRelationshipView, BlogRelationshipView, CommentRelationshipView, AuthorRelationshipView)
67
from .api.resources.identity import Identity, GenericIdentity
78

89
router = routers.DefaultRouter(trailing_slash=False)
@@ -11,6 +12,8 @@
1112
router.register(r'entries', EntryViewSet)
1213
router.register(r'authors', AuthorViewSet)
1314
router.register(r'comments', CommentViewSet)
15+
router.register(r'companies', CompanyViewset)
16+
router.register(r'projects', ProjectViewset)
1417

1518
# for the old tests
1619
router.register(r'identities', Identity)
@@ -36,4 +39,3 @@
3639
AuthorRelationshipView.as_view(),
3740
name='author-relationships'),
3841
]
39-

example/views.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from rest_framework import viewsets
22
from rest_framework_json_api.views import RelationshipView
3-
from example.models import Blog, Entry, Author, Comment
3+
from example.models import Blog, Entry, Author, Comment, Company, Project
44
from example.serializers import (
5-
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
5+
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer, CompanySerializer,
6+
ProjectSerializer)
67

78

89
class BlogViewSet(viewsets.ModelViewSet):
@@ -26,6 +27,16 @@ class CommentViewSet(viewsets.ModelViewSet):
2627
serializer_class = CommentSerializer
2728

2829

30+
class CompanyViewset(viewsets.ModelViewSet):
31+
queryset = Company.objects.all()
32+
serializer_class = CompanySerializer
33+
34+
35+
class ProjectViewset(viewsets.ModelViewSet):
36+
queryset = Project.objects.all()
37+
serializer_class = ProjectSerializer
38+
39+
2940
class EntryRelationshipView(RelationshipView):
3041
queryset = Entry.objects.all()
3142

requirements-development.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ pytest==2.8.2
33
pytest-django
44
pytest-factoryboy
55
fake-factory
6+
django-polymorphic
67
tox

0 commit comments

Comments
 (0)