From dd4d4ec3a6426857a4c191f8f59c2e7d2df3acd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Sun, 21 Feb 2016 19:16:04 +0100 Subject: [PATCH 01/31] Support polymorphic models (from django-polymorphic and django-typed-models) --- example/factories/__init__.py | 45 +++++++++++++-- example/models.py | 22 +++++++ example/serializers.py | 57 ++++++++++++++++--- example/settings/dev.py | 1 + example/tests/conftest.py | 25 ++++++-- .../tests/integration/test_polymorphism.py | 31 ++++++++++ example/urls.py | 5 +- example/urls_test.py | 8 ++- example/views.py | 15 ++++- requirements-development.txt | 1 + rest_framework_json_api/relations.py | 10 +++- rest_framework_json_api/renderers.py | 3 + rest_framework_json_api/utils.py | 12 ++++ 13 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 example/tests/integration/test_polymorphism.py diff --git a/example/factories/__init__.py b/example/factories/__init__.py index 0119f925..db74cde3 100644 --- a/example/factories/__init__.py +++ b/example/factories/__init__.py @@ -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()) @@ -25,7 +27,7 @@ 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()) @@ -33,7 +35,7 @@ class Meta: 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()) @@ -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) diff --git a/example/models.py b/example/models.py index 7895722a..6bbaaf1b 100644 --- a/example/models.py +++ b/example/models.py @@ -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): @@ -72,3 +73,24 @@ class Comment(BaseModel): def __str__(self): return self.body + +class Project(PolymorphicModel): + 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 diff --git a/example/serializers.py b/example/serializers.py index 7929577b..42ebb812 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,6 +1,6 @@ from datetime import datetime from rest_framework_json_api import serializers, relations -from example.models import Blog, Entry, Author, AuthorBio, Comment +from example import models class BlogSerializer(serializers.ModelSerializer): @@ -12,11 +12,11 @@ def get_copyright(self, resource): def get_root_meta(self, resource, many): return { - 'api_docs': '/docs/api/blogs' + 'api_docs': '/docs/api/blogs' } class Meta: - model = Blog + model = models.Blog fields = ('name', 'url',) meta_fields = ('copyright',) @@ -50,16 +50,16 @@ def __init__(self, *args, **kwargs): source='get_featured', model=Entry, read_only=True) def get_suggested(self, obj): - return Entry.objects.exclude(pk=obj.pk) + return models.Entry.objects.exclude(pk=obj.pk).first() def get_featured(self, obj): - return Entry.objects.exclude(pk=obj.pk).first() + return models.Entry.objects.exclude(pk=obj.pk).first() def get_body_format(self, obj): return 'text' class Meta: - model = Entry + model = models.Entry fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date', 'authors', 'comments', 'featured', 'suggested',) meta_fields = ('body_format',) @@ -68,7 +68,7 @@ class Meta: class AuthorBioSerializer(serializers.ModelSerializer): class Meta: - model = AuthorBio + model = models.AuthorBio fields = ('author', 'body',) @@ -78,7 +78,7 @@ class AuthorSerializer(serializers.ModelSerializer): } class Meta: - model = Author + model = models.Author fields = ('name', 'email', 'bio') @@ -89,6 +89,45 @@ class CommentSerializer(serializers.ModelSerializer): } class Meta: - model = Comment + model = models.Comment exclude = ('created_at', 'modified_at',) # fields = ('entry', 'body', 'author',) + + +class ArtProjectSerializer(serializers.ModelSerializer): + class Meta: + model = models.ArtProject + exclude = ('polymorphic_ctype',) + + +class ResearchProjectSerializer(serializers.ModelSerializer): + class Meta: + model = models.ResearchProject + exclude = ('polymorphic_ctype',) + + +class ProjectSerializer(serializers.ModelSerializer): + + class Meta: + model = models.Project + exclude = ('polymorphic_ctype',) + + def to_representation(self, instance): + # Handle polymorphism + if isinstance(instance, models.ArtProject): + return ArtProjectSerializer( + instance, context=self.context).to_representation(instance) + elif isinstance(instance, models.ResearchProject): + return ResearchProjectSerializer( + instance, context=self.context).to_representation(instance) + return super(ProjectSerializer, self).to_representation(instance) + + +class CompanySerializer(serializers.ModelSerializer): + included_serializers = { + 'current_project': ProjectSerializer, + 'future_projects': ProjectSerializer, + } + + class Meta: + model = models.Company diff --git a/example/settings/dev.py b/example/settings/dev.py index 3cc1d6e1..c5a1f742 100644 --- a/example/settings/dev.py +++ b/example/settings/dev.py @@ -23,6 +23,7 @@ 'django.contrib.auth', 'django.contrib.admin', 'rest_framework', + 'polymorphic', 'example', ] diff --git a/example/tests/conftest.py b/example/tests/conftest.py index 8a96cfdb..cb059f81 100644 --- a/example/tests/conftest.py +++ b/example/tests/conftest.py @@ -1,13 +1,16 @@ import pytest from pytest_factoryboy import register -from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory +from example import factories -register(BlogFactory) -register(AuthorFactory) -register(AuthorBioFactory) -register(EntryFactory) -register(CommentFactory) +register(factories.BlogFactory) +register(factories.AuthorFactory) +register(factories.AuthorBioFactory) +register(factories.EntryFactory) +register(factories.CommentFactory) +register(factories.ArtProjectFactory) +register(factories.ResearchProjectFactory) +register(factories.CompanyFactory) @pytest.fixture @@ -29,3 +32,13 @@ def multiple_entries(blog_factory, author_factory, entry_factory, comment_factor 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() diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py new file mode 100644 index 00000000..28e281ef --- /dev/null +++ b/example/tests/integration/test_polymorphism.py @@ -0,0 +1,31 @@ +import pytest +from django.core.urlresolvers import reverse + +from example.tests.utils import load_json + +pytestmark = pytest.mark.django_db + + +def test_polymorphism_on_detail(single_art_project, client): + response = client.get(reverse("project-detail", kwargs={'pk': single_art_project.pk})) + content = load_json(response.content) + assert content["data"]["type"] == "artProjects" + + +def test_polymorphism_on_detail_relations(single_company, client): + response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk})) + content = load_json(response.content) + assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" + assert [rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]] == [ + "researchProjects", "artProjects"] + + +def test_polymorphism_on_included_relations(single_company, client): + response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}) + + '?include=current_project,future_projects') + content = load_json(response.content) + assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" + assert [rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]] == [ + "researchProjects", "artProjects"] + assert [x.get('type') for x in content.get('included')] == ['artProjects', 'artProjects', 'researchProjects'], \ + 'Detail included types are incorrect' diff --git a/example/urls.py b/example/urls.py index f48135c7..4443960f 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,7 +1,8 @@ from django.conf.urls import include, url from rest_framework import routers -from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet +from example.views import ( + BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet, CompanyViewset, ProjectViewset) router = routers.DefaultRouter(trailing_slash=False) @@ -9,6 +10,8 @@ router.register(r'entries', EntryViewSet) router.register(r'authors', AuthorViewSet) router.register(r'comments', CommentViewSet) +router.register(r'companies', CompanyViewset) +router.register(r'projects', ProjectViewset) urlpatterns = [ url(r'^', include(router.urls)), diff --git a/example/urls_test.py b/example/urls_test.py index 0f8ed73b..21f29fd1 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -1,8 +1,9 @@ from django.conf.urls import include, url from rest_framework import routers -from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet, EntryRelationshipView, BlogRelationshipView, \ - CommentRelationshipView, AuthorRelationshipView +from example.views import ( + BlogViewSet, EntryViewSet, AuthorViewSet, CommentViewSet, CompanyViewset, ProjectViewset, + EntryRelationshipView, BlogRelationshipView, CommentRelationshipView, AuthorRelationshipView) from .api.resources.identity import Identity, GenericIdentity router = routers.DefaultRouter(trailing_slash=False) @@ -11,6 +12,8 @@ router.register(r'entries', EntryViewSet) router.register(r'authors', AuthorViewSet) router.register(r'comments', CommentViewSet) +router.register(r'companies', CompanyViewset) +router.register(r'projects', ProjectViewset) # for the old tests router.register(r'identities', Identity) @@ -36,4 +39,3 @@ AuthorRelationshipView.as_view(), name='author-relationships'), ] - diff --git a/example/views.py b/example/views.py index 988cda66..e32db8c0 100644 --- a/example/views.py +++ b/example/views.py @@ -6,9 +6,10 @@ import rest_framework_json_api.parsers import rest_framework_json_api.renderers from rest_framework_json_api.views import RelationshipView -from example.models import Blog, Entry, Author, Comment +from example.models import Blog, Entry, Author, Comment, Company, Project from example.serializers import ( - BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer) + BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer, CompanySerializer, + ProjectSerializer) from rest_framework_json_api.utils import format_drf_errors @@ -72,6 +73,16 @@ class CommentViewSet(viewsets.ModelViewSet): serializer_class = CommentSerializer +class CompanyViewset(viewsets.ModelViewSet): + queryset = Company.objects.all() + serializer_class = CompanySerializer + + +class ProjectViewset(viewsets.ModelViewSet): + queryset = Project.objects.all() + serializer_class = ProjectSerializer + + class EntryRelationshipView(RelationshipView): queryset = Entry.objects.all() diff --git a/requirements-development.txt b/requirements-development.txt index 78ccdc91..66381531 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -3,5 +3,6 @@ pytest==2.8.2 pytest-django pytest-factoryboy fake-factory +django-polymorphic tox mock diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 9762bc74..fa7c025b 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -6,7 +6,7 @@ from django.db.models.query import QuerySet from rest_framework_json_api.exceptions import Conflict -from rest_framework_json_api.utils import Hyperlink, \ +from rest_framework_json_api.utils import POLYMORPHIC_ANCESTORS, Hyperlink, \ get_resource_type_from_queryset, get_resource_type_from_instance, \ get_included_serializers, get_resource_type_from_serializer @@ -47,6 +47,12 @@ def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwar super(ResourceRelatedField, self).__init__(**kwargs) + # Determine if relation is polymorphic + self.is_polymorphic = False + model = model or getattr(self.get_queryset(), 'model', None) + if model and issubclass(model, POLYMORPHIC_ANCESTORS): + self.is_polymorphic = True + def use_pk_only_optimization(self): # We need the real object to determine its type... return False @@ -144,7 +150,7 @@ def to_representation(self, value): resource_type = None root = getattr(self.parent, 'parent', self.parent) field_name = self.field_name if self.field_name else self.parent.field_name - if getattr(root, 'included_serializers', None) is not None: + if getattr(root, 'included_serializers', None) is not None and not self.is_polymorphic: includes = get_included_serializers(root) if field_name in includes.keys(): resource_type = get_resource_type_from_serializer(includes[field_name]) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index e426af11..b5809e51 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -382,6 +382,9 @@ def extract_root_meta(serializer, resource): @staticmethod def build_json_resource_obj(fields, resource, resource_instance, resource_name): + # Determine type from the instance if the underlying model is polymorphic + if isinstance(resource_instance, utils.POLYMORPHIC_ANCESTORS): + resource_name = utils.get_resource_type_from_instance(resource_instance) resource_data = [ ('type', resource_name), ('id', encoding.force_text(resource_instance.pk) if resource_instance else None), diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 2adbf519..87940627 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -26,6 +26,18 @@ except ImportError: HyperlinkedRouterField = type(None) +POLYMORPHIC_ANCESTORS = () +try: + from polymorphic.models import PolymorphicModel + POLYMORPHIC_ANCESTORS += (PolymorphicModel,) +except ImportError: + pass +try: + from typedmodels.models import TypedModel + POLYMORPHIC_ANCESTORS += (TypedModel,) +except ImportError: + pass + def get_resource_name(context): """ From 8c73d959fc1407c10801f9f8badd37b3ecbb9081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Mon, 14 Mar 2016 11:44:40 +0100 Subject: [PATCH 02/31] Polymorphic ancestors must now be defined in Django's settings Update documentation --- docs/usage.md | 13 +++++++++++++ example/settings/test.py | 3 +++ rest_framework_json_api/utils.py | 13 +++---------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index c1c966b7..adc73a13 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -423,6 +423,19 @@ field_name_mapping = { ``` +### Working with polymorphic resources + +This package can defer the resolution of the type of polymorphic models instances to get 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', +) +``` + ### Meta You may add metadata to the rendered json in two different ways: `meta_fields` and `get_root_meta`. diff --git a/example/settings/test.py b/example/settings/test.py index 5bb3f45d..d0157138 100644 --- a/example/settings/test.py +++ b/example/settings/test.py @@ -15,3 +15,6 @@ REST_FRAMEWORK.update({ 'PAGE_SIZE': 1, }) +JSON_API_POLYMORPHIC_ANCESTORS = ( + 'polymorphic.models.PolymorphicModel', +) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 87940627..11f92c33 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -27,16 +27,9 @@ HyperlinkedRouterField = type(None) POLYMORPHIC_ANCESTORS = () -try: - from polymorphic.models import PolymorphicModel - POLYMORPHIC_ANCESTORS += (PolymorphicModel,) -except ImportError: - pass -try: - from typedmodels.models import TypedModel - POLYMORPHIC_ANCESTORS += (TypedModel,) -except ImportError: - pass +for ancestor in getattr(settings, 'JSON_API_POLYMORPHIC_ANCESTORS', ()): + ancestor_class = import_class_from_dotted_path(ancestor) + POLYMORPHIC_ANCESTORS += (ancestor_class,) def get_resource_name(context): From 681b5aa5dbe09d8d1784ed9c22b3194585803e63 Mon Sep 17 00:00:00 2001 From: gojira <jason@theograys.com> Date: Fri, 13 May 2016 09:34:38 -0400 Subject: [PATCH 03/31] Adds the following features: - support for post and patch request on polymorphic model endpoints. - makes polymorphic serializers give child fields instead of its own. --- example/migrations/0002_auto_20160513_0857.py | 71 +++++++++++++++++++ example/serializers.py | 53 +++++++++++--- .../tests/integration/test_polymorphism.py | 40 +++++++++++ rest_framework_json_api/parsers.py | 9 ++- rest_framework_json_api/renderers.py | 3 +- rest_framework_json_api/utils.py | 2 + 6 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 example/migrations/0002_auto_20160513_0857.py diff --git a/example/migrations/0002_auto_20160513_0857.py b/example/migrations/0002_auto_20160513_0857.py new file mode 100644 index 00000000..4ed9803b --- /dev/null +++ b/example/migrations/0002_auto_20160513_0857.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-05-13 08:57 +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', '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'), + ), + ] diff --git a/example/serializers.py b/example/serializers.py index 42ebb812..4990b214 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,5 +1,7 @@ from datetime import datetime -from rest_framework_json_api import serializers, relations +from django.db.models.query import QuerySet +from rest_framework.utils.serializer_helpers import BindingDict +from rest_framework_json_api import serializers, relations, utils from example import models @@ -44,13 +46,13 @@ def __init__(self, *args, **kwargs): source='comment_set', many=True, read_only=True) # many related from serializer suggested = relations.SerializerMethodResourceRelatedField( - source='get_suggested', model=Entry, many=True, read_only=True) + source='get_suggested', model=models.Entry, many=True, read_only=True) # single related from serializer featured = relations.SerializerMethodResourceRelatedField( - source='get_featured', model=Entry, read_only=True) + source='get_featured', model=models.Entry, read_only=True) def get_suggested(self, obj): - return models.Entry.objects.exclude(pk=obj.pk).first() + return models.Entry.objects.exclude(pk=obj.pk) def get_featured(self, obj): return models.Entry.objects.exclude(pk=obj.pk).first() @@ -108,19 +110,48 @@ class Meta: class ProjectSerializer(serializers.ModelSerializer): + polymorphic_serializers = [ + {'model': models.ArtProject, 'serializer': ArtProjectSerializer}, + {'model': models.ResearchProject, 'serializer': ResearchProjectSerializer}, + ] + class Meta: model = models.Project exclude = ('polymorphic_ctype',) + def _get_actual_serializer_from_instance(self, instance): + for info in self.polymorphic_serializers: + if isinstance(instance, info.get('model')): + actual_serializer = info.get('serializer') + return actual_serializer(instance, context=self.context) + + @property + def fields(self): + _fields = BindingDict(self) + for key, value in self.get_fields().items(): + _fields[key] = value + return _fields + + def get_fields(self): + if self.instance is not None: + if not isinstance(self.instance, QuerySet): + return self._get_actual_serializer_from_instance(self.instance).get_fields() + else: + raise Exception("Cannot get fields from a polymorphic serializer given a queryset") + return super(ProjectSerializer, self).get_fields() + def to_representation(self, instance): # Handle polymorphism - if isinstance(instance, models.ArtProject): - return ArtProjectSerializer( - instance, context=self.context).to_representation(instance) - elif isinstance(instance, models.ResearchProject): - return ResearchProjectSerializer( - instance, context=self.context).to_representation(instance) - return super(ProjectSerializer, self).to_representation(instance) + return self._get_actual_serializer_from_instance(instance).to_representation(instance) + + def to_internal_value(self, data): + data_type = data.get('type') + for info in self.polymorphic_serializers: + actual_serializer = info['serializer'] + if data_type == utils.get_resource_type_from_serializer(actual_serializer): + self.__class__ = actual_serializer + return actual_serializer(data, context=self.context).to_internal_value(data) + raise Exception("Could not deserialize") class CompanySerializer(serializers.ModelSerializer): diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py index 28e281ef..9fd74d13 100644 --- a/example/tests/integration/test_polymorphism.py +++ b/example/tests/integration/test_polymorphism.py @@ -1,4 +1,6 @@ import pytest +import random +import json from django.core.urlresolvers import reverse from example.tests.utils import load_json @@ -29,3 +31,41 @@ def test_polymorphism_on_included_relations(single_company, client): "researchProjects", "artProjects"] assert [x.get('type') for x in content.get('included')] == ['artProjects', 'artProjects', 'researchProjects'], \ 'Detail included types are incorrect' + # Ensure that the child fields are present. + assert content.get('included')[0].get('attributes').get('artist') is not None + assert content.get('included')[1].get('attributes').get('artist') is not None + assert content.get('included')[2].get('attributes').get('supervisor') is not None + +def test_polymorphism_on_polymorphic_model_detail_patch(single_art_project, client): + url = reverse("project-detail", kwargs={'pk': single_art_project.pk}) + response = client.get(url) + content = load_json(response.content) + test_topic = 'test-{}'.format(random.randint(0, 999999)) + test_artist = 'test-{}'.format(random.randint(0, 999999)) + content['data']['attributes']['topic'] = test_topic + content['data']['attributes']['artist'] = test_artist + response = client.patch(url, data=json.dumps(content), content_type='application/vnd.api+json') + new_content = load_json(response.content) + assert new_content["data"]["type"] == "artProjects" + assert new_content['data']['attributes']['topic'] == test_topic + assert new_content['data']['attributes']['artist'] == test_artist + +def test_polymorphism_on_polymorphic_model_list_post(client): + test_topic = 'New test topic {}'.format(random.randint(0, 999999)) + test_artist = 'test-{}'.format(random.randint(0, 999999)) + url = reverse('project-list') + data = { + 'data': { + 'type': 'artProjects', + 'attributes': { + 'topic': test_topic, + 'artist': test_artist + } + } + } + response = client.post(url, data=json.dumps(data), content_type='application/vnd.api+json') + content = load_json(response.content) + assert content['data']['id'] is not None + assert content["data"]["type"] == "artProjects" + assert content['data']['attributes']['topic'] == test_topic + assert content['data']['attributes']['artist'] == test_artist diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index a0c53f05..2e65715a 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -1,6 +1,7 @@ """ Parsers """ +import six from rest_framework import parsers from rest_framework.exceptions import ParseError @@ -80,7 +81,11 @@ def parse(self, stream, media_type=None, parser_context=None): # Check for inconsistencies resource_name = utils.get_resource_name(parser_context) - if data.get('type') != resource_name and request.method in ('PUT', 'POST', 'PATCH'): + if isinstance(resource_name, six.string_types): + doesnt_match = data.get('type') != resource_name + else: + doesnt_match = data.get('type') not in resource_name + if doesnt_match and request.method in ('PUT', 'POST', 'PATCH'): raise exceptions.Conflict( "The resource object's type ({data_type}) is not the type " "that constitute the collection represented by the endpoint ({resource_type}).".format( @@ -92,7 +97,7 @@ def parse(self, stream, media_type=None, parser_context=None): raise ParseError("The resource identifier object must contain an 'id' member") # Construct the return data - parsed_data = {'id': data.get('id')} + parsed_data = {'id': data.get('id'), 'type': data.get('type')} parsed_data.update(self.parse_attributes(data)) parsed_data.update(self.parse_relationships(data)) parsed_data.update(self.parse_metadata(result)) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index b5809e51..f6831949 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -311,8 +311,6 @@ def extract_included(fields, resource, resource_instance, included_resources): relation_type = utils.get_resource_type_from_serializer(serializer) relation_queryset = list(relation_instance) - # Get the serializer fields - serializer_fields = utils.get_serializer_fields(serializer) if serializer_data: for position in range(len(serializer_data)): serializer_resource = serializer_data[position] @@ -321,6 +319,7 @@ def extract_included(fields, resource, resource_instance, included_resources): relation_type or utils.get_resource_type_from_instance(nested_resource_instance) ) + serializer_fields = utils.get_serializer_fields(serializer.__class__(nested_resource_instance, context=serializer.context)) included_data.append( JSONRenderer.build_json_resource_obj( serializer_fields, serializer_resource, nested_resource_instance, resource_type diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 11f92c33..24bb5d54 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -242,6 +242,8 @@ def get_resource_type_from_manager(manager): def get_resource_type_from_serializer(serializer): json_api_meta = getattr(serializer, 'JSONAPIMeta', None) meta = getattr(serializer, 'Meta', None) + if hasattr(serializer, 'polymorphic_serializers'): + return [get_resource_type_from_serializer(s['serializer']) for s in serializer.polymorphic_serializers] if hasattr(json_api_meta, 'resource_name'): return json_api_meta.resource_name elif hasattr(meta, 'resource_name'): From 96cbab0a50f51ce24ef764fbd7e0c760f00cb4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Mon, 16 May 2016 00:26:48 +0200 Subject: [PATCH 04/31] Fix example migration and tests Update gitignore --- .gitignore | 2 ++ example/migrations/0002_auto_20160513_0857.py | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 29fb669d..ceac6f29 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ pip-delete-this-directory.txt # Tox .tox/ +.cache/ +.python-version # VirtualEnv .venv/ diff --git a/example/migrations/0002_auto_20160513_0857.py b/example/migrations/0002_auto_20160513_0857.py index 4ed9803b..2471ea36 100644 --- a/example/migrations/0002_auto_20160513_0857.py +++ b/example/migrations/0002_auto_20160513_0857.py @@ -1,17 +1,26 @@ # -*- 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): - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('example', '0001_initial'), - ] + # 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( From 5c63425d4c83562f30f91d35f47eee629ddd5373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Mon, 16 May 2016 15:44:38 +0200 Subject: [PATCH 05/31] Polymorphic serializers refactor --- example/serializers.py | 58 ++----- .../tests/integration/test_polymorphism.py | 6 +- rest_framework_json_api/serializers.py | 146 +++++++++++++++++- rest_framework_json_api/utils.py | 10 +- 4 files changed, 158 insertions(+), 62 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index 4990b214..9c548e94 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,7 +1,5 @@ from datetime import datetime -from django.db.models.query import QuerySet -from rest_framework.utils.serializer_helpers import BindingDict -from rest_framework_json_api import serializers, relations, utils +from rest_framework_json_api import serializers, relations from example import models @@ -41,15 +39,15 @@ def __init__(self, *args, **kwargs): } body_format = serializers.SerializerMethodField() - # many related from model + # Many related from model comments = relations.ResourceRelatedField( - source='comment_set', many=True, read_only=True) - # many related from serializer + source='comment_set', many=True, read_only=True) + # Many related from serializer suggested = relations.SerializerMethodResourceRelatedField( - source='get_suggested', model=models.Entry, many=True, read_only=True) - # single related from serializer + source='get_suggested', model=models.Entry, many=True, read_only=True) + # Single related from serializer featured = relations.SerializerMethodResourceRelatedField( - source='get_featured', model=models.Entry, read_only=True) + source='get_featured', model=models.Entry, read_only=True) def get_suggested(self, obj): return models.Entry.objects.exclude(pk=obj.pk) @@ -108,51 +106,13 @@ class Meta: exclude = ('polymorphic_ctype',) -class ProjectSerializer(serializers.ModelSerializer): - - polymorphic_serializers = [ - {'model': models.ArtProject, 'serializer': ArtProjectSerializer}, - {'model': models.ResearchProject, 'serializer': ResearchProjectSerializer}, - ] +class ProjectSerializer(serializers.PolymorphicModelSerializer): + polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer] class Meta: model = models.Project exclude = ('polymorphic_ctype',) - def _get_actual_serializer_from_instance(self, instance): - for info in self.polymorphic_serializers: - if isinstance(instance, info.get('model')): - actual_serializer = info.get('serializer') - return actual_serializer(instance, context=self.context) - - @property - def fields(self): - _fields = BindingDict(self) - for key, value in self.get_fields().items(): - _fields[key] = value - return _fields - - def get_fields(self): - if self.instance is not None: - if not isinstance(self.instance, QuerySet): - return self._get_actual_serializer_from_instance(self.instance).get_fields() - else: - raise Exception("Cannot get fields from a polymorphic serializer given a queryset") - return super(ProjectSerializer, self).get_fields() - - def to_representation(self, instance): - # Handle polymorphism - return self._get_actual_serializer_from_instance(instance).to_representation(instance) - - def to_internal_value(self, data): - data_type = data.get('type') - for info in self.polymorphic_serializers: - actual_serializer = info['serializer'] - if data_type == utils.get_resource_type_from_serializer(actual_serializer): - self.__class__ = actual_serializer - return actual_serializer(data, context=self.context).to_internal_value(data) - raise Exception("Could not deserialize") - class CompanySerializer(serializers.ModelSerializer): included_serializers = { diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py index 9fd74d13..514dc31f 100644 --- a/example/tests/integration/test_polymorphism.py +++ b/example/tests/integration/test_polymorphism.py @@ -29,13 +29,14 @@ def test_polymorphism_on_included_relations(single_company, client): assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" assert [rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]] == [ "researchProjects", "artProjects"] - assert [x.get('type') for x in content.get('included')] == ['artProjects', 'artProjects', 'researchProjects'], \ - 'Detail included types are incorrect' + assert [x.get('type') for x in content.get('included')] == [ + 'artProjects', 'artProjects', 'researchProjects'], 'Detail included types are incorrect' # Ensure that the child fields are present. assert content.get('included')[0].get('attributes').get('artist') is not None assert content.get('included')[1].get('attributes').get('artist') is not None assert content.get('included')[2].get('attributes').get('supervisor') is not None + def test_polymorphism_on_polymorphic_model_detail_patch(single_art_project, client): url = reverse("project-detail", kwargs={'pk': single_art_project.pk}) response = client.get(url) @@ -50,6 +51,7 @@ def test_polymorphism_on_polymorphic_model_detail_patch(single_art_project, clie assert new_content['data']['attributes']['topic'] == test_topic assert new_content['data']['attributes']['artist'] == test_artist + def test_polymorphism_on_polymorphic_model_list_post(client): test_topic = 'New test topic {}'.format(random.randint(0, 999999)) test_artist = 'test-{}'.format(random.randint(0, 999999)) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 917ae98c..ccc98124 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -1,9 +1,13 @@ import inflection + +from django.db.models.query import QuerySet from django.utils.translation import ugettext_lazy as _ +from django.utils import six from rest_framework.exceptions import ParseError from rest_framework.serializers import * from rest_framework_json_api.relations import ResourceRelatedField +from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.utils import ( get_resource_type_from_model, get_resource_type_from_instance, get_resource_type_from_serializer, get_included_serializers, get_included_resources) @@ -11,7 +15,8 @@ class ResourceIdentifierObjectSerializer(BaseSerializer): default_error_messages = { - 'incorrect_model_type': _('Incorrect model type. Expected {model_type}, received {received_type}.'), + 'incorrect_model_type': _('Incorrect model type. Expected {model_type}, ' + 'received {received_type}.'), 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'), } @@ -21,7 +26,8 @@ class ResourceIdentifierObjectSerializer(BaseSerializer): def __init__(self, *args, **kwargs): self.model_class = kwargs.pop('model_class', self.model_class) if 'instance' not in kwargs and not self.model_class: - raise RuntimeError('ResourceIdentifierObjectsSerializer must be initialized with a model class.') + raise RuntimeError( + 'ResourceIdentifierObjectsSerializer must be initialized with a model class.') super(ResourceIdentifierObjectSerializer, self).__init__(*args, **kwargs) def to_representation(self, instance): @@ -32,7 +38,8 @@ def to_representation(self, instance): def to_internal_value(self, data): if data['type'] != get_resource_type_from_model(self.model_class): - self.fail('incorrect_model_type', model_type=self.model_class, received_type=data['type']) + self.fail( + 'incorrect_model_type', model_type=self.model_class, received_type=data['type']) pk = data['id'] try: return self.model_class.objects.get(pk=pk) @@ -48,15 +55,18 @@ def __init__(self, *args, **kwargs): request = context.get('request') if context else None if request: - sparse_fieldset_query_param = 'fields[{}]'.format(get_resource_type_from_serializer(self)) + sparse_fieldset_query_param = 'fields[{}]'.format( + get_resource_type_from_serializer(self)) try: - param_name = next(key for key in request.query_params if sparse_fieldset_query_param in key) + param_name = next( + key for key in request.query_params if sparse_fieldset_query_param in key) except StopIteration: pass else: fieldset = request.query_params.get(param_name).split(',') - # iterate over a *copy* of self.fields' underlying OrderedDict, because we may modify the - # original during the iteration. self.fields is a `rest_framework.utils.serializer_helpers.BindingDict` + # Iterate over a *copy* of self.fields' underlying OrderedDict, because we may + # modify the original during the iteration. + # self.fields is a `rest_framework.utils.serializer_helpers.BindingDict` for field_name, field in self.fields.fields.copy().items(): if field_name == api_settings.URL_FIELD_NAME: # leave self link there continue @@ -100,7 +110,8 @@ def validate_path(serializer_class, field_path, path): super(IncludedResourcesValidationMixin, self).__init__(*args, **kwargs) -class HyperlinkedModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer): +class HyperlinkedModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, + HyperlinkedModelSerializer): """ A type of `ModelSerializer` that uses hyperlinked relationships instead of primary key relationships. Specifically: @@ -151,3 +162,122 @@ def get_field_names(self, declared_fields, info): declared[field_name] = field fields = super(ModelSerializer, self).get_field_names(declared, info) return list(fields) + list(getattr(self.Meta, 'meta_fields', list())) + + +class PolymorphicSerializerMetaclass(SerializerMetaclass): + """ + This metaclass ensures that the `polymorphic_serializers` is correctly defined on a + `PolymorphicSerializer` class and make a cache of model/serializer/type mappings. + """ + + def __new__(cls, name, bases, attrs): + new_class = super(PolymorphicSerializerMetaclass, cls).__new__(cls, name, bases, attrs) + + # Ensure initialization is only performed for subclasses of PolymorphicModelSerializer + # (excluding PolymorphicModelSerializer class itself). + parents = [b for b in bases if isinstance(b, PolymorphicSerializerMetaclass)] + if not parents: + return new_class + + polymorphic_serializers = getattr(new_class, 'polymorphic_serializers', None) + if not polymorphic_serializers: + raise NotImplementedError( + "A PolymorphicModelSerializer must define a `polymorphic_serializers` attribute.") + serializer_to_model = { + serializer: serializer.Meta.model for serializer in polymorphic_serializers} + model_to_serializer = { + serializer.Meta.model: serializer for serializer in polymorphic_serializers} + type_to_model = { + get_resource_type_from_model(model): model for model in model_to_serializer.keys()} + setattr(new_class, '_poly_serializer_model_map', serializer_to_model) + setattr(new_class, '_poly_model_serializer_map', model_to_serializer) + setattr(new_class, '_poly_type_model_map', type_to_model) + return new_class + + +@six.add_metaclass(PolymorphicSerializerMetaclass) +class PolymorphicModelSerializer(ModelSerializer): + """ + A serializer for polymorphic models. + Useful for "lazy" parent models. Leaves should be represented with a regular serializer. + """ + def get_fields(self): + """ + Return an exhaustive list of the polymorphic serializer fields. + """ + if self.instance is not None: + if not isinstance(self.instance, QuerySet): + serializer_class = self.get_polymorphic_serializer_for_instance(self.instance) + return serializer_class(self.instance, context=self.context).get_fields() + else: + raise Exception("Cannot get fields from a polymorphic serializer given a queryset") + return super(PolymorphicModelSerializer, self).get_fields() + + def get_polymorphic_serializer_for_instance(self, instance): + """ + Return the polymorphic serializer associated with the given instance/model. + Raise `NotImplementedError` if no serializer is found for the given model. This usually + means that a serializer is missing in the class's `polymorphic_serializers` attribute. + """ + try: + return self._poly_model_serializer_map[instance._meta.model] + except KeyError: + raise NotImplementedError( + "No polymorphic serializer has been found for model {}".format( + instance._meta.model.__name__)) + + def get_polymorphic_model_for_serializer(self, serializer): + """ + Return the polymorphic model associated with the given serializer. + Raise `NotImplementedError` if no model is found for the given serializer. This usually + means that a serializer is missing in the class's `polymorphic_serializers` attribute. + """ + try: + return self._poly_serializer_model_map[serializer] + except KeyError: + raise NotImplementedError( + "No polymorphic model has been found for serializer {}".format(serializer.__name__)) + + def get_polymorphic_model_for_type(self, obj_type): + """ + Return the polymorphic model associated with the given type. + Raise `NotImplementedError` if no model is found for the given type. This usually + means that a serializer is missing in the class's `polymorphic_serializers` attribute. + """ + try: + return self._poly_type_model_map[obj_type] + except KeyError: + raise NotImplementedError( + "No polymorphic model has been found for type {}".format(obj_type)) + + def get_polymorphic_serializer_for_type(self, obj_type): + """ + Return the polymorphic serializer associated with the given type. + Raise `NotImplementedError` if no serializer is found for the given type. This usually + means that a serializer is missing in the class's `polymorphic_serializers` attribute. + """ + return self.get_polymorphic_serializer_for_instance( + self.get_polymorphic_model_for_type(obj_type)) + + def to_representation(self, instance): + """ + Retrieve the appropriate polymorphic serializer and use this to handle representation. + """ + serializer_class = self.get_polymorphic_serializer_for_instance(instance) + return serializer_class(instance, context=self.context).to_representation(instance) + + def to_internal_value(self, data): + """ + Ensure that the given type is one of the expected polymorphic types, then retrieve the + appropriate polymorphic serializer and use this to handle internal value. + """ + received_type = data.get('type') + expected_types = self._poly_type_model_map.keys() + if received_type not in expected_types: + raise Conflict( + 'Incorrect relation type. Expected on of {expected_types}, ' + 'received {received_type}.'.format( + expected_types=', '.join(expected_types), received_type=received_type)) + serializer_class = self.get_polymorphic_serializer_for_type(received_type) + self.__class__ = serializer_class + return serializer_class(data, context=self.context).to_internal_value(data) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 24bb5d54..ad34daa3 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -92,6 +92,7 @@ def get_serializer_fields(serializer): pass return fields + def format_keys(obj, format_type=None): """ Takes either a dict or list and returns it with camelized keys only if @@ -147,12 +148,15 @@ def format_value(value, format_type=None): def format_relation_name(value, format_type=None): - warnings.warn("The 'format_relation_name' function has been renamed 'format_resource_type' and the settings are now 'JSON_API_FORMAT_TYPES' and 'JSON_API_PLURALIZE_TYPES'") + warnings.warn( + "The 'format_relation_name' function has been renamed 'format_resource_type' and " + "the settings are now 'JSON_API_FORMAT_TYPES' and 'JSON_API_PLURALIZE_TYPES'") if format_type is None: format_type = getattr(settings, 'JSON_API_FORMAT_RELATION_KEYS', None) pluralize = getattr(settings, 'JSON_API_PLURALIZE_RELATION_TYPE', None) return format_resource_type(value, format_type, pluralize) + def format_resource_type(value, format_type=None, pluralize=None): if format_type is None: format_type = getattr(settings, 'JSON_API_FORMAT_TYPES', False) @@ -243,8 +247,8 @@ def get_resource_type_from_serializer(serializer): json_api_meta = getattr(serializer, 'JSONAPIMeta', None) meta = getattr(serializer, 'Meta', None) if hasattr(serializer, 'polymorphic_serializers'): - return [get_resource_type_from_serializer(s['serializer']) for s in serializer.polymorphic_serializers] - if hasattr(json_api_meta, 'resource_name'): + return [get_resource_type_from_serializer(s) for s in serializer.polymorphic_serializers] + elif hasattr(json_api_meta, 'resource_name'): return json_api_meta.resource_name elif hasattr(meta, 'resource_name'): return meta.resource_name From fddb06bb2f2e275912b755449eaa9c8c36673da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Mon, 16 May 2016 17:14:32 +0200 Subject: [PATCH 06/31] Basic support of write operations on polymorphic relations --- example/serializers.py | 3 ++ .../tests/integration/test_polymorphism.py | 18 ++++++++ rest_framework_json_api/relations.py | 44 ++++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index 9c548e94..fdfac3f4 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -115,6 +115,9 @@ class Meta: 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) + included_serializers = { 'current_project': ProjectSerializer, 'future_projects': ProjectSerializer, diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py index 514dc31f..a4e02016 100644 --- a/example/tests/integration/test_polymorphism.py +++ b/example/tests/integration/test_polymorphism.py @@ -71,3 +71,21 @@ def test_polymorphism_on_polymorphic_model_list_post(client): assert content["data"]["type"] == "artProjects" assert content['data']['attributes']['topic'] == test_topic assert content['data']['attributes']['artist'] == test_artist + + +def test_polymorphism_relations_update(single_company, research_project_factory, client): + response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk})) + content = load_json(response.content) + assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" + + research_project = research_project_factory() + content["data"]["relationships"]["currentProject"]["data"] = { + "type": "researchProjects", + "id": research_project.pk + } + response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}), + data=json.dumps(content), content_type='application/vnd.api+json') + assert response.status_code is 200 + content = load_json(response.content) + assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "researchProjects" + assert int(content["data"]["relationships"]["currentProject"]["data"]["id"]) is research_project.pk diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index fa7c025b..01025013 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -12,6 +12,7 @@ class ResourceRelatedField(PrimaryKeyRelatedField): + _skip_polymorphic_optimization = True self_link_view_name = None related_link_view_name = None related_link_lookup_field = 'pk' @@ -21,6 +22,7 @@ class ResourceRelatedField(PrimaryKeyRelatedField): 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), 'incorrect_type': _('Incorrect type. Expected resource identifier object, received {data_type}.'), 'incorrect_relation_type': _('Incorrect relation type. Expected {relation_type}, received {received_type}.'), + # 'incorrect_poly_relation_type': _('Incorrect relation type. Expected one of {relation_type}, received {received_type}.'), 'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'), 'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'), 'no_match': _('Invalid hyperlink - No URL match.'), @@ -135,7 +137,8 @@ def to_internal_value(self, data): self.fail('missing_id') if data['type'] != expected_relation_type: - self.conflict('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type']) + self.conflict('incorrect_relation_type', relation_type=expected_relation_type, + received_type=data['type']) return super(ResourceRelatedField, self).to_internal_value(data['id']) @@ -150,7 +153,8 @@ def to_representation(self, value): resource_type = None root = getattr(self.parent, 'parent', self.parent) field_name = self.field_name if self.field_name else self.parent.field_name - if getattr(root, 'included_serializers', None) is not None and not self.is_polymorphic: + if getattr(root, 'included_serializers', None) is not None and \ + self._skip_polymorphic_optimization: includes = get_included_serializers(root) if field_name in includes.keys(): resource_type = get_resource_type_from_serializer(includes[field_name]) @@ -177,6 +181,42 @@ def get_choices(self, cutoff=None): ]) +class PolymorphicResourceRelatedField(ResourceRelatedField): + + _skip_polymorphic_optimization = False + default_error_messages = dict(ResourceRelatedField.default_error_messages, **{ + 'incorrect_relation_type': _('Incorrect relation type. Expected one of {relation_type}, ' + 'received {received_type}.'), + }) + + def __init__(self, polymorphic_serializer, *args, **kwargs): + self.polymorphic_serializer = polymorphic_serializer + super(PolymorphicResourceRelatedField, self).__init__(*args, **kwargs) + + def to_internal_value(self, data): + if isinstance(data, six.text_type): + try: + data = json.loads(data) + except ValueError: + # show a useful error if they send a `pk` instead of resource object + self.fail('incorrect_type', data_type=type(data).__name__) + if not isinstance(data, dict): + self.fail('incorrect_type', data_type=type(data).__name__) + + if 'type' not in data: + self.fail('missing_type') + + if 'id' not in data: + self.fail('missing_id') + + expected_relation_types = get_resource_type_from_serializer(self.polymorphic_serializer) + + if data['type'] not in expected_relation_types: + self.conflict('incorrect_relation_type', relation_type=", ".join( + expected_relation_types), received_type=data['type']) + + return super(ResourceRelatedField, self).to_internal_value(data['id']) + class SerializerMethodResourceRelatedField(ResourceRelatedField): """ From 22829d1eed54d5819864bd645f222d15a0775294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Tue, 17 May 2016 16:19:21 +0200 Subject: [PATCH 07/31] Improve polymorphism documentation --- docs/usage.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index adc73a13..09a06def 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -425,7 +425,9 @@ field_name_mapping = { ### Working with polymorphic resources -This package can defer the resolution of the type of polymorphic models instances to get the appropriate type. +#### 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: @@ -436,6 +438,40 @@ JSON_API_POLYMORPHIC_ANCESTORS = ( ) ``` +#### Writing polymorphic resources + +A polymorphic endpoint can be setup if associated with a polymorphic serializer. +A polymorphic serializer take 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 explicitely declared with the `polymorphic_serializer` (first positional argument) correctly defined. +It must be a subclass of `serializers.PolymorphicModelSerializer`. + + ### Meta You may add metadata to the rendered json in two different ways: `meta_fields` and `get_root_meta`. From d565334e1dc58616c16e41982f88d1d1a8bebd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Tue, 17 May 2016 18:02:25 +0200 Subject: [PATCH 08/31] Improve polymorphic relations and tests. --- docs/usage.md | 8 +++ example/serializers.py | 6 ++- .../tests/integration/test_polymorphism.py | 49 ++++++++++++++++- rest_framework_json_api/parsers.py | 52 +++++++++++-------- rest_framework_json_api/relations.py | 5 +- rest_framework_json_api/serializers.py | 50 +++++++++++------- rest_framework_json_api/utils.py | 13 +++-- 7 files changed, 131 insertions(+), 52 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 09a06def..e4ccc6d6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -471,6 +471,14 @@ class CompanySerializer(serializers.ModelSerializer): They must be explicitely 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 diff --git a/example/serializers.py b/example/serializers.py index fdfac3f4..73f600bd 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -115,8 +115,10 @@ class Meta: 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) + current_project = relations.PolymorphicResourceRelatedField( + ProjectSerializer, queryset=models.Project.objects.all()) + future_projects = relations.PolymorphicResourceRelatedField( + ProjectSerializer, queryset=models.Project.objects.all(), many=True) included_serializers = { 'current_project': ProjectSerializer, diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py index a4e02016..5b7fbb7b 100644 --- a/example/tests/integration/test_polymorphism.py +++ b/example/tests/integration/test_polymorphism.py @@ -73,6 +73,29 @@ def test_polymorphism_on_polymorphic_model_list_post(client): assert content['data']['attributes']['artist'] == test_artist +def test_invalid_type_on_polymorphic_model(client): + test_topic = 'New test topic {}'.format(random.randint(0, 999999)) + test_artist = 'test-{}'.format(random.randint(0, 999999)) + url = reverse('project-list') + data = { + 'data': { + 'type': 'invalidProjects', + 'attributes': { + 'topic': test_topic, + 'artist': test_artist + } + } + } + response = client.post(url, data=json.dumps(data), content_type='application/vnd.api+json') + assert response.status_code == 409 + content = load_json(response.content) + assert len(content["errors"]) is 1 + assert content["errors"][0]["status"] == "409" + assert content["errors"][0]["detail"] == \ + "The resource object's type (invalidProjects) is not the type that constitute the " \ + "collection represented by the endpoint (one of [researchProjects, artProjects])." + + def test_polymorphism_relations_update(single_company, research_project_factory, client): response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk})) content = load_json(response.content) @@ -85,7 +108,29 @@ def test_polymorphism_relations_update(single_company, research_project_factory, } response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}), data=json.dumps(content), content_type='application/vnd.api+json') - assert response.status_code is 200 + assert response.status_code == 200 content = load_json(response.content) assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "researchProjects" - assert int(content["data"]["relationships"]["currentProject"]["data"]["id"]) is research_project.pk + assert int(content["data"]["relationships"]["currentProject"]["data"]["id"]) == \ + research_project.pk + + +def test_invalid_type_on_polymorphic_relation(single_company, research_project_factory, client): + response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk})) + content = load_json(response.content) + assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" + + research_project = research_project_factory() + content["data"]["relationships"]["currentProject"]["data"] = { + "type": "invalidProjects", + "id": research_project.pk + } + response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}), + data=json.dumps(content), content_type='application/vnd.api+json') + assert response.status_code == 409 + content = load_json(response.content) + assert len(content["errors"]) is 1 + assert content["errors"][0]["status"] == "409" + assert content["errors"][0]["detail"] == \ + "Incorrect relation type. Expected one of [researchProjects, artProjects], " \ + "received invalidProjects." diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 2e65715a..ba1fb193 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -30,7 +30,8 @@ class JSONParser(parsers.JSONParser): @staticmethod def parse_attributes(data): - return utils.format_keys(data.get('attributes'), 'underscore') if data.get('attributes') else dict() + return utils.format_keys( + data.get('attributes'), 'underscore') if data.get('attributes') else dict() @staticmethod def parse_relationships(data): @@ -59,40 +60,49 @@ def parse(self, stream, media_type=None, parser_context=None): """ Parses the incoming bytestream as JSON and returns the resulting data """ - result = super(JSONParser, self).parse(stream, media_type=media_type, parser_context=parser_context) + result = super(JSONParser, self).parse( + stream, media_type=media_type, parser_context=parser_context) data = result.get('data') if data: from rest_framework_json_api.views import RelationshipView if isinstance(parser_context['view'], RelationshipView): - # We skip parsing the object as JSONAPI Resource Identifier Object and not a regular Resource Object + # We skip parsing the object as JSONAPI Resource Identifier Object is not a + # regular Resource Object if isinstance(data, list): for resource_identifier_object in data: - if not (resource_identifier_object.get('id') and resource_identifier_object.get('type')): - raise ParseError( - 'Received data contains one or more malformed JSONAPI Resource Identifier Object(s)' - ) + if not (resource_identifier_object.get('id') and + resource_identifier_object.get('type')): + raise ParseError('Received data contains one or more malformed ' + 'JSONAPI Resource Identifier Object(s)') elif not (data.get('id') and data.get('type')): - raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') + raise ParseError('Received data is not a valid ' + 'JSONAPI Resource Identifier Object') return data request = parser_context.get('request') # Check for inconsistencies - resource_name = utils.get_resource_name(parser_context) - if isinstance(resource_name, six.string_types): - doesnt_match = data.get('type') != resource_name - else: - doesnt_match = data.get('type') not in resource_name - if doesnt_match and request.method in ('PUT', 'POST', 'PATCH'): - raise exceptions.Conflict( - "The resource object's type ({data_type}) is not the type " - "that constitute the collection represented by the endpoint ({resource_type}).".format( - data_type=data.get('type'), - resource_type=resource_name - ) - ) + if request.method in ('PUT', 'POST', 'PATCH'): + resource_name = utils.get_resource_name( + parser_context, expand_polymorphic_types=True) + if isinstance(resource_name, six.string_types): + if data.get('type') != resource_name: + raise exceptions.Conflict( + "The resource object's type ({data_type}) is not the type that " + "constitute the collection represented by the endpoint " + "({resource_type}).".format( + data_type=data.get('type'), + resource_type=resource_name)) + else: + if data.get('type') not in resource_name: + raise exceptions.Conflict( + "The resource object's type ({data_type}) is not the type that " + "constitute the collection represented by the endpoint " + "(one of [{resource_types}]).".format( + data_type=data.get('type'), + resource_types=", ".join(resource_name))) if not data.get('id') and request.method in ('PATCH', 'PUT'): raise ParseError("The resource identifier object must contain an 'id' member") diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 01025013..81645acd 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -22,7 +22,6 @@ class ResourceRelatedField(PrimaryKeyRelatedField): 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), 'incorrect_type': _('Incorrect type. Expected resource identifier object, received {data_type}.'), 'incorrect_relation_type': _('Incorrect relation type. Expected {relation_type}, received {received_type}.'), - # 'incorrect_poly_relation_type': _('Incorrect relation type. Expected one of {relation_type}, received {received_type}.'), 'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'), 'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'), 'no_match': _('Invalid hyperlink - No URL match.'), @@ -185,7 +184,7 @@ class PolymorphicResourceRelatedField(ResourceRelatedField): _skip_polymorphic_optimization = False default_error_messages = dict(ResourceRelatedField.default_error_messages, **{ - 'incorrect_relation_type': _('Incorrect relation type. Expected one of {relation_type}, ' + 'incorrect_relation_type': _('Incorrect relation type. Expected one of [{relation_type}], ' 'received {received_type}.'), }) @@ -209,7 +208,7 @@ def to_internal_value(self, data): if 'id' not in data: self.fail('missing_id') - expected_relation_types = get_resource_type_from_serializer(self.polymorphic_serializer) + expected_relation_types = self.polymorphic_serializer.get_polymorphic_types() if data['type'] not in expected_relation_types: self.conflict('incorrect_relation_type', relation_type=", ".join( diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index ccc98124..c3dc7a36 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -187,11 +187,12 @@ def __new__(cls, name, bases, attrs): serializer: serializer.Meta.model for serializer in polymorphic_serializers} model_to_serializer = { serializer.Meta.model: serializer for serializer in polymorphic_serializers} - type_to_model = { - get_resource_type_from_model(model): model for model in model_to_serializer.keys()} + type_to_serializer = { + get_resource_type_from_serializer(serializer): serializer for + serializer in polymorphic_serializers} setattr(new_class, '_poly_serializer_model_map', serializer_to_model) setattr(new_class, '_poly_model_serializer_map', model_to_serializer) - setattr(new_class, '_poly_type_model_map', type_to_model) + setattr(new_class, '_poly_type_serializer_map', type_to_serializer) return new_class @@ -213,51 +214,62 @@ def get_fields(self): raise Exception("Cannot get fields from a polymorphic serializer given a queryset") return super(PolymorphicModelSerializer, self).get_fields() - def get_polymorphic_serializer_for_instance(self, instance): + @classmethod + def get_polymorphic_serializer_for_instance(cls, instance): """ Return the polymorphic serializer associated with the given instance/model. Raise `NotImplementedError` if no serializer is found for the given model. This usually means that a serializer is missing in the class's `polymorphic_serializers` attribute. """ try: - return self._poly_model_serializer_map[instance._meta.model] + return cls._poly_model_serializer_map[instance._meta.model] except KeyError: raise NotImplementedError( "No polymorphic serializer has been found for model {}".format( instance._meta.model.__name__)) - def get_polymorphic_model_for_serializer(self, serializer): + @classmethod + def get_polymorphic_model_for_serializer(cls, serializer): """ Return the polymorphic model associated with the given serializer. Raise `NotImplementedError` if no model is found for the given serializer. This usually means that a serializer is missing in the class's `polymorphic_serializers` attribute. """ try: - return self._poly_serializer_model_map[serializer] + return cls._poly_serializer_model_map[serializer] except KeyError: raise NotImplementedError( "No polymorphic model has been found for serializer {}".format(serializer.__name__)) - def get_polymorphic_model_for_type(self, obj_type): + @classmethod + def get_polymorphic_serializer_for_type(cls, obj_type): """ - Return the polymorphic model associated with the given type. - Raise `NotImplementedError` if no model is found for the given type. This usually + Return the polymorphic serializer associated with the given type. + Raise `NotImplementedError` if no serializer is found for the given type. This usually means that a serializer is missing in the class's `polymorphic_serializers` attribute. """ try: - return self._poly_type_model_map[obj_type] + return cls._poly_type_serializer_map[obj_type] except KeyError: raise NotImplementedError( - "No polymorphic model has been found for type {}".format(obj_type)) + "No polymorphic serializer has been found for type {}".format(obj_type)) - def get_polymorphic_serializer_for_type(self, obj_type): + @classmethod + def get_polymorphic_model_for_type(cls, obj_type): """ - Return the polymorphic serializer associated with the given type. - Raise `NotImplementedError` if no serializer is found for the given type. This usually + Return the polymorphic model associated with the given type. + Raise `NotImplementedError` if no model is found for the given type. This usually means that a serializer is missing in the class's `polymorphic_serializers` attribute. """ - return self.get_polymorphic_serializer_for_instance( - self.get_polymorphic_model_for_type(obj_type)) + return cls.get_polymorphic_model_for_serializer( + cls.get_polymorphic_serializer_for_type(obj_type)) + + @classmethod + def get_polymorphic_types(cls): + """ + Return the list of accepted types. + """ + return cls._poly_type_serializer_map.keys() def to_representation(self, instance): """ @@ -272,10 +284,10 @@ def to_internal_value(self, data): appropriate polymorphic serializer and use this to handle internal value. """ received_type = data.get('type') - expected_types = self._poly_type_model_map.keys() + expected_types = self.get_polymorphic_types() if received_type not in expected_types: raise Conflict( - 'Incorrect relation type. Expected on of {expected_types}, ' + 'Incorrect relation type. Expected on of [{expected_types}], ' 'received {received_type}.'.format( expected_types=', '.join(expected_types), received_type=received_type)) serializer_class = self.get_polymorphic_serializer_for_type(received_type) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index ad34daa3..28f9e8bc 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -32,10 +32,11 @@ POLYMORPHIC_ANCESTORS += (ancestor_class,) -def get_resource_name(context): +def get_resource_name(context, expand_polymorphic_types=False): """ Return the name of a resource. """ + from . import serializers view = context.get('view') # Sanity check to make sure we have a view. @@ -57,7 +58,11 @@ def get_resource_name(context): except AttributeError: try: serializer = view.get_serializer_class() - return get_resource_type_from_serializer(serializer) + if issubclass(serializer, serializers.PolymorphicModelSerializer) and \ + expand_polymorphic_types: + return serializer.get_polymorphic_types() + else: + return get_resource_type_from_serializer(serializer) except AttributeError: try: resource_name = get_resource_type_from_model(view.model) @@ -246,9 +251,7 @@ def get_resource_type_from_manager(manager): def get_resource_type_from_serializer(serializer): json_api_meta = getattr(serializer, 'JSONAPIMeta', None) meta = getattr(serializer, 'Meta', None) - if hasattr(serializer, 'polymorphic_serializers'): - return [get_resource_type_from_serializer(s) for s in serializer.polymorphic_serializers] - elif hasattr(json_api_meta, 'resource_name'): + if hasattr(json_api_meta, 'resource_name'): return json_api_meta.resource_name elif hasattr(meta, 'resource_name'): return meta.resource_name From e840438e829264b50a0500c649d1f389df616887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Thu, 8 Sep 2016 17:05:32 +0200 Subject: [PATCH 09/31] Add django-polymorphic as test dependency pytest-factoryboy does not support pytest3.0+ --- .travis.yml | 3 ++- setup.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6359c019..ab1007f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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} diff --git a/setup.py b/setup.py index ad4805a0..ec69c97a 100755 --- a/setup.py +++ b/setup.py @@ -105,7 +105,8 @@ def get_package_data(package): tests_require=[ 'pytest-factoryboy', 'pytest-django', - 'pytest>=2.8', + 'pytest>=2.8,<3', + 'django-polymorphic', ] + mock, zip_safe=False, ) From 19b0238c6441b413f1512b6a7e47bf3d5b15cf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Thu, 8 Sep 2016 17:06:42 +0200 Subject: [PATCH 10/31] Avoid type list comparison in polymorphic tests --- .../tests/integration/test_polymorphism.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py index 5b7fbb7b..1bfe091e 100644 --- a/example/tests/integration/test_polymorphism.py +++ b/example/tests/integration/test_polymorphism.py @@ -18,8 +18,8 @@ def test_polymorphism_on_detail_relations(single_company, client): response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk})) content = load_json(response.content) assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" - assert [rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]] == [ - "researchProjects", "artProjects"] + assert set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) == set([ + "researchProjects", "artProjects"]) def test_polymorphism_on_included_relations(single_company, client): @@ -27,10 +27,10 @@ def test_polymorphism_on_included_relations(single_company, client): '?include=current_project,future_projects') content = load_json(response.content) assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" - assert [rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]] == [ - "researchProjects", "artProjects"] - assert [x.get('type') for x in content.get('included')] == [ - 'artProjects', 'artProjects', 'researchProjects'], 'Detail included types are incorrect' + assert set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) == set([ + "researchProjects", "artProjects"]) + assert set([x.get('type') for x in content.get('included')]) == set([ + 'artProjects', 'artProjects', 'researchProjects']), 'Detail included types are incorrect' # Ensure that the child fields are present. assert content.get('included')[0].get('attributes').get('artist') is not None assert content.get('included')[1].get('attributes').get('artist') is not None @@ -47,7 +47,7 @@ def test_polymorphism_on_polymorphic_model_detail_patch(single_art_project, clie content['data']['attributes']['artist'] = test_artist response = client.patch(url, data=json.dumps(content), content_type='application/vnd.api+json') new_content = load_json(response.content) - assert new_content["data"]["type"] == "artProjects" + assert new_content['data']['type'] == "artProjects" assert new_content['data']['attributes']['topic'] == test_topic assert new_content['data']['attributes']['artist'] == test_artist @@ -68,7 +68,7 @@ def test_polymorphism_on_polymorphic_model_list_post(client): response = client.post(url, data=json.dumps(data), content_type='application/vnd.api+json') content = load_json(response.content) assert content['data']['id'] is not None - assert content["data"]["type"] == "artProjects" + assert content['data']['type'] == "artProjects" assert content['data']['attributes']['topic'] == test_topic assert content['data']['attributes']['artist'] == test_artist @@ -91,9 +91,15 @@ def test_invalid_type_on_polymorphic_model(client): content = load_json(response.content) assert len(content["errors"]) is 1 assert content["errors"][0]["status"] == "409" - assert content["errors"][0]["detail"] == \ - "The resource object's type (invalidProjects) is not the type that constitute the " \ - "collection represented by the endpoint (one of [researchProjects, artProjects])." + try: + assert content["errors"][0]["detail"] == \ + "The resource object's type (invalidProjects) is not the type that constitute the " \ + "collection represented by the endpoint (one of [researchProjects, artProjects])." + except AssertionError: + # Available type list order isn't enforced + assert content["errors"][0]["detail"] == \ + "The resource object's type (invalidProjects) is not the type that constitute the " \ + "collection represented by the endpoint (one of [artProjects, researchProjects])." def test_polymorphism_relations_update(single_company, research_project_factory, client): @@ -131,6 +137,12 @@ def test_invalid_type_on_polymorphic_relation(single_company, research_project_f content = load_json(response.content) assert len(content["errors"]) is 1 assert content["errors"][0]["status"] == "409" - assert content["errors"][0]["detail"] == \ - "Incorrect relation type. Expected one of [researchProjects, artProjects], " \ - "received invalidProjects." + try: + assert content["errors"][0]["detail"] == \ + "Incorrect relation type. Expected one of [researchProjects, artProjects], " \ + "received invalidProjects." + except AssertionError: + # Available type list order isn't enforced + assert content["errors"][0]["detail"] == \ + "Incorrect relation type. Expected one of [artProjects, researchProjects], " \ + "received invalidProjects." From b8bf612e63e8a4cb5841e9efd7d7101484dc0a3a Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 14:50:43 -0400 Subject: [PATCH 11/31] Flake8 --- example/factories/__init__.py | 4 +++- example/serializers.py | 5 ++++- example/tests/conftest.py | 5 +++-- example/tests/integration/test_polymorphism.py | 8 ++++---- rest_framework_json_api/parsers.py | 5 ++++- rest_framework_json_api/renderers.py | 6 +++++- rest_framework_json_api/utils.py | 14 ++++++++------ 7 files changed, 31 insertions(+), 16 deletions(-) diff --git a/example/factories/__init__.py b/example/factories/__init__.py index 8bcbc259..a7485500 100644 --- a/example/factories/__init__.py +++ b/example/factories/__init__.py @@ -2,7 +2,9 @@ import factory from faker import Factory as FakerFactory -from example.models import Blog, Author, AuthorBio, Entry, Comment, TaggedItem, ArtProject, ResearchProject, Company +from example.models import ( + Blog, Author, AuthorBio, Entry, Comment, TaggedItem, ArtProject, ResearchProject, Company +) faker = FakerFactory.create() faker.seed(983843) diff --git a/example/serializers.py b/example/serializers.py index 3add4377..d215d448 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,6 +1,9 @@ from datetime import datetime from rest_framework_json_api import serializers, relations -from example.models import Blog, Entry, Author, AuthorBio, Comment, TaggedItemm, Project, ArtProject, ResearchProject, Company +from example.models import ( + Blog, Entry, Author, AuthorBio, Comment, TaggedItem, Project, ArtProject, ResearchProject, + Company, +) class TaggedItemSerializer(serializers.ModelSerializer): diff --git a/example/tests/conftest.py b/example/tests/conftest.py index 74c30ac4..9db8edc1 100644 --- a/example/tests/conftest.py +++ b/example/tests/conftest.py @@ -3,7 +3,7 @@ from example.factories import ( BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory, - TaggedItemFactory + TaggedItemFactory, ArtProjectFactory, ResearchProjectFactory, CompanyFactory, ) register(BlogFactory) @@ -16,8 +16,9 @@ register(ResearchProjectFactory) register(CompanyFactory) + @pytest.fixture -def single_entry(blog, author, entry_factory, comment_factory): +def single_entry(blog, author, entry_factory, comment_factory, tagged_item_factory): entry = entry_factory(blog=blog, authors=(author,)) comment_factory(entry=entry) diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py index 1bfe091e..8c6e9b94 100644 --- a/example/tests/integration/test_polymorphism.py +++ b/example/tests/integration/test_polymorphism.py @@ -18,8 +18,8 @@ def test_polymorphism_on_detail_relations(single_company, client): response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk})) content = load_json(response.content) assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" - assert set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) == set([ - "researchProjects", "artProjects"]) + assert (set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) + == set(["researchProjects", "artProjects"])) def test_polymorphism_on_included_relations(single_company, client): @@ -27,8 +27,8 @@ def test_polymorphism_on_included_relations(single_company, client): '?include=current_project,future_projects') content = load_json(response.content) assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" - assert set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) == set([ - "researchProjects", "artProjects"]) + assert (set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) + == set(["researchProjects", "artProjects"])) assert set([x.get('type') for x in content.get('included')]) == set([ 'artProjects', 'artProjects', 'researchProjects']), 'Detail included types are incorrect' # Ensure that the child fields are present. diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 517c9e97..bc7a911a 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -1,6 +1,8 @@ """ Parsers """ +from django.conf import settings +from django.utils import six from rest_framework import parsers from rest_framework.exceptions import ParseError @@ -127,7 +129,8 @@ def parse(self, stream, media_type=None, parser_context=None): raise ParseError("The resource identifier object must contain an 'id' member") # Construct the return data - parsed_data = {'id': data.get('id'), 'type': data.get('type')} if 'id' in data else {'type': data.get('type')} + parsed_data = {'id': data.get('id')} if 'id' in data else {} + parsed_data['type'] = data.get('type') parsed_data.update(self.parse_attributes(data)) parsed_data.update(self.parse_relationships(data)) parsed_data.update(self.parse_metadata(result)) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 74639f35..03f7686d 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -352,7 +352,11 @@ def extract_included(cls, fields, resource, resource_instance, included_resource relation_type or utils.get_resource_type_from_instance(nested_resource_instance) ) - serializer_fields = utils.get_serializer_fields(serializer.__class__(nested_resource_instance, context=serializer.context)) + serializer_fields = utils.get_serializer_fields( + serializer.__class__( + nested_resource_instance, context=serializer.context + ) + ) included_data.append( cls.build_json_resource_obj( serializer_fields, diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index e3a060a6..35c791c8 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -17,6 +17,8 @@ from django.utils.module_loading import import_string as import_class_from_dotted_path from django.utils.translation import ugettext_lazy as _ +from rest_framework_json_api.serializers import PolymorphicModelSerializer + try: from rest_framework.serializers import ManyRelatedField except ImportError: @@ -29,25 +31,26 @@ if django.VERSION >= (1, 9): from django.db.models.fields.related_descriptors import ( - ManyToManyDescriptor, ReverseManyToOneDescriptor + ManyToManyDescriptor, ReverseManyToOneDescriptor # noqa: F401 ) ReverseManyRelatedObjectsDescriptor = object() else: + # noqa: F401 from django.db.models.fields.related import ManyRelatedObjectsDescriptor as ManyToManyDescriptor from django.db.models.fields.related import ( ForeignRelatedObjectsDescriptor as ReverseManyToOneDescriptor ) - from django.db.models.fields.related import ReverseManyRelatedObjectsDescriptor + from django.db.models.fields.related import ReverseManyRelatedObjectsDescriptor # noqa: F401 # Generic relation descriptor from django.contrib.contenttypes. if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS: # pragma: no cover # Target application does not use contenttypes. Importing would cause errors. ReverseGenericManyToOneDescriptor = object() elif django.VERSION >= (1, 9): - from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor + from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor # noqa: F401 else: from django.contrib.contenttypes.fields import ( - ReverseGenericRelatedObjectsDescriptor as ReverseGenericManyToOneDescriptor + ReverseGenericRelatedObjectsDescriptor as ReverseGenericManyToOneDescriptor # noqa: F401 ) POLYMORPHIC_ANCESTORS = () @@ -81,8 +84,7 @@ def get_resource_name(context, expand_polymorphic_types=False): except AttributeError: try: serializer = view.get_serializer_class() - if issubclass(serializer, serializers.PolymorphicModelSerializer) and \ - expand_polymorphic_types: + if issubclass(serializer, PolymorphicModelSerializer) and expand_polymorphic_types: return serializer.get_polymorphic_types() else: return get_resource_type_from_serializer(serializer) From 8fd4617c1f96503b844a5bba27ed253f8c4ccc10 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 14:58:18 -0400 Subject: [PATCH 12/31] Flake8 --- example/tests/integration/test_polymorphism.py | 12 ++++++++---- rest_framework_json_api/utils.py | 7 ++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py index 8c6e9b94..8612319a 100644 --- a/example/tests/integration/test_polymorphism.py +++ b/example/tests/integration/test_polymorphism.py @@ -18,8 +18,10 @@ def test_polymorphism_on_detail_relations(single_company, client): response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk})) content = load_json(response.content) assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" - assert (set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) - == set(["researchProjects", "artProjects"])) + assert ( + set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) == + set(["researchProjects", "artProjects"]) + ) def test_polymorphism_on_included_relations(single_company, client): @@ -27,8 +29,10 @@ def test_polymorphism_on_included_relations(single_company, client): '?include=current_project,future_projects') content = load_json(response.content) assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects" - assert (set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) - == set(["researchProjects", "artProjects"])) + assert ( + set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) == + set(["researchProjects", "artProjects"]) + ) assert set([x.get('type') for x in content.get('included')]) == set([ 'artProjects', 'artProjects', 'researchProjects']), 'Detail included types are incorrect' # Ensure that the child fields are present. diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 35c791c8..bd0925e3 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -35,8 +35,9 @@ ) ReverseManyRelatedObjectsDescriptor = object() else: - # noqa: F401 - from django.db.models.fields.related import ManyRelatedObjectsDescriptor as ManyToManyDescriptor + from django.db.models.fields.related import ( # noqa: F401 + ManyRelatedObjectsDescriptor as ManyToManyDescriptor + ) from django.db.models.fields.related import ( ForeignRelatedObjectsDescriptor as ReverseManyToOneDescriptor ) @@ -49,7 +50,7 @@ elif django.VERSION >= (1, 9): from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor # noqa: F401 else: - from django.contrib.contenttypes.fields import ( + from django.contrib.contenttypes.fields import ( # noqa: F401 ReverseGenericRelatedObjectsDescriptor as ReverseGenericManyToOneDescriptor # noqa: F401 ) From 275793ce2706d4aa7553933027fe51c86da4c1c7 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 15:18:31 -0400 Subject: [PATCH 13/31] Better handle imports? --- rest_framework_json_api/exceptions.py | 2 +- rest_framework_json_api/renderers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework_json_api/exceptions.py b/rest_framework_json_api/exceptions.py index a4a78b74..0ae02b0e 100644 --- a/rest_framework_json_api/exceptions.py +++ b/rest_framework_json_api/exceptions.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import status, exceptions -from rest_framework_json_api import utils +import rest_framework_json_api.utils as utils from rest_framework_json_api import renderers diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 03f7686d..53628db7 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -12,7 +12,7 @@ from rest_framework.serializers import BaseSerializer, Serializer, ListSerializer from rest_framework.settings import api_settings -from . import utils +import rest_framework_json_api.utils as utils class JSONRenderer(renderers.JSONRenderer): From a26df130f2bfe465a656029134c526c5dc2f317d Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 15:25:23 -0400 Subject: [PATCH 14/31] Resolve circular reference --- rest_framework_json_api/exceptions.py | 2 +- rest_framework_json_api/renderers.py | 2 +- rest_framework_json_api/utils.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rest_framework_json_api/exceptions.py b/rest_framework_json_api/exceptions.py index 0ae02b0e..a4a78b74 100644 --- a/rest_framework_json_api/exceptions.py +++ b/rest_framework_json_api/exceptions.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import status, exceptions -import rest_framework_json_api.utils as utils +from rest_framework_json_api import utils from rest_framework_json_api import renderers diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 53628db7..1dbc30bc 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -12,7 +12,7 @@ from rest_framework.serializers import BaseSerializer, Serializer, ListSerializer from rest_framework.settings import api_settings -import rest_framework_json_api.utils as utils +from rest_framework_json_api import utils class JSONRenderer(renderers.JSONRenderer): diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index bd0925e3..167f4e94 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -17,7 +17,7 @@ from django.utils.module_loading import import_string as import_class_from_dotted_path from django.utils.translation import ugettext_lazy as _ -from rest_framework_json_api.serializers import PolymorphicModelSerializer +from rest_framework_json_api import serializers try: from rest_framework.serializers import ManyRelatedField @@ -85,7 +85,8 @@ def get_resource_name(context, expand_polymorphic_types=False): except AttributeError: try: serializer = view.get_serializer_class() - if issubclass(serializer, PolymorphicModelSerializer) and expand_polymorphic_types: + if issubclass(serializer, serializers.PolymorphicModelSerializer) and \ + expand_polymorphic_types: return serializer.get_polymorphic_types() else: return get_resource_type_from_serializer(serializer) From 2278976cf7221a2bde63b851ec570224508760be Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 15:46:01 -0400 Subject: [PATCH 15/31] Really break up import loop --- rest_framework_json_api/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 167f4e94..0fa5b8ba 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -17,8 +17,6 @@ from django.utils.module_loading import import_string as import_class_from_dotted_path from django.utils.translation import ugettext_lazy as _ -from rest_framework_json_api import serializers - try: from rest_framework.serializers import ManyRelatedField except ImportError: @@ -64,6 +62,7 @@ def get_resource_name(context, expand_polymorphic_types=False): """ Return the name of a resource. """ + from rest_framework_json_api.serializers import PolymorphicModelSerializer view = context.get('view') # Sanity check to make sure we have a view. @@ -85,7 +84,7 @@ def get_resource_name(context, expand_polymorphic_types=False): except AttributeError: try: serializer = view.get_serializer_class() - if issubclass(serializer, serializers.PolymorphicModelSerializer) and \ + if issubclass(serializer, PolymorphicModelSerializer) and \ expand_polymorphic_types: return serializer.get_polymorphic_types() else: From 8563b6596be3b80a70a17045acb93f9e043d6c9d Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 15:48:40 -0400 Subject: [PATCH 16/31] Missed something in the merge --- rest_framework_json_api/relations.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index ba4a2657..12bac707 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -12,6 +12,13 @@ get_resource_type_from_queryset, get_resource_type_from_instance, \ get_included_serializers, get_resource_type_from_serializer +LINKS_PARAMS = [ + 'self_link_view_name', + 'related_link_view_name', + 'related_link_lookup_field', + 'related_link_url_kwarg' +] + class ResourceRelatedField(PrimaryKeyRelatedField): _skip_polymorphic_optimization = True From 4aaeac23de01710b5bb27c2b65f484b0406ff288 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 15:50:21 -0400 Subject: [PATCH 17/31] Redo migrations --- ..._20160513_0857.py => 0003_polymorphics.py} | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) rename example/migrations/{0002_auto_20160513_0857.py => 0003_polymorphics.py} (83%) diff --git a/example/migrations/0002_auto_20160513_0857.py b/example/migrations/0003_polymorphics.py similarity index 83% rename from example/migrations/0002_auto_20160513_0857.py rename to example/migrations/0003_polymorphics.py index 2471ea36..9020176b 100644 --- a/example/migrations/0002_auto_20160513_0857.py +++ b/example/migrations/0003_polymorphics.py @@ -1,26 +1,17 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2016-05-13 08:57 +# Generated by Django 1.11.1 on 2017-05-17 14:49 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'), - ] + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('example', '0002_taggeditem'), + ] operations = [ migrations.CreateModel( @@ -40,6 +31,11 @@ class Migration(migrations.Migration): '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=[ From 030f6c838b1cc00dfc7ff08fda7ff739ed0c7669 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 17 May 2017 16:36:14 -0400 Subject: [PATCH 18/31] Wrong indentation --- rest_framework_json_api/parsers.py | 52 +++++++++++++++--------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index bc7a911a..68ec45d2 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -101,32 +101,32 @@ def parse(self, stream, media_type=None, parser_context=None): elif not (data.get('id') and data.get('type')): raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') - return data - - request = parser_context.get('request') - - # Check for inconsistencies - if request.method in ('PUT', 'POST', 'PATCH'): - resource_name = utils.get_resource_name( - parser_context, expand_polymorphic_types=True) - if isinstance(resource_name, six.string_types): - if data.get('type') != resource_name: - raise exceptions.Conflict( - "The resource object's type ({data_type}) is not the type that " - "constitute the collection represented by the endpoint " - "({resource_type}).".format( - data_type=data.get('type'), - resource_type=resource_name)) - else: - if data.get('type') not in resource_name: - raise exceptions.Conflict( - "The resource object's type ({data_type}) is not the type that " - "constitute the collection represented by the endpoint " - "(one of [{resource_types}]).".format( - data_type=data.get('type'), - resource_types=", ".join(resource_name))) - if not data.get('id') and request.method in ('PATCH', 'PUT'): - raise ParseError("The resource identifier object must contain an 'id' member") + return data + + request = parser_context.get('request') + + # Check for inconsistencies + if request.method in ('PUT', 'POST', 'PATCH'): + resource_name = utils.get_resource_name( + parser_context, expand_polymorphic_types=True) + if isinstance(resource_name, six.string_types): + if data.get('type') != resource_name: + raise exceptions.Conflict( + "The resource object's type ({data_type}) is not the type that " + "constitute the collection represented by the endpoint " + "({resource_type}).".format( + data_type=data.get('type'), + resource_type=resource_name)) + else: + if data.get('type') not in resource_name: + raise exceptions.Conflict( + "The resource object's type ({data_type}) is not the type that " + "constitute the collection represented by the endpoint " + "(one of [{resource_types}]).".format( + data_type=data.get('type'), + resource_types=", ".join(resource_name))) + if not data.get('id') and request.method in ('PATCH', 'PUT'): + raise ParseError("The resource identifier object must contain an 'id' member") # Construct the return data parsed_data = {'id': data.get('id')} if 'id' in data else {} From ca23885ff1c924365cefaf01ca7789aa06277dc6 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 24 May 2017 17:36:10 -0400 Subject: [PATCH 19/31] Fix a deprecation --- example/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/example/serializers.py b/example/serializers.py index d215d448..058d56dc 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -153,3 +153,4 @@ class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company + fields = '__all__' From ae759e5dda8ca5eb779d86b57a04a8faeb0d8b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Thu, 25 May 2017 14:27:55 +0200 Subject: [PATCH 20/31] Fix polymorphic type resolution in relations --- rest_framework_json_api/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 12bac707..b5cd52cc 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -169,7 +169,7 @@ def to_representation(self, value): pk = value.pk resource_type = self.get_resource_type_from_included_serializer() - if resource_type is None and self._skip_polymorphic_optimization: + if resource_type is None or not self._skip_polymorphic_optimization: resource_type = get_resource_type_from_instance(value) return OrderedDict([('type', resource_type), ('id', str(pk))]) From 37c5ae633737a62cff2697a429e5fa2d675f1a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Thu, 25 May 2017 19:48:17 +0200 Subject: [PATCH 21/31] Fix tests among different environments --- example/serializers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index 058d56dc..9903a271 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,4 +1,7 @@ from datetime import datetime +from packaging import version + +import rest_framework from rest_framework_json_api import serializers, relations from example.models import ( Blog, Entry, Author, AuthorBio, Comment, TaggedItem, Project, ArtProject, ResearchProject, @@ -40,12 +43,12 @@ class Meta: class EntrySerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): + super(EntrySerializer, self).__init__(*args, **kwargs) # to make testing more concise we'll only output the # `featured` field when it's requested via `include` request = kwargs.get('context', {}).get('request') if request and 'featured' not in request.query_params.get('include', []): - self.fields.pop('featured') - super(EntrySerializer, self).__init__(*args, **kwargs) + self.fields.pop('featured', None) included_serializers = { 'authors': 'example.serializers.AuthorSerializer', @@ -153,4 +156,5 @@ class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company - fields = '__all__' + if version.parse(rest_framework.VERSION) >= version.parse('3.3'): + fields = '__all__' From f36821b4455aa9fc00cd675c1b50b9e3942b72cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Thu, 25 May 2017 20:42:19 +0200 Subject: [PATCH 22/31] Update tox.ini environment list --- tox.ini | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 7db9e4cc..3b32b700 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,22 @@ [tox] envlist = - py{27,33,34,35}-django18-drf{31,32,33,34}, - py{27,34,35}-django19-drf{33,34}, - py{27,34,35}-django110-drf{34}, + py{27,33,34,35,36}-django18-drf{31,32,33,34}, + py{27,34,35,36}-django19-drf{33,34}, + py{27,34,35,36}-django110-drf34, + py{27,34,35,36}-django111-drf{34,35,36}, [testenv] deps = django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 + django111: Django>=1.11,<1.12 drf31: djangorestframework>=3.1,<3.2 drf32: djangorestframework>=3.2,<3.3 drf33: djangorestframework>=3.3,<3.4 drf34: djangorestframework>=3.4,<3.5 + drf35: djangorestframework>=3.5,<3.6 + drf36: djangorestframework>=3.6,<3.7 setenv = PYTHONPATH = {toxinidir} From 4eec4aa690e1a1f806411bb41165f16e913aae71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Thu, 25 May 2017 20:56:36 +0200 Subject: [PATCH 23/31] Add packaging module as requirement for old python versions --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index bf5fc434..016d3df0 100755 --- a/setup.py +++ b/setup.py @@ -108,6 +108,7 @@ def get_package_data(package): 'pytest-django', 'pytest>=2.8,<3', 'django-polymorphic', + 'packaging', ] + mock, zip_safe=False, ) From bc12e0f25f5bf32f8019beee45281aaed1ed1fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Fri, 26 May 2017 11:28:14 +0200 Subject: [PATCH 24/31] Remove the POLYMORPHIC_ANCESTOR code --- docs/usage.md | 13 ------------- example/settings/test.py | 3 --- rest_framework_json_api/renderers.py | 18 ++++++++++++------ rest_framework_json_api/serializers.py | 6 ++++++ rest_framework_json_api/utils.py | 5 ----- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 16734c5c..1bd06ff7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -427,19 +427,6 @@ field_name_mapping = { ### Working with polymorphic resources -#### 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', -) -``` - #### Writing polymorphic resources A polymorphic endpoint can be setup if associated with a polymorphic serializer. diff --git a/example/settings/test.py b/example/settings/test.py index 2de15d40..1f0e959d 100644 --- a/example/settings/test.py +++ b/example/settings/test.py @@ -15,6 +15,3 @@ REST_FRAMEWORK.update({ 'PAGE_SIZE': 1, }) -JSON_API_POLYMORPHIC_ANCESTORS = ( - 'polymorphic.models.PolymorphicModel', -) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 1dbc30bc..4c325525 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -362,7 +362,8 @@ def extract_included(cls, fields, resource, resource_instance, included_resource serializer_fields, serializer_resource, nested_resource_instance, - resource_type + resource_type, + getattr(serializer, '_poly_force_type_resolution', False) ) ) included_data.extend( @@ -384,7 +385,8 @@ def extract_included(cls, fields, resource, resource_instance, included_resource included_data.append( cls.build_json_resource_obj( serializer_fields, serializer_data, - relation_instance, relation_type) + relation_instance, relation_type, + getattr(field, '_poly_force_type_resolution', False)) ) included_data.extend( cls.extract_included( @@ -426,9 +428,10 @@ def extract_root_meta(cls, serializer, resource): return data @classmethod - def build_json_resource_obj(cls, fields, resource, resource_instance, resource_name): + def build_json_resource_obj(cls, fields, resource, resource_instance, resource_name, + force_type_resolution=False): # Determine type from the instance if the underlying model is polymorphic - if isinstance(resource_instance, utils.POLYMORPHIC_ANCESTORS): + if force_type_resolution: resource_name = utils.get_resource_type_from_instance(resource_instance) resource_data = [ ('type', resource_name), @@ -512,6 +515,9 @@ def render(self, data, accepted_media_type=None, renderer_context=None): # Get the serializer fields fields = utils.get_serializer_fields(serializer) + # Determine if resource name must be resolved on each instance (polymorphic serializer) + force_type_resolution = getattr(serializer, '_poly_force_type_resolution', False) + # Extract root meta for any type of serializer json_api_meta.update(self.extract_root_meta(serializer, serializer_data)) @@ -523,7 +529,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): resource_instance = serializer.instance[position] # Get current instance json_resource_obj = self.build_json_resource_obj( - fields, resource, resource_instance, resource_name + fields, resource, resource_instance, resource_name, force_type_resolution ) meta = self.extract_meta(serializer, resource) if meta: @@ -538,7 +544,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): else: resource_instance = serializer.instance json_api_data = self.build_json_resource_obj( - fields, serializer_data, resource_instance, resource_name + fields, serializer_data, resource_instance, resource_name, force_type_resolution ) meta = self.extract_meta(serializer, serializer_data) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index e06a7600..470fdcf9 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -198,6 +198,12 @@ def __new__(cls, name, bases, attrs): setattr(new_class, '_poly_serializer_model_map', serializer_to_model) setattr(new_class, '_poly_model_serializer_map', model_to_serializer) setattr(new_class, '_poly_type_serializer_map', type_to_serializer) + setattr(new_class, '_poly_force_type_resolution', True) + + # Flag each linked polymorphic serializer to force type resolution based on instance + for serializer in polymorphic_serializers: + setattr(serializer, '_poly_force_type_resolution', True) + return new_class diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 0fa5b8ba..41fbf3a7 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -52,11 +52,6 @@ ReverseGenericRelatedObjectsDescriptor as ReverseGenericManyToOneDescriptor # noqa: F401 ) -POLYMORPHIC_ANCESTORS = () -for ancestor in getattr(settings, 'JSON_API_POLYMORPHIC_ANCESTORS', ()): - ancestor_class = import_class_from_dotted_path(ancestor) - POLYMORPHIC_ANCESTORS += (ancestor_class,) - def get_resource_name(context, expand_polymorphic_types=False): """ From 6b4f45b40d330c45fbefd81565e70dce78aed69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20S?= <leo@naeka.fr> Date: Mon, 29 May 2017 17:23:46 +0200 Subject: [PATCH 25/31] Fix some typos and little errors --- docs/usage.md | 6 +++--- example/serializers.py | 2 +- rest_framework_json_api/relations.py | 4 ++++ rest_framework_json_api/serializers.py | 10 +++++----- rest_framework_json_api/utils.py | 3 +-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 1bd06ff7..d2a9eb11 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -429,8 +429,8 @@ field_name_mapping = { #### Writing polymorphic resources -A polymorphic endpoint can be setup if associated with a polymorphic serializer. -A polymorphic serializer take care of (de)serializing the correct instances types and can be defined like this: +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): @@ -457,7 +457,7 @@ class CompanySerializer(serializers.ModelSerializer): model = models.Company ``` -They must be explicitely declared with the `polymorphic_serializer` (first positional argument) correctly defined. +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"> diff --git a/example/serializers.py b/example/serializers.py index 9903a271..d0bc5a35 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,8 +1,8 @@ from datetime import datetime -from packaging import version import rest_framework from rest_framework_json_api import serializers, relations +from packaging import version from example.models import ( Blog, Entry, Author, AuthorBio, Comment, TaggedItem, Project, ArtProject, ResearchProject, Company, diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index b5cd52cc..ae68f5de 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -226,6 +226,10 @@ def get_choices(self, cutoff=None): class PolymorphicResourceRelatedField(ResourceRelatedField): + """ + Inform DRF that the relation must be considered polymorphic. + Takes a `polymorphic_serializer` as the first positional argument to retrieve then validate the accepted types set. + """ _skip_polymorphic_optimization = False default_error_messages = dict(ResourceRelatedField.default_error_messages, **{ diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 470fdcf9..d4808bec 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -195,14 +195,14 @@ def __new__(cls, name, bases, attrs): type_to_serializer = { get_resource_type_from_serializer(serializer): serializer for serializer in polymorphic_serializers} - setattr(new_class, '_poly_serializer_model_map', serializer_to_model) - setattr(new_class, '_poly_model_serializer_map', model_to_serializer) - setattr(new_class, '_poly_type_serializer_map', type_to_serializer) - setattr(new_class, '_poly_force_type_resolution', True) + new_class._poly_serializer_model_map = serializer_to_model + new_class._poly_model_serializer_map = model_to_serializer + new_class._poly_type_serializer_map = type_to_serializer + new_class._poly_force_type_resolution = True # Flag each linked polymorphic serializer to force type resolution based on instance for serializer in polymorphic_serializers: - setattr(serializer, '_poly_force_type_resolution', True) + serializer._poly_force_type_resolution = True return new_class diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 41fbf3a7..7ad2e808 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -79,8 +79,7 @@ def get_resource_name(context, expand_polymorphic_types=False): except AttributeError: try: serializer = view.get_serializer_class() - if issubclass(serializer, PolymorphicModelSerializer) and \ - expand_polymorphic_types: + if expand_polymorphic_types and issubclass(serializer, PolymorphicModelSerializer): return serializer.get_polymorphic_types() else: return get_resource_type_from_serializer(serializer) From 36f3b6a47c4f640ace65188ad66bf9f9d266e05c Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Tue, 30 May 2017 11:27:23 -0400 Subject: [PATCH 26/31] Administrivia --- AUTHORS | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index bc8d6dee..8bd60b5d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,7 @@ 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> Matt Layman <http://www.mattlayman.com> Oliver Sauder <os@esite.ch> diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c157642..04d25ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ v2.3.0 +* Added support for polymorphic models via `django-polymorphic` * 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. From 05cdb515b8e5e5c57c2877d471e84cea092325c8 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Tue, 30 May 2017 11:53:17 -0400 Subject: [PATCH 27/31] Restore generic relation support Tests? --- rest_framework_json_api/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 7ad2e808..a8cf40e2 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -242,6 +242,15 @@ def get_related_resource_type(relation): relation_model = parent_model_relation.related.related_model else: relation_model = parent_model_relation.related.model + elif parent_model_relation_type is ManyToManyDescriptor: + relation_model = parent_model_relation.field.remote_field.model + elif parent_model_relation_type is ReverseManyRelatedObjectsDescriptor: + relation_model = parent_model_relation.field.related.model + elif parent_model_relation_type is ReverseGenericManyToOneDescriptor: + if django.VERSION >= (1, 9): + relation_model = parent_model_relation.rel.model + else: + relation_model = parent_model_relation.field.related_model elif hasattr(parent_model_relation, 'field'): try: relation_model = parent_model_relation.field.remote_field.model From c1afe354d5fccaca505ef3f7535824421dba9d95 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Tue, 30 May 2017 11:55:51 -0400 Subject: [PATCH 28/31] Add Leo to authors --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 8bd60b5d..5efdce3a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,6 @@ 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> - From 8ff54658cf27f011912553634ba87650b44667cd Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Tue, 30 May 2017 11:57:37 -0400 Subject: [PATCH 29/31] PEP8 --- rest_framework_json_api/relations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index ae68f5de..a697815e 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -228,7 +228,8 @@ def get_choices(self, cutoff=None): class PolymorphicResourceRelatedField(ResourceRelatedField): """ Inform DRF that the relation must be considered polymorphic. - Takes a `polymorphic_serializer` as the first positional argument to retrieve then validate the accepted types set. + Takes a `polymorphic_serializer` as the first positional argument to + retrieve then validate the accepted types set. """ _skip_polymorphic_optimization = False From c5599c0bb29176bcce2e6c818cd7ad5da633dd83 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Wed, 31 May 2017 16:43:13 -0400 Subject: [PATCH 30/31] Really bad writing. --- docs/usage.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index d2a9eb11..a04a1b2b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -427,6 +427,12 @@ field_name_mapping = { ### Working with polymorphic resources +Polymorphic resources (as backed by [django-polymorphic](https://django-polymorphic.readthedocs.io/en/stable/)) +allow you to easily use specialized subclasses without have to have 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. + #### Writing polymorphic resources A polymorphic endpoint can be set up if associated with a polymorphic serializer. From 89ad6075002c02849b5b17c0f4e24a18768a3367 Mon Sep 17 00:00:00 2001 From: Jamie Bliss <jamie.bliss@ilq.com> Date: Thu, 1 Jun 2017 11:43:58 -0400 Subject: [PATCH 31/31] Editing --- CHANGELOG.md | 2 +- docs/usage.md | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d25ec1..b78f0110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ v2.3.0 -* Added support for polymorphic models via `django-polymorphic` +* 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. diff --git a/docs/usage.md b/docs/usage.md index a04a1b2b..f4faeea8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -427,12 +427,15 @@ field_name_mapping = { ### Working with polymorphic resources -Polymorphic resources (as backed by [django-polymorphic](https://django-polymorphic.readthedocs.io/en/stable/)) -allow you to easily use specialized subclasses without have to have special -endpoints to expose the specialized versions. For example, if you had a +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.