Skip to content

Support polymorphic models#211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Jun 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dd4d4ec
Support polymorphic models (from django-polymorphic and django-typed-…
leo-naeka Feb 21, 2016
8c73d95
Polymorphic ancestors must now be defined in Django's settings
leo-naeka Mar 14, 2016
681b5aa
Adds the following features:
ograycode May 13, 2016
96cbab0
Fix example migration and tests
leo-naeka May 15, 2016
5c63425
Polymorphic serializers refactor
leo-naeka May 16, 2016
fddb06b
Basic support of write operations on polymorphic relations
leo-naeka May 16, 2016
22829d1
Improve polymorphism documentation
leo-naeka May 17, 2016
d565334
Improve polymorphic relations and tests.
leo-naeka May 17, 2016
e840438
Add django-polymorphic as test dependency
leo-naeka Sep 8, 2016
19b0238
Avoid type list comparison in polymorphic tests
leo-naeka Sep 8, 2016
0ddf5ca
Merge remote-tracking branch 'origin/develop' into polymorphism
AstraLuma May 17, 2017
b8bf612
Flake8
AstraLuma May 17, 2017
8fd4617
Flake8
AstraLuma May 17, 2017
275793c
Better handle imports?
AstraLuma May 17, 2017
a26df13
Resolve circular reference
AstraLuma May 17, 2017
2278976
Really break up import loop
AstraLuma May 17, 2017
8563b65
Missed something in the merge
AstraLuma May 17, 2017
4aaeac2
Redo migrations
AstraLuma May 17, 2017
030f6c8
Wrong indentation
AstraLuma May 17, 2017
ca23885
Fix a deprecation
AstraLuma May 24, 2017
ae759e5
Fix polymorphic type resolution in relations
leo-naeka May 25, 2017
37c5ae6
Fix tests among different environments
leo-naeka May 25, 2017
f36821b
Update tox.ini environment list
leo-naeka May 25, 2017
4eec4aa
Add packaging module as requirement for old python versions
leo-naeka May 25, 2017
bc12e0f
Remove the POLYMORPHIC_ANCESTOR code
leo-naeka May 26, 2017
6b4f45b
Fix some typos and little errors
leo-naeka May 29, 2017
36f3b6a
Administrivia
AstraLuma May 30, 2017
05cdb51
Restore generic relation support
AstraLuma May 30, 2017
c1afe35
Add Leo to authors
AstraLuma May 30, 2017
8ff5465
PEP8
AstraLuma May 30, 2017
35c90d4
Merge branch 'develop' into polymorphism
AstraLuma May 30, 2017
c5599c0
Really bad writing.
AstraLuma May 31, 2017
8d94efb
Merge branch 'polymorphism' of github.com:leo-naeka/django-rest-frame…
AstraLuma May 31, 2017
89ad607
Editing
AstraLuma Jun 1, 2017
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,8 @@ pip-delete-this-directory.txt

# Tox
.tox/
.cache/
.python-version

# VirtualEnv
.venv/
Expand Down
2 changes: 2 additions & 0 deletions AUTHORS
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
Adam Wróbel <https://adamwrobel.com>
Christian Zosel <https://zosel.ch>
Greg Aker <greg@gregaker.net>
Jamie Bliss <astronouth7303@gmail.com>
Jerel Unruh <mail@unruhdesigns.com>
Léo S. <leo@naeka.fr>
Matt Layman <http://www.mattlayman.com>
Oliver Sauder <os@esite.ch>
Yaniv Peer <yanivpeer@gmail.com>
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
v2.3.0

* Added support for polymorphic models
* When `JSON_API_FORMAT_KEYS` is False (the default) do not translate request
attributes and relations to snake\_case format. This conversion was unexpected
and there was no way to turn it off.
Expand Down
53 changes: 53 additions & 0 deletions docs/usage.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -425,6 +425,59 @@ field_name_mapping = {
```


### Working with polymorphic resources

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This jumps straight into how to use polymorphic resources. While that is appropriate for a Usage page, if this is the only place in the documentation that describes polymorphic resources, I think an introductory paragraph providing context and why a user would need this would be beneficial. Also, are there going to be related packages that are needed for this work? If so, can we link to them? I'm thinking about django-polymorphic here.

Polymorphic resources allow you to use specialized subclasses without requiring
special endpoints to expose the specialized versions. For example, if you had a
`Project` that could be either an `ArtProject` or a `ResearchProject`, you can
have both kinds at the same URL.

DJA tests its polymorphic support against [django-polymorphic](https://django-polymorphic.readthedocs.io/en/stable/).
The polymorphic feature should also work with other popular libraries like
django-polymodels or django-typed-models.

#### Writing polymorphic resources

A polymorphic endpoint can be set up if associated with a polymorphic serializer.
A polymorphic serializer takes care of (de)serializing the correct instances types and can be defined like this:

```python
class ProjectSerializer(serializers.PolymorphicModelSerializer):
polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer]

class Meta:
model = models.Project
```

It must inherit from `serializers.PolymorphicModelSerializer` and define the `polymorphic_serializers` list.
This attribute defines the accepted resource types.


Polymorphic relations can also be handled with `relations.PolymorphicResourceRelatedField` like this:

```python
class CompanySerializer(serializers.ModelSerializer):
current_project = relations.PolymorphicResourceRelatedField(
ProjectSerializer, queryset=models.Project.objects.all())
future_projects = relations.PolymorphicResourceRelatedField(
ProjectSerializer, queryset=models.Project.objects.all(), many=True)

class Meta:
model = models.Company
```

They must be explicitly declared with the `polymorphic_serializer` (first positional argument) correctly defined.
It must be a subclass of `serializers.PolymorphicModelSerializer`.

<div class="warning">
<strong>Note:</strong>
Polymorphic resources are not compatible with
<code class="docutils literal">
<span class="pre">resource_name</span>
</code>
defined on the view.
</div>

### Meta

You may add metadata to the rendered json in two different ways: `meta_fields` and `get_root_meta`.
Expand Down
36 changes: 35 additions & 1 deletion example/factories/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,9 @@

import factory
from faker import Factory as FakerFactory
from example.models import Blog, Author, AuthorBio, Entry, Comment, TaggedItem
from example.models import (
Blog, Author, AuthorBio, Entry, Comment, TaggedItem, ArtProject, ResearchProject, Company
)

faker = FakerFactory.create()
faker.seed(983843)
Expand DownExpand Up@@ -68,3 +70,35 @@ class Meta:

content_object = factory.SubFactory(EntryFactory)
tag = factory.LazyAttribute(lambda x: faker.word())


class ArtProjectFactory(factory.django.DjangoModelFactory):
class Meta:
model = ArtProject

topic = factory.LazyAttribute(lambda x: faker.catch_phrase())
artist = factory.LazyAttribute(lambda x: faker.name())


class ResearchProjectFactory(factory.django.DjangoModelFactory):
class Meta:
model = ResearchProject

topic = factory.LazyAttribute(lambda x: faker.catch_phrase())
supervisor = factory.LazyAttribute(lambda x: faker.name())


class CompanyFactory(factory.django.DjangoModelFactory):
class Meta:
model = Company

name = factory.LazyAttribute(lambda x: faker.company())
current_project = factory.SubFactory(ArtProjectFactory)

@factory.post_generation
def future_projects(self, create, extracted, **kwargs):
if not create:
return
if extracted:
for project in extracted:
self.future_projects.add(project)
76 changes: 76 additions & 0 deletions example/migrations/0003_polymorphics.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-17 14:49
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', '0002_taggeditem'),
]

operations = [
migrations.CreateModel(
name='Company',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('topic', models.CharField(max_length=30)),
],
options={
'abstract': False,
},
),
migrations.AlterField(
model_name='comment',
name='entry',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='example.Entry'),
),
migrations.CreateModel(
name='ArtProject',
fields=[
('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')),
('artist', models.CharField(max_length=30)),
],
options={
'abstract': False,
},
bases=('example.project',),
),
migrations.CreateModel(
name='ResearchProject',
fields=[
('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')),
('supervisor', models.CharField(max_length=30)),
],
options={
'abstract': False,
},
bases=('example.project',),
),
migrations.AddField(
model_name='project',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_example.project_set+', to='contenttypes.ContentType'),
),
migrations.AddField(
model_name='company',
name='current_project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='companies', to='example.Project'),
),
migrations.AddField(
model_name='company',
name='future_projects',
field=models.ManyToManyField(to='example.Project'),
),
]
23 changes: 23 additions & 0 deletions example/models.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from polymorphic.models import PolymorphicModel


class BaseModel(models.Model):
Expand DownExpand Up@@ -86,3 +87,25 @@ class Comment(BaseModel):

def __str__(self):
return self.body


class Project(PolymorphicModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of the Project example. I see the same thing over in the django-polymorphic docs. That will be a nice help to those who are coming from viewing that documentation.

topic = models.CharField(max_length=30)


class ArtProject(Project):
artist = models.CharField(max_length=30)


class ResearchProject(Project):
supervisor = models.CharField(max_length=30)


@python_2_unicode_compatible
class Company(models.Model):
name = models.CharField(max_length=100)
current_project = models.ForeignKey(Project, related_name='companies')
future_projects = models.ManyToManyField(Project)

def __str__(self):
return self.name
45 changes: 44 additions & 1 deletion example/serializers.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
from datetime import datetime

import rest_framework
from rest_framework_json_api import serializers, relations
from example.models import Blog, Entry, Author, AuthorBio, Comment, TaggedItem
from packaging import version
from example.models import (
Blog, Entry, Author, AuthorBio, Comment, TaggedItem, Project, ArtProject, ResearchProject,
Company,
)


class TaggedItemSerializer(serializers.ModelSerializer):
Expand DownExpand Up@@ -115,3 +121,40 @@ class Meta:
model = Comment
exclude = ('created_at', 'modified_at',)
# fields = ('entry', 'body', 'author',)


class ArtProjectSerializer(serializers.ModelSerializer):
class Meta:
model = ArtProject
exclude = ('polymorphic_ctype',)


class ResearchProjectSerializer(serializers.ModelSerializer):
class Meta:
model = ResearchProject
exclude = ('polymorphic_ctype',)


class ProjectSerializer(serializers.PolymorphicModelSerializer):
polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer]

class Meta:
model = Project
exclude = ('polymorphic_ctype',)


class CompanySerializer(serializers.ModelSerializer):
current_project = relations.PolymorphicResourceRelatedField(
ProjectSerializer, queryset=Project.objects.all())
future_projects = relations.PolymorphicResourceRelatedField(
ProjectSerializer, queryset=Project.objects.all(), many=True)

included_serializers = {
'current_project': ProjectSerializer,
'future_projects': ProjectSerializer,
}

class Meta:
model = Company
if version.parse(rest_framework.VERSION) >= version.parse('3.3'):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... I had no idea it was possible to use an if statement in a class definition. I learned something new about Python today. Thanks!

fields = '__all__'
1 change: 1 addition & 0 deletions example/settings/dev.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
'django.contrib.auth',
'django.contrib.admin',
'rest_framework',
'polymorphic',
'example',
]

Expand Down
16 changes: 15 additions & 1 deletion example/tests/conftest.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@

from example.factories import (
BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory,
TaggedItemFactory
TaggedItemFactory, ArtProjectFactory, ResearchProjectFactory, CompanyFactory,
)

register(BlogFactory)
Expand All@@ -12,6 +12,9 @@
register(EntryFactory)
register(CommentFactory)
register(TaggedItemFactory)
register(ArtProjectFactory)
register(ResearchProjectFactory)
register(CompanyFactory)


@pytest.fixture
Expand All@@ -33,3 +36,14 @@ def multiple_entries(blog_factory, author_factory, entry_factory, comment_factor
comment_factory(entry=entries[0])
comment_factory(entry=entries[1])
return entries


@pytest.fixture
def single_company(art_project_factory, research_project_factory, company_factory):
company = company_factory(future_projects=(research_project_factory(), art_project_factory()))
return company


@pytest.fixture
def single_art_project(art_project_factory):
return art_project_factory()
Loading
close