Skip to content

Commit 6140662

Browse files
leo-naekamblayman
authored andcommitted
Support polymorphic models (#211)
* Support polymorphic models (from django-polymorphic and django-typed-models) * Polymorphic ancestors must now be defined in Django's settings Update documentation * Adds the following features: - support for post and patch request on polymorphic model endpoints. - makes polymorphic serializers give child fields instead of its own. * Fix example migration and tests Update gitignore * Polymorphic serializers refactor * Basic support of write operations on polymorphic relations * Improve polymorphism documentation * Improve polymorphic relations and tests. * Add django-polymorphic as test dependency pytest-factoryboy does not support pytest3.0+ * Avoid type list comparison in polymorphic tests * Flake8 * Flake8 * Better handle imports? * Resolve circular reference * Really break up import loop * Missed something in the merge * Redo migrations * Wrong indentation * Fix a deprecation * Fix polymorphic type resolution in relations * Fix tests among different environments * Update tox.ini environment list * Add packaging module as requirement for old python versions * Remove the POLYMORPHIC_ANCESTOR code * Fix some typos and little errors * Administrivia * Restore generic relation support Tests? * Add Leo to authors * PEP8 * Really bad writing. * Editing
1 parent 629d1d6 commit 6140662

22 files changed

+682
-39
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pip-delete-this-directory.txt
3434

3535
# Tox
3636
.tox/
37+
.cache/
38+
.python-version
3739

3840
# VirtualEnv
3941
.venv/

AUTHORS

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
Adam Wróbel <https://adamwrobel.com>
22
Christian Zosel <https://zosel.ch>
33
Greg Aker <greg@gregaker.net>
4+
Jamie Bliss <astronouth7303@gmail.com>
45
Jerel Unruh <mail@unruhdesigns.com>
6+
Léo S. <leo@naeka.fr>
57
Matt Layman <http://www.mattlayman.com>
68
Oliver Sauder <os@esite.ch>
79
Raphael Cohen <raphael.cohen.utt@gmail.com>

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
v2.3.0
22

3+
* Added support for polymorphic models
34
* When `JSON_API_FORMAT_KEYS` is False (the default) do not translate request
45
attributes and relations to snake\_case format. This conversion was unexpected
56
and there was no way to turn it off.

docs/usage.md

+53
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,59 @@ field_name_mapping = {
425425
```
426426

427427

428+
### Working with polymorphic resources
429+
430+
Polymorphic resources allow you to use specialized subclasses without requiring
431+
special endpoints to expose the specialized versions. For example, if you had a
432+
`Project` that could be either an `ArtProject` or a `ResearchProject`, you can
433+
have both kinds at the same URL.
434+
435+
DJA tests its polymorphic support against [django-polymorphic](https://django-polymorphic.readthedocs.io/en/stable/).
436+
The polymorphic feature should also work with other popular libraries like
437+
django-polymodels or django-typed-models.
438+
439+
#### Writing polymorphic resources
440+
441+
A polymorphic endpoint can be set up if associated with a polymorphic serializer.
442+
A polymorphic serializer takes care of (de)serializing the correct instances types and can be defined like this:
443+
444+
```python
445+
class ProjectSerializer(serializers.PolymorphicModelSerializer):
446+
polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer]
447+
448+
class Meta:
449+
model = models.Project
450+
```
451+
452+
It must inherit from `serializers.PolymorphicModelSerializer` and define the `polymorphic_serializers` list.
453+
This attribute defines the accepted resource types.
454+
455+
456+
Polymorphic relations can also be handled with `relations.PolymorphicResourceRelatedField` like this:
457+
458+
```python
459+
class CompanySerializer(serializers.ModelSerializer):
460+
current_project = relations.PolymorphicResourceRelatedField(
461+
ProjectSerializer, queryset=models.Project.objects.all())
462+
future_projects = relations.PolymorphicResourceRelatedField(
463+
ProjectSerializer, queryset=models.Project.objects.all(), many=True)
464+
465+
class Meta:
466+
model = models.Company
467+
```
468+
469+
They must be explicitly declared with the `polymorphic_serializer` (first positional argument) correctly defined.
470+
It must be a subclass of `serializers.PolymorphicModelSerializer`.
471+
472+
<div class="warning">
473+
<strong>Note:</strong>
474+
Polymorphic resources are not compatible with
475+
<code class="docutils literal">
476+
<span class="pre">resource_name</span>
477+
</code>
478+
defined on the view.
479+
</div>
480+
428481
### Meta
429482

430483
You may add metadata to the rendered json in two different ways: `meta_fields` and `get_root_meta`.

example/factories/__init__.py

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

33
import factory
44
from faker import Factory as FakerFactory
5-
from example.models import Blog, Author, AuthorBio, Entry, Comment, TaggedItem
5+
from example.models import (
6+
Blog, Author, AuthorBio, Entry, Comment, TaggedItem, ArtProject, ResearchProject, Company
7+
)
68

79
faker = FakerFactory.create()
810
faker.seed(983843)
@@ -68,3 +70,35 @@ class Meta:
6870

6971
content_object = factory.SubFactory(EntryFactory)
7072
tag = factory.LazyAttribute(lambda x: faker.word())
73+
74+
75+
class ArtProjectFactory(factory.django.DjangoModelFactory):
76+
class Meta:
77+
model = ArtProject
78+
79+
topic = factory.LazyAttribute(lambda x: faker.catch_phrase())
80+
artist = factory.LazyAttribute(lambda x: faker.name())
81+
82+
83+
class ResearchProjectFactory(factory.django.DjangoModelFactory):
84+
class Meta:
85+
model = ResearchProject
86+
87+
topic = factory.LazyAttribute(lambda x: faker.catch_phrase())
88+
supervisor = factory.LazyAttribute(lambda x: faker.name())
89+
90+
91+
class CompanyFactory(factory.django.DjangoModelFactory):
92+
class Meta:
93+
model = Company
94+
95+
name = factory.LazyAttribute(lambda x: faker.company())
96+
current_project = factory.SubFactory(ArtProjectFactory)
97+
98+
@factory.post_generation
99+
def future_projects(self, create, extracted, **kwargs):
100+
if not create:
101+
return
102+
if extracted:
103+
for project in extracted:
104+
self.future_projects.add(project)
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.1 on 2017-05-17 14:49
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', '0002_taggeditem'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='Company',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('name', models.CharField(max_length=100)),
22+
],
23+
),
24+
migrations.CreateModel(
25+
name='Project',
26+
fields=[
27+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
28+
('topic', models.CharField(max_length=30)),
29+
],
30+
options={
31+
'abstract': False,
32+
},
33+
),
34+
migrations.AlterField(
35+
model_name='comment',
36+
name='entry',
37+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='example.Entry'),
38+
),
39+
migrations.CreateModel(
40+
name='ArtProject',
41+
fields=[
42+
('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='example.Project')),
43+
('artist', models.CharField(max_length=30)),
44+
],
45+
options={
46+
'abstract': False,
47+
},
48+
bases=('example.project',),
49+
),
50+
migrations.CreateModel(
51+
name='ResearchProject',
52+
fields=[
53+
('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='example.Project')),
54+
('supervisor', models.CharField(max_length=30)),
55+
],
56+
options={
57+
'abstract': False,
58+
},
59+
bases=('example.project',),
60+
),
61+
migrations.AddField(
62+
model_name='project',
63+
name='polymorphic_ctype',
64+
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_example.project_set+', to='contenttypes.ContentType'),
65+
),
66+
migrations.AddField(
67+
model_name='company',
68+
name='current_project',
69+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='companies', to='example.Project'),
70+
),
71+
migrations.AddField(
72+
model_name='company',
73+
name='future_projects',
74+
field=models.ManyToManyField(to='example.Project'),
75+
),
76+
]

example/models.py

+23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.contrib.contenttypes.fields import GenericRelation
77
from django.db import models
88
from django.utils.encoding import python_2_unicode_compatible
9+
from polymorphic.models import PolymorphicModel
910

1011

1112
class BaseModel(models.Model):
@@ -86,3 +87,25 @@ class Comment(BaseModel):
8687

8788
def __str__(self):
8889
return self.body
90+
91+
92+
class Project(PolymorphicModel):
93+
topic = models.CharField(max_length=30)
94+
95+
96+
class ArtProject(Project):
97+
artist = models.CharField(max_length=30)
98+
99+
100+
class ResearchProject(Project):
101+
supervisor = models.CharField(max_length=30)
102+
103+
104+
@python_2_unicode_compatible
105+
class Company(models.Model):
106+
name = models.CharField(max_length=100)
107+
current_project = models.ForeignKey(Project, related_name='companies')
108+
future_projects = models.ManyToManyField(Project)
109+
110+
def __str__(self):
111+
return self.name

example/serializers.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
from datetime import datetime
2+
3+
import rest_framework
24
from rest_framework_json_api import serializers, relations
3-
from example.models import Blog, Entry, Author, AuthorBio, Comment, TaggedItem
5+
from packaging import version
6+
from example.models import (
7+
Blog, Entry, Author, AuthorBio, Comment, TaggedItem, Project, ArtProject, ResearchProject,
8+
Company,
9+
)
410

511

612
class TaggedItemSerializer(serializers.ModelSerializer):
@@ -115,3 +121,40 @@ class Meta:
115121
model = Comment
116122
exclude = ('created_at', 'modified_at',)
117123
# fields = ('entry', 'body', 'author',)
124+
125+
126+
class ArtProjectSerializer(serializers.ModelSerializer):
127+
class Meta:
128+
model = ArtProject
129+
exclude = ('polymorphic_ctype',)
130+
131+
132+
class ResearchProjectSerializer(serializers.ModelSerializer):
133+
class Meta:
134+
model = ResearchProject
135+
exclude = ('polymorphic_ctype',)
136+
137+
138+
class ProjectSerializer(serializers.PolymorphicModelSerializer):
139+
polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer]
140+
141+
class Meta:
142+
model = Project
143+
exclude = ('polymorphic_ctype',)
144+
145+
146+
class CompanySerializer(serializers.ModelSerializer):
147+
current_project = relations.PolymorphicResourceRelatedField(
148+
ProjectSerializer, queryset=Project.objects.all())
149+
future_projects = relations.PolymorphicResourceRelatedField(
150+
ProjectSerializer, queryset=Project.objects.all(), many=True)
151+
152+
included_serializers = {
153+
'current_project': ProjectSerializer,
154+
'future_projects': ProjectSerializer,
155+
}
156+
157+
class Meta:
158+
model = Company
159+
if version.parse(rest_framework.VERSION) >= version.parse('3.3'):
160+
fields = '__all__'

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

+15-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from example.factories import (
55
BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory,
6-
TaggedItemFactory
6+
TaggedItemFactory, ArtProjectFactory, ResearchProjectFactory, CompanyFactory,
77
)
88

99
register(BlogFactory)
@@ -12,6 +12,9 @@
1212
register(EntryFactory)
1313
register(CommentFactory)
1414
register(TaggedItemFactory)
15+
register(ArtProjectFactory)
16+
register(ResearchProjectFactory)
17+
register(CompanyFactory)
1518

1619

1720
@pytest.fixture
@@ -33,3 +36,14 @@ def multiple_entries(blog_factory, author_factory, entry_factory, comment_factor
3336
comment_factory(entry=entries[0])
3437
comment_factory(entry=entries[1])
3538
return entries
39+
40+
41+
@pytest.fixture
42+
def single_company(art_project_factory, research_project_factory, company_factory):
43+
company = company_factory(future_projects=(research_project_factory(), art_project_factory()))
44+
return company
45+
46+
47+
@pytest.fixture
48+
def single_art_project(art_project_factory):
49+
return art_project_factory()

0 commit comments

Comments
 (0)