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 10 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

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 number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pip-delete-this-directory.txt

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

# VirtualEnv
.venv/
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ env:

- DJANGO=">=1.10,<1.11" DRF=">=3.4,<3.5"
before_install:
# Force an upgrade of py to avoid VersionConflict
# Force an upgrade of py & pytest to avoid VersionConflict
- pip install --upgrade py
- pip install "pytest>=2.8,<3"
- pip install codecov
install:
- pip install Django${DJANGO} djangorestframework${DRF}
Expand Down
57 changes: 57 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,63 @@ 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.

#### Extraction of the polymorphic type

This package can defer the resolution of the type of polymorphic models instances to retrieve the appropriate type.
However, most models are not polymorphic and for performance reasons this is only done if the underlying model is a subclass of a polymorphic model.

Polymorphic ancestors must be defined on settings like this:

```python
JSON_API_POLYMORPHIC_ANCESTORS = (
'polymorphic.models.PolymorphicModel',
)
```

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we could put a new paragraph here that includes comments about polymorphic libraries (and remove the "as backed by" parenthetical in the previous paragraph). How does the following sound?

DJA tests its polymorphic support against django-polymorphic. 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 setup if associated with a polymorphic serializer.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"setup" (a noun) should be "set up" (a verb phrase) here. As a native English speaker, I apologize for the stupidity and craziness of the English language. 😄

A polymorphic serializer take care of (de)serializing the correct instances types and can be defined like this:
Copy link
Collaborator

Choose a reason for hiding this comment

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

"take" should be "takes"


```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 explicitely declared with the `polymorphic_serializer` (first positional argument) correctly defined.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"explicitely" should be "explicitly"

It must be a subclass of `serializers.PolymorphicModelSerializer`.

<div class="warning">
Copy link
Collaborator

Choose a reason for hiding this comment

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

sigh... the downside of using Markdown for Sphinx, no directive support. Nothing to do here, just lamenting. 😢

<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
45 changes: 39 additions & 6 deletions example/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@

import factory
from faker import Factory as FakerFactory
from example.models import Blog, Author, AuthorBio, Entry, Comment
from example import models


faker = FakerFactory.create()
faker.seed(983843)


class BlogFactory(factory.django.DjangoModelFactory):
class Meta:
model = Blog
model = models.Blog

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


class AuthorFactory(factory.django.DjangoModelFactory):
class Meta:
model = Author
model = models.Author

name = factory.LazyAttribute(lambda x: faker.name())
email = factory.LazyAttribute(lambda x: faker.email())
Expand All @@ -25,15 +27,15 @@ class Meta:

class AuthorBioFactory(factory.django.DjangoModelFactory):
class Meta:
model = AuthorBio
model = models.AuthorBio

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


class EntryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Entry
model = models.Entry

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

class CommentFactory(factory.django.DjangoModelFactory):
class Meta:
model = Comment
model = models.Comment

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


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

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


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

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


class CompanyFactory(factory.django.DjangoModelFactory):
class Meta:
model = models.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)
80 changes: 80 additions & 0 deletions example/migrations/0002_auto_20160513_0857.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.6 on 2016-05-13 08:57
from __future__ import unicode_literals
from distutils.version import LooseVersion

from django.db import migrations, models
import django.db.models.deletion
import django


class Migration(migrations.Migration):

# TODO: Must be removed as soon as Django 1.7 support is dropped
if django.get_version() < LooseVersion('1.8'):
dependencies = [
('contenttypes', '0001_initial'),
('example', '0001_initial'),
]
else:
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('example', '0001_initial'),
]

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.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'),
),
]
22 changes: 22 additions & 0 deletions example/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from polymorphic.models import PolymorphicModel


class BaseModel(models.Model):
Expand Down Expand Up @@ -72,3 +73,24 @@ 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
Loading