diff --git a/CHANGELOG.md b/CHANGELOG.md index e4bd3686..0e483e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ any parts of the framework not mentioned in the documentation should generally b * Removed support for Django 4.2. * Removed support for Django 5.1. +### Removed + +* Removed support for exposing Polymorphic resources through [django-polymorphic](https://github.com/django-commons/django-polymorphic) as JSON:API endpoints. + ## [8.1.0] - 2025-10-17 This is the last release supporting Python 3.9, Django 4.2, Django 5.1, Django REST framework 3.15 and Django REST framework 3.16. @@ -39,7 +43,7 @@ This is the last release supporting Python 3.9, Django 4.2, Django 5.1, Django R ### Deprecated -* Deprecated support for exposing Polymorphic resources through [django-polymorphic](https://github.com/jazzband/django-polymorphic) as JSON:API endpoints. +* Deprecated support for exposing Polymorphic resources through [django-polymorphic](https://github.com/django-commons/django-polymorphic) as JSON:API endpoints. There is currently no replacement. In case you are affected of this change please join [our discussion](https://github.com/django-json-api/django-rest-framework-json-api/discussions/1194). diff --git a/README.rst b/README.rst index c59d84fe..acaf0724 100644 --- a/README.rst +++ b/README.rst @@ -113,7 +113,6 @@ Install using ``pip``... $ pip install djangorestframework-jsonapi $ # for optional package integrations $ pip install djangorestframework-jsonapi['django-filter'] - $ pip install djangorestframework-jsonapi['django-polymorphic'] or from source... diff --git a/docs/getting-started.md b/docs/getting-started.md index 2d02c85b..6d0f81f6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -68,7 +68,6 @@ Install using `pip`... pip install djangorestframework-jsonapi # for optional package integrations pip install djangorestframework-jsonapi['django-filter'] - pip install djangorestframework-jsonapi['django-polymorphic'] or from source... diff --git a/docs/usage.md b/docs/usage.md index 6f6ba6af..1c429365 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -874,74 +874,6 @@ field_name_mapping = { } ``` -### Working with polymorphic resources - --- - -**Deprecation notice:** - - -Support for using Polymorphic resources through [django-polymorphic](https://github.com/jazzband/django-polymorphic) is deprecated and will be removed in a future release. - -There is currently no replacement. In case you are affected of this change please join [our discussion](https://github.com/django-json-api/django-rest-framework-json-api/discussions/1194). - ---- - -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. - -As this feature depends on `django-polymorphic` you need to run - - pip install djangorestframework-jsonapi['django-polymorphic'] - -#### Writing polymorphic resources - -A polymorphic endpoint can be set up if associated with a polymorphic serializer. -A polymorphic serializer takes care of (de)serializing the correct instances types and can be defined like this: - -```python -class ProjectSerializer(serializers.PolymorphicModelSerializer): - polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer] - - class Meta: - model = models.Project -``` - -It must inherit from `serializers.PolymorphicModelSerializer` and define the `polymorphic_serializers` list. -This attribute defines the accepted resource types. - - -Polymorphic relations can also be handled with `relations.PolymorphicResourceRelatedField` like this: - -```python -class CompanySerializer(serializers.ModelSerializer): - current_project = relations.PolymorphicResourceRelatedField( - ProjectSerializer, queryset=models.Project.objects.all()) - future_projects = relations.PolymorphicResourceRelatedField( - ProjectSerializer, queryset=models.Project.objects.all(), many=True) - - class Meta: - model = models.Company -``` - -They must be explicitly declared with the `polymorphic_serializer` (first positional argument) correctly defined. -It must be a subclass of `serializers.PolymorphicModelSerializer`. - -
- Note: - Polymorphic resources are not compatible with - - resource_name - - defined on the view. -
- ### Meta You may add metadata to the rendered json in two different ways: `meta_fields` and `get_root_meta`. diff --git a/example/factories.py b/example/factories.py index 87698fa2..765bb024 100644 --- a/example/factories.py +++ b/example/factories.py @@ -2,18 +2,15 @@ from faker import Factory as FakerFactory from example.models import ( - ArtProject, Author, AuthorBio, AuthorBioMetadata, AuthorType, Blog, Comment, - Company, Entry, ProjectType, Questionnaire, - ResearchProject, TaggedItem, ) @@ -112,41 +109,6 @@ class Meta: name = factory.LazyAttribute(lambda x: faker.name()) -class ArtProjectFactory(factory.django.DjangoModelFactory): - class Meta: - model = ArtProject - - topic = factory.LazyAttribute(lambda x: faker.catch_phrase()) - artist = factory.LazyAttribute(lambda x: faker.name()) - project_type = factory.SubFactory(ProjectTypeFactory) - - -class ResearchProjectFactory(factory.django.DjangoModelFactory): - class Meta: - model = ResearchProject - - topic = factory.LazyAttribute(lambda x: faker.catch_phrase()) - supervisor = factory.LazyAttribute(lambda x: faker.name()) - project_type = factory.SubFactory(ProjectTypeFactory) - - -class CompanyFactory(factory.django.DjangoModelFactory): - class Meta: - model = Company - skip_postgeneration_save = True - - 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: # pragma: no cover - return - if extracted: - for project in extracted: - self.future_projects.add(project) - - class QuestionnaireFactory(factory.django.DjangoModelFactory): class Meta: model = Questionnaire diff --git a/example/migrations/0015_remove_company_current_project_and_more.py b/example/migrations/0015_remove_company_current_project_and_more.py new file mode 100644 index 00000000..07ed8ad8 --- /dev/null +++ b/example/migrations/0015_remove_company_current_project_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 6.0.7 on 2026-10-08 02:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("example", "0014_questionnaire_metadata"), + ] + + operations = [ + migrations.RemoveField( + model_name="company", + name="current_project", + ), + migrations.RemoveField( + model_name="company", + name="future_projects", + ), + migrations.RemoveField( + model_name="labresults", + name="author", + ), + migrations.RemoveField( + model_name="labresults", + name="research_project", + ), + migrations.RemoveField( + model_name="project", + name="polymorphic_ctype", + ), + migrations.RemoveField( + model_name="project", + name="project_type", + ), + migrations.RemoveField( + model_name="researchproject", + name="project_ptr", + ), + migrations.DeleteModel( + name="ArtProject", + ), + migrations.DeleteModel( + name="Company", + ), + migrations.DeleteModel( + name="LabResults", + ), + migrations.DeleteModel( + name="Project", + ), + migrations.DeleteModel( + name="ResearchProject", + ), + ] diff --git a/example/models.py b/example/models.py index 35a15a8e..feefb225 100644 --- a/example/models.py +++ b/example/models.py @@ -1,7 +1,6 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models -from polymorphic.models import PolymorphicModel class BaseModel(models.Model): @@ -139,49 +138,6 @@ class Meta: ordering = ("id",) -class Project(PolymorphicModel): - topic = models.CharField(max_length=30) - project_type = models.ForeignKey(ProjectType, null=True, on_delete=models.CASCADE) - - -class ArtProject(Project): - artist = models.CharField(max_length=30) - description = models.CharField(max_length=100, null=True) - - -class ResearchProject(Project): - supervisor = models.CharField(max_length=30) - - -class LabResults(models.Model): - research_project = models.ForeignKey( - ResearchProject, related_name="lab_results", on_delete=models.CASCADE - ) - date = models.DateField() - measurements = models.TextField() - author = models.ForeignKey( - Author, - null=True, - blank=True, - on_delete=models.CASCADE, - related_name="lab_results", - ) - - class Meta: - ordering = ("id",) - - -class Company(models.Model): - name = models.CharField(max_length=100) - current_project = models.ForeignKey( - Project, related_name="companies", on_delete=models.CASCADE - ) - future_projects = models.ManyToManyField(Project) - - def __str__(self): - return self.name - - class Questionnaire(models.Model): name = models.CharField(max_length=100) questions = models.JSONField() diff --git a/example/serializers.py b/example/serializers.py index 94fe8556..ecf45e5f 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,25 +1,19 @@ from datetime import datetime -from rest_framework import fields as drf_fields from rest_framework import serializers as drf_serilazers from rest_framework_json_api import relations, serializers from example.models import ( - ArtProject, Author, AuthorBio, AuthorBioMetadata, AuthorType, Blog, Comment, - Company, Entry, - LabResults, - Project, ProjectType, Questionnaire, - ResearchProject, TaggedItem, ) @@ -347,83 +341,6 @@ class BaseProjectSerializer(serializers.ModelSerializer): } -class ArtProjectSerializer(BaseProjectSerializer): - class Meta: - model = ArtProject - exclude = ("polymorphic_ctype",) - - -class ResearchProjectSerializer(BaseProjectSerializer): - # testing exclusive related field on inherited polymorphic model - lab_results = relations.ResourceRelatedField(many=True, read_only=True) - - class Meta: - model = ResearchProject - exclude = ("polymorphic_ctype",) - - -class LabResultsSerializer(serializers.ModelSerializer): - included_serializers = {"author": AuthorSerializer} - - class Meta: - model = LabResults - fields = ("date", "measurements", "author") - - -class ProjectSerializer(serializers.PolymorphicModelSerializer): - included_serializers = { - "project_type": ProjectTypeSerializer, - } - polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer] - - class Meta: - model = Project - exclude = ("polymorphic_ctype",) - - -class CurrentProjectRelatedField(relations.PolymorphicResourceRelatedField): - def get_attribute(self, instance): - obj = super().get_attribute(instance) - - is_art = self.field_name == "current_art_project" and isinstance( - obj, ArtProject - ) - is_res = self.field_name == "current_research_project" and isinstance( - obj, ResearchProject - ) - - if is_art or is_res: - return obj - - raise drf_fields.SkipField() - - -class CompanySerializer(serializers.ModelSerializer): - current_project = relations.PolymorphicResourceRelatedField( - ProjectSerializer, queryset=Project.objects.all() - ) - current_art_project = CurrentProjectRelatedField( - ProjectSerializer, source="current_project", read_only=True - ) - current_research_project = CurrentProjectRelatedField( - ProjectSerializer, source="current_project", read_only=True - ) - future_projects = relations.PolymorphicResourceRelatedField( - ProjectSerializer, queryset=Project.objects.all(), many=True - ) - - included_serializers = { - "current_project": ProjectSerializer, - "future_projects": ProjectSerializer, - "current_art_project": ProjectSerializer, - "current_research_project": ProjectSerializer, - } - - class Meta: - model = Company - fields = "__all__" - - class QuestionSerializer(serializers.Serializer): text = serializers.CharField() required = serializers.BooleanField(default=False) diff --git a/example/settings/dev.py b/example/settings/dev.py index 05cab4d1..0eecccdb 100644 --- a/example/settings/dev.py +++ b/example/settings/dev.py @@ -25,7 +25,6 @@ "django.contrib.auth", "rest_framework_json_api", "rest_framework", - "polymorphic", "example", "django_filters", "tests", diff --git a/example/tests/conftest.py b/example/tests/conftest.py index 6e4b05ba..659b8e0f 100644 --- a/example/tests/conftest.py +++ b/example/tests/conftest.py @@ -3,17 +3,14 @@ from rest_framework.test import APIClient from example.factories import ( - ArtProjectFactory, AuthorBioFactory, AuthorBioMetadataFactory, AuthorFactory, AuthorTypeFactory, BlogFactory, CommentFactory, - CompanyFactory, EntryFactory, QuestionnaireFactory, - ResearchProjectFactory, TaggedItemFactory, ) @@ -25,9 +22,6 @@ register(EntryFactory) register(CommentFactory) register(TaggedItemFactory) -register(ArtProjectFactory) -register(ResearchProjectFactory) -register(CompanyFactory) register(QuestionnaireFactory) diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py deleted file mode 100644 index 836a9046..00000000 --- a/example/tests/integration/test_polymorphism.py +++ /dev/null @@ -1,268 +0,0 @@ -import random - -import pytest -from django.urls import reverse -from rest_framework import status - -from example.factories import ArtProjectFactory, ProjectTypeFactory - -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 = response.json() - 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 = response.json() - 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,current_art_project,current_research_project" # noqa: B950 - ) - content = response.json() - assert ( - content["data"]["relationships"]["currentProject"]["data"]["type"] - == "artProjects" - ) - assert ( - content["data"]["relationships"]["currentArtProject"]["data"]["type"] - == "artProjects" - ) - assert content["data"]["relationships"]["currentResearchProject"]["data"] is None - assert { - rel["type"] - for rel in content["data"]["relationships"]["futureProjects"]["data"] - } == {"researchProjects", "artProjects"} - assert {x.get("type") for x in content.get("included")} == { - "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 = response.json() - test_topic = f"test-{random.randint(0, 999999)}" - test_artist = f"test-{random.randint(0, 999999)}" - content["data"]["attributes"]["topic"] = test_topic - content["data"]["attributes"]["artist"] = test_artist - response = client.patch(url, data=content) - new_content = response.json() - assert new_content["data"]["type"] == "artProjects" - assert new_content["data"]["attributes"]["topic"] == test_topic - assert new_content["data"]["attributes"]["artist"] == test_artist - - -def test_patch_on_polymorphic_model_without_including_required_field( - single_art_project, client -): - url = reverse("project-detail", kwargs={"pk": single_art_project.pk}) - data = { - "data": { - "id": single_art_project.pk, - "type": "artProjects", - "attributes": {"description": "New description"}, - } - } - response = client.patch(url, data) - assert response.status_code == status.HTTP_200_OK - assert response.json()["data"]["attributes"]["description"] == "New description" - - -def test_polymorphism_on_polymorphic_model_list_post(client): - test_topic = f"New test topic {random.randint(0, 999999)}" - test_artist = f"test-{random.randint(0, 999999)}" - test_project_type = ProjectTypeFactory() - url = reverse("project-list") - data = { - "data": { - "type": "artProjects", - "attributes": {"topic": test_topic, "artist": test_artist}, - "relationships": { - "projectType": { - "data": {"type": "projectTypes", "id": test_project_type.pk} - } - }, - } - } - response = client.post(url, data=data) - content = response.json() - 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 - assert content["data"]["relationships"]["projectType"]["data"]["id"] == str( - test_project_type.pk - ) - - -def test_polymorphism_on_polymorphic_model_w_included_serializers(client): - test_project = ArtProjectFactory() - query = "?include=projectType" - url = reverse("project-list") - response = client.get(url + query) - content = response.json() - assert content["data"][0]["id"] == str(test_project.pk) - assert content["data"][0]["type"] == "artProjects" - assert content["data"][0]["relationships"]["projectType"]["data"]["id"] == str( - test_project.project_type.pk - ) - assert content["included"][0]["type"] == "projectTypes" - assert content["included"][0]["id"] == str(test_project.project_type.pk) - - -def test_polymorphic_model_without_any_instance(client): - expected = { - "links": { - "first": "http://testserver/projects?page%5Bnumber%5D=1", - "last": "http://testserver/projects?page%5Bnumber%5D=1", - "next": None, - "prev": None, - }, - "data": [], - "meta": {"pagination": {"page": 1, "pages": 1, "count": 0}}, - } - - response = client.get(reverse("project-list")) - assert response.status_code == 200 - content = response.json() - assert expected == content - - -def test_invalid_type_on_polymorphic_model(client): - test_topic = f"New test topic {random.randint(0, 999999)}" - test_artist = f"test-{random.randint(0, 999999)}" - url = reverse("project-list") - data = { - "data": { - "type": "invalidProjects", - "attributes": {"topic": test_topic, "artist": test_artist}, - } - } - response = client.post(url, data=data) - assert response.status_code == 409 - content = response.json() - assert len(content["errors"]) == 1 - assert content["errors"][0]["status"] == "409" - try: - assert ( - content["errors"][0]["detail"] - == "The resource object's type (invalidProjects) is not the type that constitute the " # noqa: B950 - "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 " # noqa: B950 - "collection represented by the endpoint (one of [artProjects, researchProjects])." - ) - - -def test_polymorphism_relations_update( - single_company, research_project_factory, client -): - response = client.get(reverse("company-detail", kwargs={"pk": single_company.pk})) - content = response.json() - 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.patch( - reverse("company-detail", kwargs={"pk": single_company.pk}), data=content - ) - assert response.status_code == 200 - content = response.json() - assert ( - content["data"]["relationships"]["currentProject"]["data"]["type"] - == "researchProjects" - ) - assert ( - int(content["data"]["relationships"]["currentProject"]["data"]["id"]) - == research_project.pk - ) - - -def test_polymorphism_relations_put_405( - single_company, research_project_factory, client -): - response = client.get(reverse("company-detail", kwargs={"pk": single_company.pk})) - content = response.json() - 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=content - ) - assert response.status_code == 405 - - -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 = response.json() - 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.patch( - reverse("company-detail", kwargs={"pk": single_company.pk}), data=content - ) - assert response.status_code == 409 - content = response.json() - assert len(content["errors"]) == 1 - assert content["errors"][0]["status"] == "409" - 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." - ) diff --git a/example/tests/test_performance.py b/example/tests/test_performance.py index ec4a81b2..cae11ed4 100644 --- a/example/tests/test_performance.py +++ b/example/tests/test_performance.py @@ -1,11 +1,8 @@ -from datetime import date, timedelta -from random import randint - from django.utils import timezone from rest_framework.test import APITestCase from example.factories import CommentFactory, EntryFactory -from example.models import Author, Blog, Comment, Entry, LabResults, ResearchProject +from example.models import Author, Blog, Comment, Entry class PerformanceTestCase(APITestCase): @@ -87,32 +84,3 @@ def test_query_prefetch_uses_included_resources(self): "/entries?fields[entries]=comments&page[size]=25" ) self.assertEqual(len(response.data["results"]), 25) - - def test_query_prefetch_read_only(self): - """We expect a read only list view with an include have five queries: - - 1. Primary resource COUNT query - 2. Primary resource SELECT - 3. Authors prefetched - 4. Author types prefetched - 5. Entries prefetched - """ - project = ResearchProject.objects.create( - topic="Mars Mission", supervisor="Elon Musk" - ) - - LabResults.objects.bulk_create( - [ - LabResults( - research_project=project, - date=date.today() + timedelta(days=i), - measurements=randint(0, 10000), - author=self.author, - ) - for i in range(20) - ] - ) - - with self.assertNumQueries(5): - response = self.client.get("/lab-results?include=author&page[size]=25") - self.assertEqual(len(response.data["results"]), 20) diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py index 9ad487e5..c9e9d093 100644 --- a/example/tests/test_serializers.py +++ b/example/tests/test_serializers.py @@ -12,13 +12,11 @@ DateField, ModelSerializer, ResourceIdentifierObjectSerializer, - empty, ) from rest_framework_json_api.utils import format_resource_type -from example.factories import ArtProjectFactory from example.models import Author, Blog, Entry -from example.serializers import ArtProjectSerializer, BlogSerializer, ProjectSerializer +from example.serializers import BlogSerializer request_factory = APIRequestFactory() pytestmark = pytest.mark.django_db @@ -256,51 +254,3 @@ def test_model_serializer_with_subserializers(self, questionnaire, client): assert response.status_code == 200 assert expected == response.json() - - -class TestPolymorphicModelSerializer(TestCase): - def setUp(self): - self.project = ArtProjectFactory.create() - self.child_init_args = {} - - # Override `__init__` with our own method - def overridden_init(child_self, instance=None, data=empty, **kwargs): - """ - Override `ArtProjectSerializer.__init__` with the same signature that - `BaseSerializer.__init__` has to assert that it receives the parameters - that `BaseSerializer` expects - """ - self.child_init_args = dict(instance=instance, data=data, **kwargs) - - return super(ArtProjectSerializer, child_self).__init__( - instance, data, **kwargs - ) - - self.child_serializer_init = ArtProjectSerializer.__init__ - ArtProjectSerializer.__init__ = overridden_init - - def tearDown(self): - # Restore original init to avoid affecting other tests - ArtProjectSerializer.__init__ = self.child_serializer_init - - def test_polymorphic_model_serializer_passes_instance_to_child(self): - """ - Ensure that `PolymorphicModelSerializer` is passing the instance to the - child serializer when initializing them - """ - # Initialize a serializer that would partially update a model instance - initial_data = {"artist": "Mark Bishop", "type": "artProjects"} - parent_serializer = ProjectSerializer( - instance=self.project, data=initial_data, partial=True - ) - - parent_serializer.is_valid(raise_exception=True) - - # Run save to force `ProjectSerializer` to init `ArtProjectSerializer` - parent_serializer.save() - - # Assert that child init received the expected arguments - assert self.child_init_args["instance"] == self.project - assert self.child_init_args["data"] == initial_data - assert self.child_init_args["partial"] == parent_serializer.partial - assert self.child_init_args["context"] == parent_serializer.context diff --git a/example/urls.py b/example/urls.py index 471fbe81..b77ba0a2 100644 --- a/example/urls.py +++ b/example/urls.py @@ -8,13 +8,10 @@ BlogViewSet, CommentRelationshipView, CommentViewSet, - CompanyViewset, EntryRelationshipView, EntryViewSet, - LabResultViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset, QuestionnaireViewset, ) @@ -25,10 +22,7 @@ router.register(r"nopage-entries", NonPaginatedEntryViewSet, "nopage-entry") router.register(r"authors", AuthorViewSet) router.register(r"comments", CommentViewSet) -router.register(r"companies", CompanyViewset) -router.register(r"projects", ProjectViewset) router.register(r"project-types", ProjectTypeViewset) -router.register(r"lab-results", LabResultViewSet) router.register(r"questionnaires", QuestionnaireViewset) urlpatterns = [ diff --git a/example/urls_test.py b/example/urls_test.py index 26d4db21..ae62bf55 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -9,17 +9,14 @@ BlogViewSet, CommentRelationshipView, CommentViewSet, - CompanyViewset, DRFBlogViewSet, DRFEntryViewSet, EntryRelationshipView, EntryViewSet, FiltersetEntryViewSet, - LabResultViewSet, NoFiltersetEntryViewSet, NonPaginatedEntryViewSet, ProjectTypeViewset, - ProjectViewset, QuestionnaireViewset, ) @@ -35,10 +32,7 @@ router.register(r"nofilterset-entries", NoFiltersetEntryViewSet, "nofilterset-entry") router.register(r"authors", AuthorViewSet) router.register(r"comments", CommentViewSet) -router.register(r"companies", CompanyViewset) -router.register(r"projects", ProjectViewset) router.register(r"project-types", ProjectTypeViewset) -router.register(r"lab-results", LabResultViewSet) router.register(r"questionnaires", QuestionnaireViewset) # for the old tests diff --git a/example/views.py b/example/views.py index da171698..94ebca6b 100644 --- a/example/views.py +++ b/example/views.py @@ -14,23 +14,9 @@ ) from rest_framework_json_api.pagination import JsonApiPageNumberPagination from rest_framework_json_api.utils import format_drf_errors -from rest_framework_json_api.views import ( - ModelViewSet, - ReadOnlyModelViewSet, - RelationshipView, -) +from rest_framework_json_api.views import ModelViewSet, RelationshipView -from example.models import ( - Author, - Blog, - Comment, - Company, - Entry, - LabResults, - Project, - ProjectType, - Questionnaire, -) +from example.models import Author, Blog, Comment, Entry, ProjectType, Questionnaire from example.serializers import ( AuthorDetailSerializer, AuthorListSerializer, @@ -38,11 +24,8 @@ BlogDRFSerializer, BlogSerializer, CommentSerializer, - CompanySerializer, EntryDRFSerializers, EntrySerializer, - LabResultsSerializer, - ProjectSerializer, ProjectTypeSerializer, QuestionnaireSerializer, ) @@ -258,16 +241,6 @@ def get_queryset(self, *args, **kwargs): return queryset -class CompanyViewset(ModelViewSet): - queryset = Company.objects.all() - serializer_class = CompanySerializer - - -class ProjectViewset(ModelViewSet): - queryset = Project.objects.all().order_by("pk") - serializer_class = ProjectSerializer - - class ProjectTypeViewset(ModelViewSet): queryset = ProjectType.objects.all() serializer_class = ProjectTypeSerializer @@ -290,15 +263,6 @@ class AuthorRelationshipView(RelationshipView): self_link_view_name = "author-relationships" -class LabResultViewSet(ReadOnlyModelViewSet): - queryset = LabResults.objects.all() - serializer_class = LabResultsSerializer - prefetch_for_includes = { - "__all__": [], - "author": ["author__bio", "author__entries"], - } - - class QuestionnaireViewset(ModelViewSet): queryset = Questionnaire.objects.all() serializer_class = QuestionnaireSerializer diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index e38f445f..c2c7eb1e 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,2 +1 @@ django-filter==26.1 -django-polymorphic==4.11.7 diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 8940c653..30ec85bf 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -116,9 +116,7 @@ def parse_data(self, result, parser_context): # Check for inconsistencies if method in ("PUT", "POST", "PATCH"): - resource_name = get_resource_name( - parser_context, expand_polymorphic_types=True - ) + resource_name = get_resource_name(parser_context) if isinstance(resource_name, str): if data.get("type") != resource_name: raise exceptions.Conflict( diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 0742c1ec..244d4e19 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -169,7 +169,6 @@ def many_init(cls, *args, **kwargs): class ResourceRelatedField(HyperlinkedMixin, PrimaryKeyRelatedField): - _skip_polymorphic_optimization = True self_link_view_name = None related_link_view_name = None related_link_lookup_field = "pk" @@ -249,7 +248,7 @@ def to_internal_value(self, data): def to_representation(self, value): pk = self.get_resource_id(value) resource_type = self.get_resource_type_from_included_serializer() - if resource_type is None or not self._skip_polymorphic_optimization: + if resource_type is None: resource_type = get_resource_type_from_instance(value) return {"type": resource_type, "id": str(pk)} @@ -310,59 +309,6 @@ 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, - **{ - "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().__init__(*args, **kwargs) - - def use_pk_only_optimization(self): - return False - - def to_internal_value(self, data): - if isinstance(data, str): - 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 = self.polymorphic_serializer.get_polymorphic_types() - - 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 SerializerMethodFieldBase(Field): def __init__(self, method_name=None, **kwargs): self.method_name = method_name diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index f418b080..e49484f2 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -1,7 +1,3 @@ -""" -Renderers -""" - import copy from collections import defaultdict from collections.abc import Iterable @@ -15,7 +11,6 @@ from rest_framework.serializers import ListSerializer, Serializer from rest_framework.settings import api_settings -import rest_framework_json_api from rest_framework_json_api.relations import ( HyperlinkedMixin, ManySerializerMethodResourceRelatedField, @@ -357,7 +352,6 @@ def extract_included( nested_resource_instance, resource_type, serializer, - getattr(serializer, "_poly_force_type_resolution", False), ) included_cache[new_item["type"]][new_item["id"]] = new_item @@ -381,7 +375,6 @@ def extract_included( relation_instance, relation_type, field, - getattr(field, "_poly_force_type_resolution", False), ) included_cache[new_item["type"]][new_item["id"]] = new_item @@ -459,14 +452,8 @@ def build_json_resource_obj( resource_instance, resource_name, serializer, - force_type_resolution=False, ): - """ - Builds the resource object (type, id, attributes) and extracts relationships. - """ - # Determine type from the instance if the underlying model is polymorphic - if force_type_resolution: - resource_name = get_resource_type_from_instance(resource_instance) + """Builds the resource object (type, id, attributes) and extracts relationships.""" resource_data = { "type": resource_name, "id": get_resource_id(resource_instance, resource), @@ -567,22 +554,8 @@ def render(self, data, accepted_media_type=None, renderer_context=None): position ] # Get current instance - if isinstance( - serializer.child, - rest_framework_json_api.serializers.PolymorphicModelSerializer, - ): - resource_serializer_class = ( - serializer.child.get_polymorphic_serializer_for_instance( - resource_instance - )(context=serializer.child.context) - ) - else: - resource_serializer_class = serializer.child - + resource_serializer_class = serializer.child fields = get_serializer_fields(resource_serializer_class) - force_type_resolution = getattr( - resource_serializer_class, "_poly_force_type_resolution", False - ) json_resource_obj = self.build_json_resource_obj( fields, @@ -590,7 +563,6 @@ def render(self, data, accepted_media_type=None, renderer_context=None): resource_instance, resource_name, serializer, - force_type_resolution, ) json_api_data.append(json_resource_obj) @@ -603,9 +575,6 @@ def render(self, data, accepted_media_type=None, renderer_context=None): ) else: fields = get_serializer_fields(serializer) - force_type_resolution = getattr( - serializer, "_poly_force_type_resolution", False - ) resource_instance = serializer.instance json_api_data = self.build_json_resource_obj( @@ -614,7 +583,6 @@ def render(self, data, accepted_media_type=None, renderer_context=None): resource_instance, resource_name, serializer, - force_type_resolution, ) self.extract_included( diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 5583d3b9..ea786e31 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -1,8 +1,6 @@ -import warnings from collections.abc import Mapping from django.core.exceptions import ObjectDoesNotExist -from django.db.models.query import QuerySet from django.utils.module_loading import import_string as import_class_from_dotted_path from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import ParseError @@ -20,7 +18,6 @@ ) from rest_framework.settings import api_settings -from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.relations import ResourceRelatedField from rest_framework_json_api.utils import ( get_included_resources, @@ -326,175 +323,3 @@ def get_field_names(self, declared_fields, info): } fields = super().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().__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 - - warnings.warn( - DeprecationWarning( - "Support for polymorphic resources is deprecated." - "There is currently no replacement. In case you are affected of this " - "change please join discussion at " - "https://github.com/django-json-api/django-rest-framework-json-api/discussions/1194." - ), - stacklevel=2, - ) - - polymorphic_serializers = getattr(new_class, "polymorphic_serializers", None) - assert ( - polymorphic_serializers is not None - ), "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_serializer = { - get_resource_type_from_serializer(serializer): serializer - for serializer in polymorphic_serializers - } - 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: - serializer._poly_force_type_resolution = True - - return new_class - - -class PolymorphicModelSerializer( - ModelSerializer, metaclass=PolymorphicSerializerMetaclass -): - """ - 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 not in (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().get_fields() - - @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 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__ - ) - ) - - @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 cls._poly_serializer_model_map[serializer] - except KeyError: - raise NotImplementedError( - "No polymorphic model has been found for serializer {}".format( - serializer.__name__ - ) - ) - - @classmethod - def get_polymorphic_serializer_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 - means that a serializer is missing in the class's `polymorphic_serializers` attribute. - """ - try: - return cls._poly_type_serializer_map[obj_type] - except KeyError: - raise NotImplementedError( - f"No polymorphic serializer has been found for type {obj_type}" - ) - - @classmethod - def get_polymorphic_model_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 - means that a serializer is missing in the class's `polymorphic_serializers` attribute. - """ - 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): - """ - 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.get_polymorphic_types() - 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( - self.instance, data, context=self.context, partial=self.partial - ).to_internal_value(data) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 2dd79677..ac57fdd1 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -25,11 +25,10 @@ from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor -def get_resource_name(context, expand_polymorphic_types=False): +def get_resource_name(context): """ Return the name of a resource. """ - from rest_framework_json_api.serializers import PolymorphicModelSerializer view = context.get("view") @@ -55,12 +54,7 @@ def get_resource_name(context, expand_polymorphic_types=False): serializer = view.get_related_serializer_class() else: serializer = view.get_serializer_class() - if expand_polymorphic_types and issubclass( - serializer, PolymorphicModelSerializer - ): - return serializer.get_polymorphic_types() - else: - return get_resource_type_from_serializer(serializer) + return get_resource_type_from_serializer(serializer) except AttributeError: try: resource_name = get_resource_type_from_model(view.model) @@ -193,8 +187,6 @@ def format_resource_type(value, format_type=None, pluralize=None): def get_related_resource_type(relation): - from rest_framework_json_api.serializers import PolymorphicModelSerializer - try: return get_resource_type_from_serializer(relation) except AttributeError: @@ -218,11 +210,7 @@ def get_related_resource_type(relation): if not relation_model: parent_serializer = relation.parent parent_model = None - if isinstance(parent_serializer, PolymorphicModelSerializer): - parent_model = parent_serializer.get_polymorphic_serializer_for_instance( - parent_serializer.instance - ).Meta.model - elif hasattr(parent_serializer, "Meta"): + if hasattr(parent_serializer, "Meta"): parent_model = getattr(parent_serializer.Meta, "model", None) elif hasattr(parent_serializer, "parent") and hasattr( parent_serializer.parent, "Meta" diff --git a/setup.cfg b/setup.cfg index 05c3bd26..92606700 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,6 @@ DJANGO_SETTINGS_MODULE=example.settings.test filterwarnings = error::DeprecationWarning error::PendingDeprecationWarning - ignore:Support for polymorphic resources is deprecated testpaths = example tests diff --git a/setup.py b/setup.py index 68e91167..e12f2455 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,6 @@ def get_package_data(package): "django>=5.2", ], extras_require={ - "django-polymorphic": ["django-polymorphic>=4.0.0"], "django-filter": ["django-filter>=2.4"], }, setup_requires=wheel,