integrate django-rest-polymorphic

This commit is contained in:
Brian Kohan 2026-01-13 00:38:00 -08:00
parent 2ca4926a52
commit 6150696b6c
No known key found for this signature in database
GPG Key ID: 5C6CE8BA38C43FC1
61 changed files with 860 additions and 1150 deletions

View File

@ -72,3 +72,16 @@
* Diederik van der Boor
* Charlie Denton
* Jerome Leclanche
## django-rest-framework
### Development Lead
* Denis Orehovsky <denis.orehovsky@gmail.com>
### Contributors
* Jeff Hackshaw <jeffrey.hackshaw@gmail.com>
* TFranzel
* Ignacio Losiggio <iglosiggio@gmail.com>

View File

@ -0,0 +1,7 @@
drf.serializers
===============
.. automodule:: polymorphic.contrib.drf.serializers
:members:
:undoc-members:
:show-inheritance:

View File

@ -12,3 +12,4 @@ polymorphic.contrib
extra_views
guardian
drf

View File

@ -1,10 +1,14 @@
.. :changelog:
.. :drf_changelog:
drf
---
Below is the changelog for the :pypi:`django-rest-polymorphic` before it was included into
:pypi:`django-polymorphic`.
History
-------
0.1.10 (2022-07-17)
++++++++++++++++++
+++++++++++++++++++
* Allow partial updates without resourcetype.

View File

@ -4,6 +4,12 @@ Changelog
.. v4.10.0 (2026-XX-XX)
.. --------------------
.. * Implemented `Bring django-rest-polymorphic into this package. <https://github.com/jazzband/django-polymorphic/issues/655>`_
.. :pypi:`django-rest-polymorphic` is now part of :pypi:`django-polymorphic`. You must update
.. your import paths from ``rest_polymorphic.serializers`` to
.. ``polymorphic.contrib.drf.serializers``.
.. * Implemented `System check error for PolymorphicManager marked as use_in_migrations <https://github.com/jazzband/django-polymorphic/issues/820>`_
.. * Implemented `Manager warnings should be system checks instead. <https://github.com/jazzband/django-polymorphic/issues/819>`_
@ -936,3 +942,9 @@ This should be the final DB schema now.
Django's ContentType is now used instead of app-label and model-name
This is a cleaner and more efficient solution
Thanks to Ilya Semenov for the suggestion.
.. toctree::
drf

View File

@ -108,7 +108,7 @@ Getting started
quickstart
admin
performance
integrations
integrations/index
Advanced topics
---------------
@ -122,7 +122,7 @@ Advanced topics
managers
deletion
advanced
changelog
changelog/index
api/index

View File

@ -0,0 +1,129 @@
.. _django-rest-framework-support:
===================
djangorestframework
===================
Polymorphic serializers for `Django REST Framework <https://www.django-rest-framework.org/>`_.
The :pypi:`django-rest-polymorphic` package has been incorporated into :pypi:`django-polymorphic`.
This contrib package allows you to easily define serializers for your inherited models that you have
created using ``django-polymorphic`` library. To migrate from :pypi:`django-rest-polymorphic`, you
need to change your import paths from ``rest_polymorphic.serializers`` to
``polymorphic.contrib.drf.serializers``.
Usage
-----
Define your polymorphic models:
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/models/example_models.py
:language: python
:linenos:
Define serializers for each polymorphic model the way you did it when you used
:pypi:`djangorestframework`:
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/example_serializers.py
:language: python
:linenos:
:lines: 1-26
Note that if you extend `HyperlinkedModelSerializer
<https://www.django-rest-framework.org/api-guide/serializers/#hyperlinkedmodelserializer>`_ instead
of `ModelSerializer <https://www.django-rest-framework.org/api-guide/serializers/#modelserializer>`_
you need to define `extra_kwargs
<https://www.django-rest-framework.org/community/3.0-announcement/#the-extra_kwargs-option>`_ to
direct the URL to the appropriate view for your polymorphic serializer.
Then you have to create a polymorphic serializer that serves as a mapper between models and
serializers which you have defined above:
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/example_serializers.py
:language: python
:lines: 29-
Create viewset with serializer_class equals to your polymorphic serializer:
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/drf/views.py
:language: python
:linenos:
Test it:
.. code-block:: bash
$ http GET "http://localhost:8000/projects/"
.. code-block:: http
HTTP/1.0 200 OK
Content-Length: 227
Content-Type: application/json
[
{
"resourcetype": "Project",
"topic": "John's gathering"
},
{
"artist": "T. Turner",
"resourcetype": "ArtProject",
"topic": "Sculpting with Tim",
"url": "http://localhost:8000/projects/2/"
},
{
"resourcetype": "ResearchProject",
"supervisor": "Dr. Winter",
"topic": "Swallow Aerodynamics"
}
]
.. code-block:: bash
$ http POST "http://localhost:8000/projects/" resourcetype="ArtProject" topic="Guernica" artist="Picasso"
.. code-block:: http
HTTP/1.0 201 Created
Content-Length: 67
Content-Type: application/json
{
"artist": "Picasso",
"resourcetype": "ArtProject",
"topic": "Guernica",
"url": "http://localhost:8000/projects/4/"
}
Customize resource type
-----------------------
As you can see from the example above, in order to specify the type of your polymorphic model, you need to send a request with resource type field. The value of resource type should be the name of the model.
If you want to change the resource type field name from ``resourcetype`` to something else, you should override ``resource_type_field_name`` attribute:
.. code-block:: python
class ProjectPolymorphicSerializer(PolymorphicSerializer):
resource_type_field_name = 'projecttype'
...
If you want to change the behavior of resource type, you should override ``to_resource_type`` method:
.. code-block:: python
class ProjectPolymorphicSerializer(PolymorphicSerializer):
...
def to_resource_type(self, model_or_instance):
return model_or_instance._meta.object_name.lower()
Now, the request for creating new object will look like this:
.. code-block:: bash
$ http POST "http://localhost:8000/projects/" projecttype="artproject" topic="Guernica" artist="Picasso"

View File

@ -22,7 +22,7 @@ refer to `our integrations discussion page
For the integration examples on this page, we use the following polymorphic model hierarchy:
.. literalinclude:: ../src/polymorphic/tests/examples/integrations/models.py
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/models.py
:language: python
:linenos:
@ -45,67 +45,6 @@ This option requires :pypi:`django-guardian` >= 1.4.6. Details about how this op
available in the `django-guardian documentation
<https://django-guardian.readthedocs.io/en/stable/configuration>`_.
.. _django-rest-framework-support:
djangorestframework
-------------------
The :pypi:`django-rest-polymorphic` package provides polymorphic serializers that help you integrate
your polymorphic models with :pypi:`djangorestframework`.
Example
~~~~~~~
Define serializers:
.. code-block:: python
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from .models import Project, ArtProject, ResearchProject
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('topic', )
class ArtProjectSerializer(serializers.ModelSerializer):
class Meta:
model = ArtProject
fields = ('topic', 'artist')
class ResearchProjectSerializer(serializers.ModelSerializer):
class Meta:
model = ResearchProject
fields = ('topic', 'supervisor')
class ProjectPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
Project: ProjectSerializer,
ArtProject: ArtProjectSerializer,
ResearchProject: ResearchProjectSerializer
}
Create viewset with serializer_class equals to your polymorphic serializer:
.. code-block:: python
from rest_framework import viewsets
from .models import Project
from .serializers import ProjectPolymorphicSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectPolymorphicSerializer
.. _django-extra-views-support:
django-extra-views
@ -132,7 +71,7 @@ Example View
Here's how to create a view using :class:`~polymorphic.contrib.extra_views.PolymorphicFormSetView`
to handle polymorphic formsets:
.. literalinclude:: ../src/polymorphic/tests/examples/integrations/extra_views/views.py
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/views.py
:language: python
:linenos:
@ -142,7 +81,7 @@ URL Configuration
Configure the URL patterns to route to your formset view:
.. literalinclude:: ../src/polymorphic/tests/examples/integrations/extra_views/urls.py
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/urls.py
:language: python
:linenos:
@ -152,12 +91,12 @@ Template
The template for rendering the formset:
.. literalinclude:: ../src/polymorphic/tests/examples/integrations/extra_views/templates/extra_views/article_formset.html
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/templates/extra_views/article_formset.html
:language: html+django
``model_name`` is a template tag implemented like so:
.. literalinclude:: ../src/polymorphic/tests/examples/integrations/extra_views/templatetags/extra_views_tags.py
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/extra_views/templatetags/extra_views_tags.py
:language: python
:lines: 6-
@ -187,7 +126,7 @@ The admin configuration combines :class:`~polymorphic.admin.PolymorphicParentMod
:class:`~polymorphic.admin.PolymorphicChildModelAdmin` with
:ref:`VersionAdmin <django-reversion:versionadmin>`:
.. literalinclude:: ../src/polymorphic/tests/examples/integrations/reversion/admin.py
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/reversion/admin.py
:language: python
:linenos:
@ -199,9 +138,15 @@ Since both :class:`~polymorphic.admin.PolymorphicParentModelAdmin` and
:ref:`VersionAdmin <django-reversion:versionadmin>`. define ``object_history.html`` template, you
need to create a custom template that combines both:
.. literalinclude:: ../src/polymorphic/tests/examples/integrations/reversion/templates/admin/polymorphic/object_history.html
.. literalinclude:: ../../src/polymorphic/tests/examples/integrations/reversion/templates/admin/polymorphic/object_history.html
:language: html+django
This makes sure both the reversion template is used, and the breadcrumb is corrected for the
polymorphic model using the :templatetag:`breadcrumb_scope`
tag.
.. toctree::
djangorestframework

View File

@ -105,7 +105,7 @@ remake-test-migrations:
- rm src/polymorphic/tests/other/migrations/00*.py
- rm src/polymorphic/tests/examples/**/migrations/00*.py
- rm src/polymorphic/tests/examples/integrations/**/migrations/00*.py
uv run --isolated --resolution lowest-direct --script ./manage.py makemigrations
uv run --exact --isolated --resolution lowest-direct --group reversion --group extra-views --group drf --script ./manage.py makemigrations
# open the html documentation
[script]
@ -193,27 +193,31 @@ test-lock +PACKAGES: _lock-python
# run tests
test *TESTS: install-playwright
@just run pytest {{ TESTS }} --cov
@just run --exact pytest {{ TESTS }} --cov
test-db DB_CLIENT="dev" *TESTS: install-playwright
# No Optional Dependency Unit Tests
# todo clean this up, rerunning a lot of tests
uv sync --group {{ DB_CLIENT }}
uv sync --exact --group {{ DB_CLIENT }}
@just run pytest {{ TESTS }} --cov
# run django-reversion integration tests
test-reversion *TESTS: install-playwright
uv sync --group reversion
uv sync --exact --group reversion
@just run pytest -m integration src/polymorphic/tests/examples/integrations/reversion {{ TESTS }}
test-extra-views *TESTS:
uv sync --group extra-views
@just run pytest -m integration src/polymorphic/tests/examples/integrations/extra_views {{ TESTS }}
test-drf *TESTS:
uv sync --group drf
@just run pytest -m integration src/polymorphic/tests/examples/integrations/drf {{ TESTS }}
# run all third party integration tests
test-integrations DB_CLIENT="dev": install-playwright
# Integration Tests
uv sync --group {{ DB_CLIENT }} --group reversion --group extra-views
uv sync --group {{ DB_CLIENT }} --group reversion --group extra-views --group drf
@just run pytest -m integration --cov --cov-append
# debug an test

View File

@ -8,7 +8,10 @@ version = "4.9.0"
description = "Seamless polymorphic inheritance for Django models."
readme = "README.md"
license = "BSD-3-Clause"
license-files = [ "LICENSE" ]
license-files = [
"LICENSE",
"src/polymorphic/contrib/drf/LICENSE"
]
requires-python = ">=3.10,<4.0"
repository = "https://github.com/jazzband/django-polymorphic"
homepage = "https://django-polymorphic.rtfd.io"
@ -154,6 +157,7 @@ dev = [
"pytest>=8.3.4",
"pytest-cov>=5.0.0",
"pytest-django>=4.10.0",
"pytest-mock>=3.15.1",
"pytest-playwright>=0.7.2",
"ruff>=0.9.8",
"tomlkit>=0.13.3",
@ -162,6 +166,7 @@ dev = [
]
docs = [
"django-extra-views>=0.16.0",
"djangorestframework>=3.16.1",
"doc8>=1.1.2",
"furo>=2025.7.19",
"readme-renderer[md]>=43.0",
@ -191,3 +196,6 @@ reversion = [
extra-views = [
"django-extra-views>=0.16.0",
]
drf = [
"djangorestframework>=3.16.1",
]

View File

@ -1,23 +0,0 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,rst,ini}]
indent_style = space
indent_size = 4
[*.{html,css,scss,json,yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

View File

@ -1,50 +0,0 @@
*.py[cod]
__pycache__
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
htmlcov
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Pycharm/Intellij
.idea
# Complexity
output/*.html
output/*/index.html
# Sphinx
docs/_build
# DB
db.sqlite3

View File

@ -1,94 +0,0 @@
sudo: false
language: python
cache: pip
matrix:
fast_finish: true
include:
- python: 3.6
env: TOXENV=py36-django22-drf38
- python: 3.6
env: TOXENV=py36-django22-drf39
- python: 3.6
env: TOXENV=py36-django22-drf310
- python: 3.6
env: TOXENV=py36-django22-drf311
- python: 3.6
env: TOXENV=py36-django30-drf310
- python: 3.6
env: TOXENV=py36-django30-drf311
- python: 3.6
env: TOXENV=py36-djangomaster-drfmaster
- python: 3.7
env: TOXENV=py37-django22-drf38
dist: xenial
sudo: required
- python: 3.7
env: TOXENV=py37-django22-drf39
dist: xenial
sudo: required
- python: 3.7
env: TOXENV=py37-django22-drf310
dist: xenial
sudo: required
- python: 3.7
env: TOXENV=py37-django22-drf311
dist: xenial
sudo: required
- python: 3.7
env: TOXENV=py37-django30-drf310
dist: xenial
sudo: required
- python: 3.7
env: TOXENV=py37-django30-drf311
dist: xenial
sudo: required
- python: 3.7
env: TOXENV=py37-djangomaster-drfmaster
dist: xenial
sudo: required
- python: 3.8
env: TOXENV=py38-django22-drf38
dist: xenial
sudo: required
- python: 3.8
env: TOXENV=py38-django22-drf39
dist: xenial
sudo: required
- python: 3.8
env: TOXENV=py38-django22-drf310
dist: xenial
sudo: required
- python: 3.8
env: TOXENV=py38-django22-drf311
dist: xenial
sudo: required
- python: 3.8
env: TOXENV=py38-django30-drf310
dist: xenial
sudo: required
- python: 3.8
env: TOXENV=py38-django30-drf311
dist: xenial
sudo: required
- python: 3.8
env: TOXENV=py38-djangomaster-drfmaster
dist: xenial
sudo: required
allow_failures:
- env: TOXENV=py36-djangomaster-drfmaster
- env: TOXENV=py37-djangomaster-drfmaster
- env: TOXENV=py38-djangomaster-drfmaster
install:
- travis_retry pip install -U tox-travis
script:
- tox
after_success:
- travis_retry pip install -U codecov
- codecov

View File

@ -1,15 +0,0 @@
=======
Credits
=======
Development Lead
----------------
* Denis Orehovsky <denis.orehovsky@gmail.com>
Contributors
------------
* Jeff Hackshaw <jeffrey.hackshaw@gmail.com>
* TFranzel
* Ignacio Losiggio <iglosiggio@gmail.com>

View File

@ -1,5 +0,0 @@
include AUTHORS.rst
include HISTORY.rst
include LICENSE
include README.rst
recursive-include rest_polymorphic *.html *.png *.gif *js *.css *jpg *jpeg *svg *py

View File

@ -1,192 +0,0 @@
.. image:: https://travis-ci.org/apirobot/django-rest-polymorphic.svg?branch=master
:target: https://travis-ci.org/apirobot/django-rest-polymorphic
.. image:: https://codecov.io/gh/apirobot/django-rest-polymorphic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/apirobot/django-rest-polymorphic
.. image:: https://badge.fury.io/py/django-rest-polymorphic.svg
:target: https://badge.fury.io/py/django-rest-polymorphic
=======================
Django REST Polymorphic
=======================
Polymorphic serializers for Django REST Framework.
Overview
--------
``django-rest-polymorphic`` allows you to easily define serializers for your inherited models that you have created using ``django-polymorphic`` library.
Installation
------------
Install using ``pip``:
.. code-block:: bash
$ pip install django-rest-polymorphic
Usage
-----
Define your polymorphic models:
.. code-block:: python
# models.py
from django.db import models
from polymorphic.models import PolymorphicModel
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)
Define serializers for each polymorphic model the way you did it when you used ``django-rest-framework``:
.. code-block:: python
# serializers.py
from rest_framework import serializers
from .models import Project, ArtProject, ResearchProject
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('topic', )
class ArtProjectSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ArtProject
fields = ('topic', 'artist', 'url')
extra_kwargs = {
'url': {'view_name': 'project-detail', 'lookup_field': 'pk'},
}
class ResearchProjectSerializer(serializers.ModelSerializer):
class Meta:
model = ResearchProject
fields = ('topic', 'supervisor')
Note that if you extend ``HyperlinkedModelSerializer`` instead of ``ModelSerializer`` you need to define ``extra_kwargs`` to direct the URL to the appropriate view for your polymorphic serializer.
Then you have to create a polymorphic serializer that serves as a mapper between models and serializers which you have defined above:
.. code-block:: python
# serializers.py
from rest_polymorphic.serializers import PolymorphicSerializer
class ProjectPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
Project: ProjectSerializer,
ArtProject: ArtProjectSerializer,
ResearchProject: ResearchProjectSerializer
}
Create viewset with serializer_class equals to your polymorphic serializer:
.. code-block:: python
# views.py
from rest_framework import viewsets
from .models import Project
from .serializers import ProjectPolymorphicSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectPolymorphicSerializer
Test it:
.. code-block:: bash
$ http GET "http://localhost:8000/projects/"
.. code-block:: http
HTTP/1.0 200 OK
Content-Length: 227
Content-Type: application/json
[
{
"resourcetype": "Project",
"topic": "John's gathering"
},
{
"artist": "T. Turner",
"resourcetype": "ArtProject",
"topic": "Sculpting with Tim",
"url": "http://localhost:8000/projects/2/"
},
{
"resourcetype": "ResearchProject",
"supervisor": "Dr. Winter",
"topic": "Swallow Aerodynamics"
}
]
.. code-block:: bash
$ http POST "http://localhost:8000/projects/" resourcetype="ArtProject" topic="Guernica" artist="Picasso"
.. code-block:: http
HTTP/1.0 201 Created
Content-Length: 67
Content-Type: application/json
{
"artist": "Picasso",
"resourcetype": "ArtProject",
"topic": "Guernica",
"url": "http://localhost:8000/projects/4/"
}
Customize resource type
-----------------------
As you can see from the example above, in order to specify the type of your polymorphic model, you need to send a request with resource type field. The value of resource type should be the name of the model.
If you want to change the resource type field name from ``resourcetype`` to something else, you should override ``resource_type_field_name`` attribute:
.. code-block:: python
class ProjectPolymorphicSerializer(PolymorphicSerializer):
resource_type_field_name = 'projecttype'
...
If you want to change the behavior of resource type, you should override ``to_resource_type`` method:
.. code-block:: python
class ProjectPolymorphicSerializer(PolymorphicSerializer):
...
def to_resource_type(self, model_or_instance):
return model_or_instance._meta.object_name.lower()
Now, the request for creating new object will look like this:
.. code-block:: bash
$ http POST "http://localhost:8000/projects/" projecttype="artproject" topic="Guernica" artist="Picasso"

View File

@ -1,16 +0,0 @@
coverage:
precision: 2
round: down
range: 70...100
status:
project: true
patch: true
changes: false
comment:
layout: "header, diff"
behavior: default
ignore:
- "rest_polymorphic/__version__.py"

View File

@ -1,16 +0,0 @@
"""
ASGI config for example project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
application = get_asgi_application()

View File

@ -1,123 +0,0 @@
"""
Django settings for example project.
Generated by 'django-admin startproject' using Django 3.0.5.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'i=m896-(xjn#dz*kx=m8aa^7y!le5od3$(y-2doym!c%or=e64'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'projects',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'example.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'example.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'

View File

@ -1,7 +0,0 @@
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('projects.urls')),
]

View File

@ -1,16 +0,0 @@
"""
WSGI config for example project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
application = get_wsgi_application()

View File

@ -1,21 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@ -1,19 +0,0 @@
from django.core.management.base import BaseCommand
from projects.models import Project, ArtProject, ResearchProject
class Command(BaseCommand):
help = "Generates test data"
def handle(self, *args, **options):
Project.objects.all().delete()
Project.objects.create(topic="Project title #1")
ArtProject.objects.create(
topic="Art project title #1",
artist="T. Artist"
)
ResearchProject.objects.create(
topic="Research project title #1",
supervisor="Dr. Research"
)

View File

@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-12-08 12:52
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
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='projects.Project')),
('artist', models.CharField(max_length=30)),
],
options={
'abstract': False,
},
bases=('projects.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='projects.Project')),
('supervisor', models.CharField(max_length=30)),
],
options={
'abstract': False,
},
bases=('projects.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_projects.project_set+', to='contenttypes.ContentType'),
),
]

View File

@ -1,12 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

View File

@ -1,2 +0,0 @@
[pytest]
DJANGO_SETTINGS_MODULE=tests.settings

View File

@ -1,4 +0,0 @@
-e .
-r requirements/codestyle.txt
-r requirements/packaging.txt
-r requirements/testing.txt

View File

@ -1 +0,0 @@
flake8==3.7.9

View File

@ -1,2 +0,0 @@
# Twine for secured PyPI uploads.
twine==3.1.1

View File

@ -1,4 +0,0 @@
pytest==5.4.1
pytest-django==3.9.0
pytest-mock==3.0.0
pytest-cov==2.8.1

View File

@ -1,7 +0,0 @@
__version__ = '0.1.10'
__title__ = 'django-rest-polymorphic'
__description__ = 'Polymorphic serializers for Django REST Framework.'
__url__ = 'https://github.com/denisorehovsky/django-rest-polymorphic'
__author__ = 'Denis Orehovsky'
__author_email__ = 'denis.orehovsky@gmail.com'
__license__ = 'MIT License'

View File

@ -1,5 +1,4 @@
from collections.abc import Mapping
from six import string_types
from django.core.exceptions import ImproperlyConfigured
from django.db import models
@ -9,21 +8,18 @@ from rest_framework.fields import empty
class PolymorphicSerializer(serializers.Serializer):
model_serializer_mapping = None
resource_type_field_name = 'resourcetype'
resource_type_field_name = "resourcetype"
def __new__(cls, *args, **kwargs):
if cls.model_serializer_mapping is None:
raise ImproperlyConfigured(
'`{cls}` is missing a '
'`{cls}.model_serializer_mapping` attribute'.format(
"`{cls}` is missing a `{cls}.model_serializer_mapping` attribute".format(
cls=cls.__name__
)
)
if not isinstance(cls.resource_type_field_name, string_types):
if not isinstance(cls.resource_type_field_name, str):
raise ImproperlyConfigured(
'`{cls}.resource_type_field_name` must be a string'.format(
cls=cls.__name__
)
"`{cls}.resource_type_field_name` must be a string".format(cls=cls.__name__)
)
return super(PolymorphicSerializer, cls).__new__(cls, *args, **kwargs)
@ -116,17 +112,21 @@ class PolymorphicSerializer(serializers.Serializer):
# Implementation
def _to_model(self, model_or_instance):
return (model_or_instance.__class__
if isinstance(model_or_instance, models.Model)
else model_or_instance)
return (
model_or_instance.__class__
if isinstance(model_or_instance, models.Model)
else model_or_instance
)
def _get_resource_type_from_mapping(self, mapping):
try:
return mapping[self.resource_type_field_name]
except KeyError:
raise serializers.ValidationError({
self.resource_type_field_name: 'This field is required',
})
raise serializers.ValidationError(
{
self.resource_type_field_name: "This field is required",
}
)
def _get_serializer_from_model_or_instance(self, model_or_instance):
model = self._to_model(model_or_instance)
@ -136,10 +136,9 @@ class PolymorphicSerializer(serializers.Serializer):
return self.model_serializer_mapping[klass]
raise KeyError(
'`{cls}.model_serializer_mapping` is missing '
'a corresponding serializer for `{model}` model'.format(
cls=self.__class__.__name__,
model=model.__name__
"`{cls}.model_serializer_mapping` is missing "
"a corresponding serializer for `{model}` model".format(
cls=self.__class__.__name__, model=model.__name__
)
)
@ -147,10 +146,12 @@ class PolymorphicSerializer(serializers.Serializer):
try:
model = self.resource_type_model_mapping[resource_type]
except KeyError:
raise serializers.ValidationError({
self.resource_type_field_name: 'Invalid {0}'.format(
self.resource_type_field_name
)
})
raise serializers.ValidationError(
{
self.resource_type_field_name: "Invalid {0}".format(
self.resource_type_field_name
)
}
)
return self._get_serializer_from_model_or_instance(model)

View File

@ -1,94 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import io
import os
import sys
from shutil import rmtree
from setuptools import setup, Command
here = os.path.abspath(os.path.dirname(__file__))
# Long description
with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f:
readme = '\n' + f.read()
about = {}
# Get meta-data from __version__.py
with open(os.path.join(here, 'rest_polymorphic', '__version__.py')) as f:
exec(f.read(), about)
class UploadCommand(Command):
"""Support setup.py upload."""
description = 'Build and publish the package.'
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print('\033[1m{0}\033[0m'.format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status('Removing previous builds…')
rmtree(os.path.join(here, 'dist'))
except OSError:
pass
self.status('Building Source and Wheel (universal) distribution…')
os.system('{0} setup.py sdist bdist_wheel --universal'.format(
sys.executable
))
self.status('Uploading the package to PyPi via Twine…')
os.system('twine upload dist/*')
sys.exit()
setup(
name=about['__title__'],
version=about['__version__'],
description=about['__description__'],
long_description=readme,
author=about['__author__'],
author_email=about['__author_email__'],
url=about['__url__'],
license=about['__license__'],
packages=[
'rest_polymorphic',
],
install_requires=[
'django',
'djangorestframework',
'django-polymorphic',
'six',
],
classifiers=[
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
'License :: OSI Approved :: MIT License',
'Intended Audience :: Developers',
'Framework :: Django',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0',
'Programming Language :: Python',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy'
],
cmdclass={
'upload': UploadCommand,
},
)

View File

@ -1,37 +0,0 @@
# -*- coding: utf-8
from __future__ import absolute_import, unicode_literals
import django
from django.core import management
DEBUG = True
USE_TZ = True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '55555555555555555555555555555555555555555555555555'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'rest_polymorphic',
'tests',
]
SITE_ID = 1
if django.VERSION >= (1, 10):
MIDDLEWARE = ()
else:
MIDDLEWARE_CLASSES = ()
django.setup()
management.call_command('migrate')

View File

@ -1,168 +0,0 @@
from django.core.exceptions import ImproperlyConfigured
import pytest
from rest_polymorphic.serializers import PolymorphicSerializer
from tests.models import BlogBase, BlogOne, BlogTwo
from tests.serializers import BlogPolymorphicSerializer
pytestmark = pytest.mark.django_db
class TestPolymorphicSerializer:
def test_model_serializer_mapping_is_none(self):
class EmptyPolymorphicSerializer(PolymorphicSerializer):
pass
with pytest.raises(ImproperlyConfigured) as excinfo:
EmptyPolymorphicSerializer()
assert str(excinfo.value) == (
'`EmptyPolymorphicSerializer` is missing a '
'`EmptyPolymorphicSerializer.model_serializer_mapping` attribute'
)
def test_resource_type_field_name_is_not_string(self, mocker):
class NotStringPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = mocker.MagicMock
resource_type_field_name = 1
with pytest.raises(ImproperlyConfigured) as excinfo:
NotStringPolymorphicSerializer()
assert str(excinfo.value) == (
'`NotStringPolymorphicSerializer.resource_type_field_name` must '
'be a string'
)
def test_each_serializer_has_context(self, mocker):
context = mocker.MagicMock()
serializer = BlogPolymorphicSerializer(context=context)
for inner_serializer in serializer.model_serializer_mapping.values():
assert inner_serializer.context == context
def test_serialize(self):
instance = BlogBase.objects.create(name='blog', slug='blog')
serializer = BlogPolymorphicSerializer(instance)
assert serializer.data == {
'name': 'blog',
'slug': 'blog',
'resourcetype': 'BlogBase',
}
def test_deserialize(self):
data = {
'name': 'blog',
'slug': 'blog',
'resourcetype': 'BlogBase',
}
serializers = BlogPolymorphicSerializer(data=data)
assert serializers.is_valid()
assert serializers.data == data
def test_deserialize_with_invalid_resourcetype(self):
data = {
'name': 'blog',
'resourcetype': 'Invalid',
}
serializers = BlogPolymorphicSerializer(data=data)
assert not serializers.is_valid()
def test_create(self):
data = [
{
'name': 'a',
'slug': 'a',
'resourcetype': 'BlogBase'
},
{
'name': 'b',
'slug': 'b',
'info': 'info',
'resourcetype': 'BlogOne'
},
{
'name': 'c',
'slug': 'c',
'resourcetype': 'BlogTwo'
},
]
serializer = BlogPolymorphicSerializer(data=data, many=True)
assert serializer.is_valid()
instances = serializer.save()
assert len(instances) == 3
assert [item.name for item in instances] == ['a', 'b', 'c']
assert BlogBase.objects.count() == 3
assert BlogBase.objects.instance_of(BlogOne).count() == 1
assert BlogBase.objects.instance_of(BlogTwo).count() == 1
assert serializer.data == data
def test_update(self):
instance = BlogBase.objects.create(name='blog', slug='blog')
data = {
'name': 'new-blog',
'slug': 'blog',
'resourcetype': 'BlogBase'
}
serializer = BlogPolymorphicSerializer(instance, data=data)
assert serializer.is_valid()
serializer.save()
assert instance.name == 'new-blog'
assert instance.slug == 'blog'
def test_partial_update(self):
instance = BlogBase.objects.create(name='blog', slug='blog')
data = {
'name': 'new-blog',
'resourcetype': 'BlogBase'
}
serializer = BlogPolymorphicSerializer(
instance, data=data, partial=True
)
assert serializer.is_valid()
serializer.save()
assert instance.name == 'new-blog'
assert instance.slug == 'blog'
def test_partial_update_without_resourcetype(self):
instance = BlogBase.objects.create(name='blog', slug='blog')
data = {'name': 'new-blog'}
serializer = BlogPolymorphicSerializer(
instance, data=data, partial=True
)
assert serializer.is_valid()
serializer.save()
assert instance.name == 'new-blog'
assert instance.slug == 'blog'
def test_object_validators_are_applied(self):
data = {
'name': 'test-blog',
'slug': 'test-blog-slug',
'info': 'test-blog-info',
'about': 'test-blog-about',
'resourcetype': 'BlogThree'
}
serializer = BlogPolymorphicSerializer(data=data)
assert serializer.is_valid()
serializer.save()
data['slug'] = 'test-blog-slug-new'
duplicate = BlogPolymorphicSerializer(data=data)
assert not duplicate.is_valid()
assert 'non_field_errors' in duplicate.errors
err = duplicate.errors['non_field_errors']
assert err == ['The fields info, about must make a unique set.']

View File

@ -1,38 +0,0 @@
[tox]
envlist =
py{35,36,37,38}-django{22}-drf{38,39,310,311},
py{36,37,38}-django{30}-drf{310,311},
py{36,37,38}-djangomaster-drfmaster,
flake8
[testenv]
passenv = CI TRAVIS TRAVIS_*
basepython =
py35: python3.5
py36: python3.6
py37: python3.7
py38: python3.8
deps =
django22: django>=2.2,<3.0
django30: django>=3.0,<3.1
djangomaster: git+git://github.com/django/django.git
drf38: djangorestframework>=3.8,<3.9
drf39: djangorestframework>=3.9,<3.10
drf310: djangorestframework>=3.10,<3.11
drf311: djangorestframework==3.11,<3.12
drfmaster: git+git://github.com/encode/django-rest-framework.git
-rrequirements/testing.txt
commands =
py.test --capture=no --cov-report term-missing --cov-report html --cov=rest_polymorphic tests/
[testenv:flake8]
basepython = python3
deps =
-rrequirements/codestyle.txt
-rrequirements/testing.txt
commands =
flake8 rest_polymorphic/ tests/ setup.py

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-12 16:34
# Generated by Django 4.2 on 2026-01-13 11:18
from decimal import Decimal
from django.conf import settings

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class DRFExampleConfig(AppConfig):
name = "polymorphic.tests.examples.integrations.drf"
label = "drf_example"
verbose_name = "DRF Examples"

View File

@ -1,6 +1,6 @@
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from polymorphic.contrib.drf.serializers import PolymorphicSerializer
from .models import Project, ArtProject, ResearchProject
@ -8,27 +8,27 @@ from .models import Project, ArtProject, ResearchProject
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('topic', )
fields = ("topic",)
class ArtProjectSerializer(serializers.ModelSerializer):
class Meta:
model = ArtProject
fields = ('topic', 'artist', 'url')
fields = ("topic", "artist", "url")
extra_kwargs = {
'url': {'view_name': 'project-detail', 'lookup_field': 'pk'},
"url": {"view_name": "drf:project-detail", "lookup_field": "pk"},
}
class ResearchProjectSerializer(serializers.ModelSerializer):
class Meta:
model = ResearchProject
fields = ('topic', 'supervisor')
fields = ("topic", "supervisor")
class ProjectPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
Project: ProjectSerializer,
ArtProject: ArtProjectSerializer,
ResearchProject: ResearchProjectSerializer
ResearchProject: ResearchProjectSerializer,
}

View File

@ -0,0 +1,94 @@
# Generated by Django 4.2 on 2026-01-13 11:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='BlogBase',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
('slug', models.SlugField(max_length=255, unique=True)),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
},
),
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)),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
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='drf_example.project')),
('artist', models.CharField(max_length=30)),
],
options={
'abstract': False,
},
bases=('drf_example.project',),
),
migrations.CreateModel(
name='BlogOne',
fields=[
('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.blogbase')),
('info', models.CharField(max_length=10)),
],
options={
'abstract': False,
},
bases=('drf_example.blogbase',),
),
migrations.CreateModel(
name='BlogTwo',
fields=[
('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.blogbase')),
],
options={
'abstract': False,
},
bases=('drf_example.blogbase',),
),
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='drf_example.project')),
('supervisor', models.CharField(max_length=30)),
],
options={
'abstract': False,
},
bases=('drf_example.project',),
),
migrations.CreateModel(
name='BlogThree',
fields=[
('blogbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='drf_example.blogbase')),
('info', models.CharField(max_length=255)),
('about', models.CharField(max_length=255)),
],
options={
'unique_together': {('info', 'about')},
},
bases=('drf_example.blogbase',),
),
]

View File

@ -0,0 +1,16 @@
from .models_test import BlogBase, BlogOne, BlogTwo, BlogThree
from .example_models import (
Project,
ArtProject,
ResearchProject,
)
__all__ = [
"BlogBase",
"BlogOne",
"BlogTwo",
"BlogThree",
"Project",
"ArtProject",
"ResearchProject",
]

View File

@ -21,4 +21,4 @@ class BlogThree(BlogBase):
about = models.CharField(max_length=255)
class Meta:
unique_together = (('info', 'about'),)
unique_together = (("info", "about"),)

View File

@ -1,36 +1,32 @@
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from polymorphic.contrib.drf.serializers import PolymorphicSerializer
from tests.models import BlogBase, BlogOne, BlogTwo, BlogThree
from .models import BlogBase, BlogOne, BlogTwo, BlogThree
class BlogBaseSerializer(serializers.ModelSerializer):
class Meta:
model = BlogBase
fields = ('name', 'slug')
fields = ("name", "slug")
class BlogOneSerializer(serializers.ModelSerializer):
class Meta:
model = BlogOne
fields = ('name', 'slug', 'info')
fields = ("name", "slug", "info")
class BlogTwoSerializer(serializers.ModelSerializer):
class Meta:
model = BlogTwo
fields = ('name', 'slug')
fields = ("name", "slug")
class BlogThreeSerializer(serializers.ModelSerializer):
class Meta:
model = BlogThree
fields = ('name', 'slug', 'info', 'about')
fields = ("name", "slug", "info", "about")
class BlogPolymorphicSerializer(PolymorphicSerializer):
@ -38,5 +34,5 @@ class BlogPolymorphicSerializer(PolymorphicSerializer):
BlogBase: BlogBaseSerializer,
BlogOne: BlogOneSerializer,
BlogTwo: BlogTwoSerializer,
BlogThree: BlogThreeSerializer
BlogThree: BlogThreeSerializer,
}

View File

@ -0,0 +1,441 @@
from django.core.exceptions import ImproperlyConfigured
import pytest
try:
from rest_framework.test import APIClient
from polymorphic.contrib.drf.serializers import PolymorphicSerializer
from .serializers import (
BlogBaseSerializer,
BlogOneSerializer,
BlogPolymorphicSerializer,
)
from .models import BlogBase, BlogOne, BlogTwo, Project, ArtProject, ResearchProject
except ImportError:
pytest.skip("djangorestframework is not installed", allow_module_level=True)
pytestmark = pytest.mark.django_db
class TestPolymorphicSerializer:
def test_model_serializer_mapping_is_none(self):
class EmptyPolymorphicSerializer(PolymorphicSerializer):
pass
with pytest.raises(ImproperlyConfigured) as excinfo:
EmptyPolymorphicSerializer()
assert str(excinfo.value) == (
"`EmptyPolymorphicSerializer` is missing a "
"`EmptyPolymorphicSerializer.model_serializer_mapping` attribute"
)
def test_resource_type_field_name_is_not_string(self, mocker):
class NotStringPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = mocker.MagicMock
resource_type_field_name = 1
with pytest.raises(ImproperlyConfigured) as excinfo:
NotStringPolymorphicSerializer()
assert str(excinfo.value) == (
"`NotStringPolymorphicSerializer.resource_type_field_name` must be a string"
)
def test_each_serializer_has_context(self, mocker):
context = mocker.MagicMock()
serializer = BlogPolymorphicSerializer(context=context)
for inner_serializer in serializer.model_serializer_mapping.values():
assert inner_serializer.context == context
def test_non_callable_serializer_in_mapping(self):
# Test the case where serializer is already instantiated (not callable)
# This tests the else branch of the callable(serializer) check
# Create an already-instantiated serializer
blog_base_serializer_instance = BlogBaseSerializer()
class TestPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
BlogBase: blog_base_serializer_instance, # Already an instance
BlogOne: BlogOneSerializer, # Still a class (callable)
}
serializer = TestPolymorphicSerializer()
# The instance should be used directly without re-instantiation
assert serializer.model_serializer_mapping[BlogBase] is blog_base_serializer_instance
# The callable should be instantiated
assert isinstance(serializer.model_serializer_mapping[BlogOne], BlogOneSerializer)
assert serializer.model_serializer_mapping[BlogOne] is not BlogOneSerializer
# Both should be in resource_type_model_mapping
assert serializer.resource_type_model_mapping["BlogBase"] == BlogBase
assert serializer.resource_type_model_mapping["BlogOne"] == BlogOne
# Now test that serialization actually works with the non-callable serializer
base_instance = BlogBase.objects.create(name="base", slug="base-slug")
one_instance = BlogOne.objects.create(name="one", slug="one-slug", info="info")
# Serialize BlogBase (using the pre-instantiated serializer)
base_serializer = TestPolymorphicSerializer(base_instance)
base_data = base_serializer.data
assert base_data == {
"name": "base",
"slug": "base-slug",
"resourcetype": "BlogBase",
}
# Serialize BlogOne (using the callable serializer that was instantiated)
one_serializer = TestPolymorphicSerializer(one_instance)
one_data = one_serializer.data
assert one_data == {
"name": "one",
"slug": "one-slug",
"info": "info",
"resourcetype": "BlogOne",
}
# Test serialization of multiple instances (many=True)
instances = [base_instance, one_instance]
many_serializer = TestPolymorphicSerializer(instances, many=True)
many_data = many_serializer.data
assert len(many_data) == 2
assert many_data[0]["resourcetype"] == "BlogBase"
assert many_data[1]["resourcetype"] == "BlogOne"
assert many_data[0]["name"] == "base"
assert many_data[1]["name"] == "one"
assert many_data[1]["info"] == "info"
def test_serialize(self):
instance = BlogBase.objects.create(name="blog", slug="blog")
serializer = BlogPolymorphicSerializer(instance)
assert serializer.data == {
"name": "blog",
"slug": "blog",
"resourcetype": "BlogBase",
}
def test_deserialize(self):
data = {
"name": "blog",
"slug": "blog",
"resourcetype": "BlogBase",
}
serializers = BlogPolymorphicSerializer(data=data)
assert serializers.is_valid()
assert serializers.data == data
def test_deserialize_with_invalid_resourcetype(self):
data = {
"name": "blog",
"resourcetype": "Invalid",
}
serializers = BlogPolymorphicSerializer(data=data)
assert not serializers.is_valid()
def test_create(self):
data = [
{"name": "a", "slug": "a", "resourcetype": "BlogBase"},
{"name": "b", "slug": "b", "info": "info", "resourcetype": "BlogOne"},
{"name": "c", "slug": "c", "resourcetype": "BlogTwo"},
]
serializer = BlogPolymorphicSerializer(data=data, many=True)
assert serializer.is_valid()
instances = serializer.save()
assert len(instances) == 3
assert [item.name for item in instances] == ["a", "b", "c"]
assert BlogBase.objects.count() == 3
assert BlogBase.objects.instance_of(BlogOne).count() == 1
assert BlogBase.objects.instance_of(BlogTwo).count() == 1
assert serializer.data == data
def test_update(self):
instance = BlogBase.objects.create(name="blog", slug="blog")
data = {"name": "new-blog", "slug": "blog", "resourcetype": "BlogBase"}
serializer = BlogPolymorphicSerializer(instance, data=data)
assert serializer.is_valid()
serializer.save()
assert instance.name == "new-blog"
assert instance.slug == "blog"
def test_partial_update(self):
instance = BlogBase.objects.create(name="blog", slug="blog")
data = {"name": "new-blog", "resourcetype": "BlogBase"}
serializer = BlogPolymorphicSerializer(instance, data=data, partial=True)
assert serializer.is_valid()
serializer.save()
assert instance.name == "new-blog"
assert instance.slug == "blog"
def test_partial_update_without_resourcetype(self):
instance = BlogBase.objects.create(name="blog", slug="blog")
data = {"name": "new-blog"}
serializer = BlogPolymorphicSerializer(instance, data=data, partial=True)
assert serializer.is_valid()
serializer.save()
assert instance.name == "new-blog"
assert instance.slug == "blog"
def test_object_validators_are_applied(self):
data = {
"name": "test-blog",
"slug": "test-blog-slug",
"info": "test-blog-info",
"about": "test-blog-about",
"resourcetype": "BlogThree",
}
serializer = BlogPolymorphicSerializer(data=data)
assert serializer.is_valid()
serializer.save()
data["slug"] = "test-blog-slug-new"
duplicate = BlogPolymorphicSerializer(data=data)
assert not duplicate.is_valid()
assert "non_field_errors" in duplicate.errors
err = duplicate.errors["non_field_errors"]
assert err == ["The fields info, about must make a unique set."]
def test_to_internal_value_with_valid_data(self):
data = {
"name": "blog",
"slug": "blog",
"resourcetype": "BlogBase",
}
serializer = BlogPolymorphicSerializer(data=data)
internal_value = serializer.to_internal_value(data)
assert internal_value["name"] == "blog"
assert internal_value["slug"] == "blog"
assert internal_value["resourcetype"] == "BlogBase"
def test_to_internal_value_with_missing_resourcetype(self):
from rest_framework.exceptions import ValidationError
data = {
"name": "blog",
"slug": "blog",
}
serializer = BlogPolymorphicSerializer(data=data)
with pytest.raises(ValidationError) as excinfo:
serializer.to_internal_value(data)
assert "resourcetype" in excinfo.value.detail
assert excinfo.value.detail["resourcetype"] == "This field is required"
def test_to_internal_value_with_partial_update(self):
instance = BlogBase.objects.create(name="blog", slug="blog")
data = {"name": "new-blog"}
serializer = BlogPolymorphicSerializer(instance, data=data, partial=True)
internal_value = serializer.to_internal_value(data)
assert internal_value["name"] == "new-blog"
assert internal_value["resourcetype"] == "BlogBase"
def test_get_serializer_from_model_or_instance_raises_keyerror(self):
from polymorphic.models import PolymorphicModel
# Create a model that is not in the mapping
class UnmappedModel(PolymorphicModel):
class Meta:
app_label = "drf"
serializer = BlogPolymorphicSerializer()
with pytest.raises(KeyError) as excinfo:
serializer._get_serializer_from_model_or_instance(UnmappedModel)
assert "model_serializer_mapping" in str(excinfo.value)
assert "UnmappedModel" in str(excinfo.value)
def test_get_serializer_from_resource_type_keyerror_propagation(self):
# This tests the case where _get_serializer_from_resource_type
# successfully finds a resource_type in the mapping, but then
# _get_serializer_from_model_or_instance raises a KeyError
# when trying to find the serializer for that model.
#
# However, looking at the code, this scenario is actually not possible
# in normal operation because resource_type_model_mapping and
# model_serializer_mapping are populated together in __init__.
#
# The KeyError in _get_serializer_from_resource_type would only
# occur if the resource_type is not in resource_type_model_mapping,
# which is already caught and converted to ValidationError at line 149.
#
# So we'll test that the ValidationError is raised properly instead.
from rest_framework.exceptions import ValidationError
data = {
"name": "blog",
"slug": "blog",
"resourcetype": "InvalidResourceType",
}
serializer = BlogPolymorphicSerializer(data=data)
with pytest.raises(ValidationError) as excinfo:
serializer._get_serializer_from_resource_type("InvalidResourceType")
assert "resourcetype" in excinfo.value.detail
assert "Invalid resourcetype" in str(excinfo.value.detail["resourcetype"])
class TestProjectViewSet:
"""Test the example Project ViewSet with polymorphic serializers."""
@pytest.fixture
def client(self):
return APIClient()
@pytest.fixture
def base_project(self):
return Project.objects.create(topic="General Project")
@pytest.fixture
def art_project(self):
return ArtProject.objects.create(topic="Art", artist="Picasso")
@pytest.fixture
def research_project(self):
return ResearchProject.objects.create(topic="Research", supervisor="Dr. Smith")
def test_list_projects(self, client, base_project, art_project, research_project):
response = client.get("/examples/integrations/drf/projects/")
assert response.status_code == 200
assert len(response.data) == 3
topics = {item["topic"] for item in response.data}
assert topics == {"General Project", "Art", "Research"}
def test_retrieve_base_project(self, client, base_project):
response = client.get(f"/examples/integrations/drf/projects/{base_project.pk}/")
assert response.status_code == 200
assert response.data["topic"] == "General Project"
assert response.data["resourcetype"] == "Project"
def test_retrieve_art_project(self, client, art_project):
response = client.get(f"/examples/integrations/drf/projects/{art_project.pk}/")
assert response.status_code == 200
assert response.data["topic"] == "Art"
assert response.data["artist"] == "Picasso"
assert response.data["resourcetype"] == "ArtProject"
assert "url" in response.data
def test_retrieve_research_project(self, client, research_project):
response = client.get(f"/examples/integrations/drf/projects/{research_project.pk}/")
assert response.status_code == 200
assert response.data["topic"] == "Research"
assert response.data["supervisor"] == "Dr. Smith"
assert response.data["resourcetype"] == "ResearchProject"
def test_create_base_project(self, client):
data = {"topic": "New Project", "resourcetype": "Project"}
response = client.post("/examples/integrations/drf/projects/", data, format="json")
assert response.status_code == 201
assert response.data["topic"] == "New Project"
assert response.data["resourcetype"] == "Project"
assert Project.objects.count() == 1
project = Project.objects.first()
assert project.topic == "New Project"
assert type(project) is Project
def test_create_art_project(self, client):
data = {
"topic": "Sculpture",
"artist": "Michelangelo",
"resourcetype": "ArtProject",
}
response = client.post("/examples/integrations/drf/projects/", data, format="json")
assert response.status_code == 201
assert response.data["topic"] == "Sculpture"
assert response.data["artist"] == "Michelangelo"
assert response.data["resourcetype"] == "ArtProject"
assert "url" in response.data
assert Project.objects.count() == 1
project = Project.objects.first()
assert isinstance(project, ArtProject)
assert project.artist == "Michelangelo"
def test_create_research_project(self, client):
data = {
"topic": "AI Research",
"supervisor": "Dr. Johnson",
"resourcetype": "ResearchProject",
}
response = client.post("/examples/integrations/drf/projects/", data, format="json")
assert response.status_code == 201
assert response.data["topic"] == "AI Research"
assert response.data["supervisor"] == "Dr. Johnson"
assert response.data["resourcetype"] == "ResearchProject"
assert Project.objects.count() == 1
project = Project.objects.first()
assert isinstance(project, ResearchProject)
assert project.supervisor == "Dr. Johnson"
def test_update_project(self, client, base_project):
data = {"topic": "Updated Project", "resourcetype": "Project"}
response = client.put(
f"/examples/integrations/drf/projects/{base_project.pk}/", data, format="json"
)
assert response.status_code == 200
assert response.data["topic"] == "Updated Project"
base_project.refresh_from_db()
assert base_project.topic == "Updated Project"
def test_partial_update_art_project(self, client, art_project):
data = {"artist": "Van Gogh"}
response = client.patch(
f"/examples/integrations/drf/projects/{art_project.pk}/", data, format="json"
)
assert response.status_code == 200
assert response.data["artist"] == "Van Gogh"
assert response.data["topic"] == "Art" # unchanged
art_project.refresh_from_db()
assert art_project.artist == "Van Gogh"
assert art_project.topic == "Art"
def test_partial_update_research_project(self, client, research_project):
data = {"supervisor": "Dr. Williams"}
response = client.patch(
f"/examples/integrations/drf/projects/{research_project.pk}/", data, format="json"
)
assert response.status_code == 200
assert response.data["supervisor"] == "Dr. Williams"
assert response.data["topic"] == "Research" # unchanged
research_project.refresh_from_db()
assert research_project.supervisor == "Dr. Williams"
assert research_project.topic == "Research"
def test_delete_project(self, client, base_project):
project_id = base_project.pk
response = client.delete(f"/examples/integrations/drf/projects/{project_id}/")
assert response.status_code == 204
assert not Project.objects.filter(pk=project_id).exists()
def test_create_with_invalid_resourcetype(self, client):
data = {"topic": "Test", "resourcetype": "InvalidType"}
response = client.post("/examples/integrations/drf/projects/", data, format="json")
assert response.status_code == 400

View File

@ -2,7 +2,9 @@ from rest_framework.routers import DefaultRouter
from .views import ProjectViewSet
app_name = "drf"
router = DefaultRouter()
router.register(r'projects', ProjectViewSet)
router.register(r"projects", ProjectViewSet)
urlpatterns = router.urls

View File

@ -1,7 +1,7 @@
from rest_framework import viewsets
from .models import Project
from .serializers import ProjectPolymorphicSerializer
from .example_serializers import ProjectPolymorphicSerializer
class ProjectViewSet(viewsets.ModelViewSet):

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-12 16:34
# Generated by Django 4.2 on 2026-01-13 11:18
from django.db import migrations, models
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-12 16:34
# Generated by Django 4.2 on 2026-01-13 11:18
from django.db import migrations, models
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-12 16:34
# Generated by Django 4.2 on 2026-01-13 11:18
from django.conf import settings
from django.db import migrations, models

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-12 16:34
# Generated by Django 4.2 on 2026-01-13 11:18
from django.db import migrations, models
import django.db.models.deletion

View File

@ -130,6 +130,13 @@ try:
except ImportError:
pass
try:
import rest_framework # noqa: F401
INSTALLED_APPS.insert(0, "rest_framework")
INSTALLED_APPS.insert(0, "polymorphic.tests.examples.integrations.drf")
except ImportError:
pass
MIDDLEWARE = (
"django.middleware.common.CommonMiddleware",

View File

@ -20,3 +20,15 @@ try:
)
except ImportError:
pass
try:
import rest_framework # noqa: F401
urlpatterns.append(
path(
"examples/integrations/drf/",
include("polymorphic.tests.examples.integrations.drf.urls", namespace="drf"),
)
)
except ImportError:
pass

33
uv.lock
View File

@ -650,6 +650,7 @@ dev = [
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-django" },
{ name = "pytest-mock" },
{ name = "pytest-playwright" },
{ name = "ruff" },
{ name = "tomlkit" },
@ -658,6 +659,7 @@ dev = [
]
docs = [
{ name = "django-extra-views" },
{ name = "djangorestframework" },
{ name = "doc8" },
{ name = "furo" },
{ name = "readme-renderer", extra = ["md"] },
@ -668,6 +670,9 @@ docs = [
{ name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-django" },
]
drf = [
{ name = "djangorestframework" },
]
extra-views = [
{ name = "django-extra-views" },
]
@ -703,6 +708,7 @@ dev = [
{ name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-cov", specifier = ">=5.0.0" },
{ name = "pytest-django", specifier = ">=4.10.0" },
{ name = "pytest-mock", specifier = ">=3.15.1" },
{ name = "pytest-playwright", specifier = ">=0.7.2" },
{ name = "ruff", specifier = ">=0.9.8" },
{ name = "tomlkit", specifier = ">=0.13.3" },
@ -711,6 +717,7 @@ dev = [
]
docs = [
{ name = "django-extra-views", specifier = ">=0.16.0" },
{ name = "djangorestframework", specifier = ">=3.16.1" },
{ name = "doc8", specifier = ">=1.1.2" },
{ name = "furo", specifier = ">=2025.7.19" },
{ name = "readme-renderer", extras = ["md"], specifier = ">=43.0" },
@ -718,6 +725,7 @@ docs = [
{ name = "sphinx-autobuild", specifier = ">=2024.10.3" },
{ name = "sphinxcontrib-django", specifier = ">=2.5" },
]
drf = [{ name = "djangorestframework", specifier = ">=3.16.1" }]
extra-views = [{ name = "django-extra-views", specifier = ">=0.16.0" }]
mysql = [{ name = "mysqlclient", specifier = ">=1.4.0" }]
oracledb = [{ name = "oracledb", specifier = ">=2.3.0" }]
@ -750,6 +758,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/fe/38789c69f71adff9156bda7542d8fd05fcde1a109cf67bd7a1a139f8199f/django_test_migrations-1.5.0-py3-none-any.whl", hash = "sha256:96a08f085fc8bfaa53d44618341d82a2d22fd194c821cd81b147b66f0bec0da8", size = 25099, upload-time = "2025-04-18T10:15:37.16Z" },
]
[[package]]
name = "djangorestframework"
version = "3.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
]
[[package]]
name = "doc8"
version = "2.0.0"
@ -1599,6 +1620,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
]
[[package]]
name = "pytest-mock"
version = "3.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
]
[[package]]
name = "pytest-playwright"
version = "0.7.2"