├── tests ├── __init__.py ├── models.py ├── settings.py ├── serializers.py └── test_serializers.py ├── example ├── example │ ├── __init__.py │ ├── urls.py │ ├── asgi.py │ ├── wsgi.py │ └── settings.py ├── projects │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── testdata.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── urls.py │ ├── views.py │ ├── models.py │ └── serializers.py └── manage.py ├── rest_polymorphic ├── __init__.py ├── __version__.py └── serializers.py ├── requirements ├── codestyle.txt ├── packaging.txt └── testing.txt ├── pytest.ini ├── requirements.txt ├── MANIFEST.in ├── codecov.yml ├── AUTHORS.rst ├── manage.py ├── .editorconfig ├── .gitignore ├── LICENSE ├── HISTORY.rst ├── tox.ini ├── .travis.yml ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/projects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rest_polymorphic/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/projects/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/projects/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/projects/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/codestyle.txt: -------------------------------------------------------------------------------- 1 | flake8==3.7.9 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=tests.settings 3 | -------------------------------------------------------------------------------- /requirements/packaging.txt: -------------------------------------------------------------------------------- 1 | # Twine for secured PyPI uploads. 2 | twine==3.1.1 3 | -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | pytest==5.4.1 2 | pytest-django==3.9.0 3 | pytest-mock==3.0.0 4 | pytest-cov==2.8.1 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -r requirements/codestyle.txt 3 | -r requirements/packaging.txt 4 | -r requirements/testing.txt 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include HISTORY.rst 3 | include LICENSE 4 | include README.rst 5 | recursive-include rest_polymorphic *.html *.png *.gif *js *.css *jpg *jpeg *svg *py 6 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('', include('projects.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /example/projects/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import DefaultRouter 2 | 3 | from .views import ProjectViewSet 4 | 5 | router = DefaultRouter() 6 | router.register(r'projects', ProjectViewSet) 7 | 8 | urlpatterns = router.urls 9 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: 70...100 5 | 6 | status: 7 | project: true 8 | patch: true 9 | changes: false 10 | 11 | comment: 12 | layout: "header, diff" 13 | behavior: default 14 | 15 | ignore: 16 | - "rest_polymorphic/__version__.py" 17 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Denis Orehovsky 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Jeff Hackshaw 14 | * TFranzel 15 | * Ignacio Losiggio 16 | -------------------------------------------------------------------------------- /example/projects/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from .models import Project 4 | from .serializers import ProjectPolymorphicSerializer 5 | 6 | 7 | class ProjectViewSet(viewsets.ModelViewSet): 8 | queryset = Project.objects.all() 9 | serializer_class = ProjectPolymorphicSerializer 10 | -------------------------------------------------------------------------------- /rest_polymorphic/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.10' 2 | __title__ = 'django-rest-polymorphic' 3 | __description__ = 'Polymorphic serializers for Django REST Framework.' 4 | __url__ = 'https://github.com/denisorehovsky/django-rest-polymorphic' 5 | __author__ = 'Denis Orehovsky' 6 | __author_email__ = 'denis.orehovsky@gmail.com' 7 | __license__ = 'MIT License' 8 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import os 6 | import sys 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /example/projects/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from polymorphic.models import PolymorphicModel 4 | 5 | 6 | class Project(PolymorphicModel): 7 | topic = models.CharField(max_length=30) 8 | 9 | 10 | class ArtProject(Project): 11 | artist = models.CharField(max_length=30) 12 | 13 | 14 | class ResearchProject(Project): 15 | supervisor = models.CharField(max_length=30) 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /example/example/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for example project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from polymorphic.models import PolymorphicModel 4 | 5 | 6 | class BlogBase(PolymorphicModel): 7 | name = models.CharField(max_length=10) 8 | slug = models.SlugField(max_length=255, unique=True) 9 | 10 | 11 | class BlogOne(BlogBase): 12 | info = models.CharField(max_length=10) 13 | 14 | 15 | class BlogTwo(BlogBase): 16 | pass 17 | 18 | 19 | class BlogThree(BlogBase): 20 | info = models.CharField(max_length=255) 21 | about = models.CharField(max_length=255) 22 | 23 | class Meta: 24 | unique_together = (('info', 'about'),) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Pycharm/Intellij 40 | .idea 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | 49 | # DB 50 | db.sqlite3 51 | -------------------------------------------------------------------------------- /example/projects/management/commands/testdata.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from projects.models import Project, ArtProject, ResearchProject 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Generates test data" 8 | 9 | def handle(self, *args, **options): 10 | Project.objects.all().delete() 11 | Project.objects.create(topic="Project title #1") 12 | ArtProject.objects.create( 13 | topic="Art project title #1", 14 | artist="T. Artist" 15 | ) 16 | ResearchProject.objects.create( 17 | topic="Research project title #1", 18 | supervisor="Dr. Research" 19 | ) 20 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | import django 5 | from django.core import management 6 | 7 | DEBUG = True 8 | USE_TZ = True 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = '55555555555555555555555555555555555555555555555555' 12 | 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.sqlite3', 16 | 'NAME': ':memory:', 17 | } 18 | } 19 | 20 | INSTALLED_APPS = [ 21 | 'django.contrib.auth', 22 | 'django.contrib.contenttypes', 23 | 'django.contrib.sites', 24 | 25 | 'rest_polymorphic', 26 | 'tests', 27 | ] 28 | 29 | SITE_ID = 1 30 | 31 | if django.VERSION >= (1, 10): 32 | MIDDLEWARE = () 33 | else: 34 | MIDDLEWARE_CLASSES = () 35 | 36 | django.setup() 37 | management.call_command('migrate') 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017, Denis Orehovsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /example/projects/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from rest_polymorphic.serializers import PolymorphicSerializer 4 | 5 | from .models import Project, ArtProject, ResearchProject 6 | 7 | 8 | class ProjectSerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = Project 11 | fields = ('topic', ) 12 | 13 | 14 | class ArtProjectSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = ArtProject 17 | fields = ('topic', 'artist', 'url') 18 | extra_kwargs = { 19 | 'url': {'view_name': 'project-detail', 'lookup_field': 'pk'}, 20 | } 21 | 22 | 23 | class ResearchProjectSerializer(serializers.ModelSerializer): 24 | class Meta: 25 | model = ResearchProject 26 | fields = ('topic', 'supervisor') 27 | 28 | 29 | class ProjectPolymorphicSerializer(PolymorphicSerializer): 30 | model_serializer_mapping = { 31 | Project: ProjectSerializer, 32 | ArtProject: ArtProjectSerializer, 33 | ResearchProject: ResearchProjectSerializer 34 | } 35 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.1.10 (2022-07-17) 7 | ++++++++++++++++++ 8 | 9 | * Allow partial updates without resourcetype. 10 | 11 | 0.1.9 (2020-04-02) 12 | ++++++++++++++++++ 13 | 14 | * Fix validation error and update versions. 15 | 16 | 0.1.8 (2018-10-11) 17 | ++++++++++++++++++ 18 | 19 | * Add Python 3.7 and Django 2.1 support. 20 | 21 | 0.1.7 (2018-06-06) 22 | ++++++++++++++++++ 23 | 24 | 0.1.6 (2018-06-03) 25 | ++++++++++++++++++ 26 | 27 | * Add Django REST Framework 3.8 support. 28 | 29 | 0.1.5 (2018-03-29) 30 | ++++++++++++++++++ 31 | 32 | * Fix validation for nested serializers. 33 | 34 | 0.1.4 (2018-02-10) 35 | ++++++++++++++++++ 36 | 37 | * Fix serializer instantiation. 38 | 39 | 0.1.3 (2018-02-09) 40 | ++++++++++++++++++ 41 | 42 | * Add Python 2.7 support. 43 | 44 | 0.1.2 (2018-01-30) 45 | ++++++++++++++++++ 46 | 47 | * Add Django 2.0 support. 48 | * Drop Django 1.10 support. 49 | 50 | 0.1.1 (2017-12-08) 51 | ++++++++++++++++++ 52 | 53 | * Add example project. 54 | 55 | 0.1.0 (2017-12-07) 56 | ++++++++++++++++++ 57 | 58 | * First release on PyPI. 59 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{35,36,37,38}-django{22}-drf{38,39,310,311}, 4 | py{36,37,38}-django{30}-drf{310,311}, 5 | py{36,37,38}-djangomaster-drfmaster, 6 | flake8 7 | 8 | 9 | [testenv] 10 | passenv = CI TRAVIS TRAVIS_* 11 | basepython = 12 | py35: python3.5 13 | py36: python3.6 14 | py37: python3.7 15 | py38: python3.8 16 | deps = 17 | django22: django>=2.2,<3.0 18 | django30: django>=3.0,<3.1 19 | djangomaster: git+git://github.com/django/django.git 20 | 21 | drf38: djangorestframework>=3.8,<3.9 22 | drf39: djangorestframework>=3.9,<3.10 23 | drf310: djangorestframework>=3.10,<3.11 24 | drf311: djangorestframework==3.11,<3.12 25 | drfmaster: git+git://github.com/encode/django-rest-framework.git 26 | 27 | -rrequirements/testing.txt 28 | commands = 29 | py.test --capture=no --cov-report term-missing --cov-report html --cov=rest_polymorphic tests/ 30 | 31 | 32 | [testenv:flake8] 33 | basepython = python3 34 | deps = 35 | -rrequirements/codestyle.txt 36 | -rrequirements/testing.txt 37 | commands = 38 | flake8 rest_polymorphic/ tests/ setup.py 39 | -------------------------------------------------------------------------------- /tests/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from rest_polymorphic.serializers import PolymorphicSerializer 4 | 5 | from tests.models import BlogBase, BlogOne, BlogTwo, BlogThree 6 | 7 | 8 | class BlogBaseSerializer(serializers.ModelSerializer): 9 | 10 | class Meta: 11 | model = BlogBase 12 | fields = ('name', 'slug') 13 | 14 | 15 | class BlogOneSerializer(serializers.ModelSerializer): 16 | 17 | class Meta: 18 | model = BlogOne 19 | fields = ('name', 'slug', 'info') 20 | 21 | 22 | class BlogTwoSerializer(serializers.ModelSerializer): 23 | 24 | class Meta: 25 | model = BlogTwo 26 | fields = ('name', 'slug') 27 | 28 | 29 | class BlogThreeSerializer(serializers.ModelSerializer): 30 | 31 | class Meta: 32 | model = BlogThree 33 | fields = ('name', 'slug', 'info', 'about') 34 | 35 | 36 | class BlogPolymorphicSerializer(PolymorphicSerializer): 37 | model_serializer_mapping = { 38 | BlogBase: BlogBaseSerializer, 39 | BlogOne: BlogOneSerializer, 40 | BlogTwo: BlogTwoSerializer, 41 | BlogThree: BlogThreeSerializer 42 | } 43 | -------------------------------------------------------------------------------- /example/projects/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-12-08 12:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('contenttypes', '0002_remove_content_type_name'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Project', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('topic', models.CharField(max_length=30)), 23 | ], 24 | options={ 25 | 'abstract': False, 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='ArtProject', 30 | fields=[ 31 | ('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')), 32 | ('artist', models.CharField(max_length=30)), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | }, 37 | bases=('projects.project',), 38 | ), 39 | migrations.CreateModel( 40 | name='ResearchProject', 41 | fields=[ 42 | ('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')), 43 | ('supervisor', models.CharField(max_length=30)), 44 | ], 45 | options={ 46 | 'abstract': False, 47 | }, 48 | bases=('projects.project',), 49 | ), 50 | migrations.AddField( 51 | model_name='project', 52 | name='polymorphic_ctype', 53 | field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_projects.project_set+', to='contenttypes.ContentType'), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | cache: pip 4 | 5 | matrix: 6 | fast_finish: true 7 | include: 8 | - python: 3.6 9 | env: TOXENV=py36-django22-drf38 10 | - python: 3.6 11 | env: TOXENV=py36-django22-drf39 12 | - python: 3.6 13 | env: TOXENV=py36-django22-drf310 14 | - python: 3.6 15 | env: TOXENV=py36-django22-drf311 16 | - python: 3.6 17 | env: TOXENV=py36-django30-drf310 18 | - python: 3.6 19 | env: TOXENV=py36-django30-drf311 20 | - python: 3.6 21 | env: TOXENV=py36-djangomaster-drfmaster 22 | 23 | - python: 3.7 24 | env: TOXENV=py37-django22-drf38 25 | dist: xenial 26 | sudo: required 27 | - python: 3.7 28 | env: TOXENV=py37-django22-drf39 29 | dist: xenial 30 | sudo: required 31 | - python: 3.7 32 | env: TOXENV=py37-django22-drf310 33 | dist: xenial 34 | sudo: required 35 | - python: 3.7 36 | env: TOXENV=py37-django22-drf311 37 | dist: xenial 38 | sudo: required 39 | - python: 3.7 40 | env: TOXENV=py37-django30-drf310 41 | dist: xenial 42 | sudo: required 43 | - python: 3.7 44 | env: TOXENV=py37-django30-drf311 45 | dist: xenial 46 | sudo: required 47 | - python: 3.7 48 | env: TOXENV=py37-djangomaster-drfmaster 49 | dist: xenial 50 | sudo: required 51 | 52 | - python: 3.8 53 | env: TOXENV=py38-django22-drf38 54 | dist: xenial 55 | sudo: required 56 | - python: 3.8 57 | env: TOXENV=py38-django22-drf39 58 | dist: xenial 59 | sudo: required 60 | - python: 3.8 61 | env: TOXENV=py38-django22-drf310 62 | dist: xenial 63 | sudo: required 64 | - python: 3.8 65 | env: TOXENV=py38-django22-drf311 66 | dist: xenial 67 | sudo: required 68 | - python: 3.8 69 | env: TOXENV=py38-django30-drf310 70 | dist: xenial 71 | sudo: required 72 | - python: 3.8 73 | env: TOXENV=py38-django30-drf311 74 | dist: xenial 75 | sudo: required 76 | - python: 3.8 77 | env: TOXENV=py38-djangomaster-drfmaster 78 | dist: xenial 79 | sudo: required 80 | 81 | allow_failures: 82 | - env: TOXENV=py36-djangomaster-drfmaster 83 | - env: TOXENV=py37-djangomaster-drfmaster 84 | - env: TOXENV=py38-djangomaster-drfmaster 85 | 86 | install: 87 | - travis_retry pip install -U tox-travis 88 | 89 | script: 90 | - tox 91 | 92 | after_success: 93 | - travis_retry pip install -U codecov 94 | - codecov 95 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import io 5 | import os 6 | import sys 7 | from shutil import rmtree 8 | 9 | from setuptools import setup, Command 10 | 11 | here = os.path.abspath(os.path.dirname(__file__)) 12 | 13 | # Long description 14 | with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 15 | readme = '\n' + f.read() 16 | 17 | about = {} 18 | # Get meta-data from __version__.py 19 | with open(os.path.join(here, 'rest_polymorphic', '__version__.py')) as f: 20 | exec(f.read(), about) 21 | 22 | 23 | class UploadCommand(Command): 24 | """Support setup.py upload.""" 25 | 26 | description = 'Build and publish the package.' 27 | user_options = [] 28 | 29 | @staticmethod 30 | def status(s): 31 | """Prints things in bold.""" 32 | print('\033[1m{0}\033[0m'.format(s)) 33 | 34 | def initialize_options(self): 35 | pass 36 | 37 | def finalize_options(self): 38 | pass 39 | 40 | def run(self): 41 | try: 42 | self.status('Removing previous builds…') 43 | rmtree(os.path.join(here, 'dist')) 44 | except OSError: 45 | pass 46 | 47 | self.status('Building Source and Wheel (universal) distribution…') 48 | os.system('{0} setup.py sdist bdist_wheel --universal'.format( 49 | sys.executable 50 | )) 51 | 52 | self.status('Uploading the package to PyPi via Twine…') 53 | os.system('twine upload dist/*') 54 | 55 | sys.exit() 56 | 57 | 58 | setup( 59 | name=about['__title__'], 60 | version=about['__version__'], 61 | description=about['__description__'], 62 | long_description=readme, 63 | author=about['__author__'], 64 | author_email=about['__author_email__'], 65 | url=about['__url__'], 66 | license=about['__license__'], 67 | packages=[ 68 | 'rest_polymorphic', 69 | ], 70 | install_requires=[ 71 | 'django', 72 | 'djangorestframework', 73 | 'django-polymorphic', 74 | 'six', 75 | ], 76 | classifiers=[ 77 | # Trove classifiers 78 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 79 | 'License :: OSI Approved :: MIT License', 80 | 'Intended Audience :: Developers', 81 | 'Framework :: Django', 82 | 'Framework :: Django :: 2.2', 83 | 'Framework :: Django :: 3.0', 84 | 'Programming Language :: Python', 85 | 'Programming Language :: Python :: 3.6', 86 | 'Programming Language :: Python :: 3.7', 87 | 'Programming Language :: Python :: 3.8', 88 | 'Programming Language :: Python :: Implementation :: CPython', 89 | 'Programming Language :: Python :: Implementation :: PyPy' 90 | ], 91 | cmdclass={ 92 | 'upload': UploadCommand, 93 | }, 94 | ) 95 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'i=m896-(xjn#dz*kx=m8aa^7y!le5od3$(y-2doym!c%or=e64' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'rest_framework', 42 | 'projects', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'example.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [os.path.join(BASE_DIR, 'templates'), ], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'example.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | -------------------------------------------------------------------------------- /tests/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | 3 | import pytest 4 | 5 | from rest_polymorphic.serializers import PolymorphicSerializer 6 | 7 | from tests.models import BlogBase, BlogOne, BlogTwo 8 | from tests.serializers import BlogPolymorphicSerializer 9 | 10 | pytestmark = pytest.mark.django_db 11 | 12 | 13 | class TestPolymorphicSerializer: 14 | 15 | def test_model_serializer_mapping_is_none(self): 16 | class EmptyPolymorphicSerializer(PolymorphicSerializer): 17 | pass 18 | 19 | with pytest.raises(ImproperlyConfigured) as excinfo: 20 | EmptyPolymorphicSerializer() 21 | 22 | assert str(excinfo.value) == ( 23 | '`EmptyPolymorphicSerializer` is missing a ' 24 | '`EmptyPolymorphicSerializer.model_serializer_mapping` attribute' 25 | ) 26 | 27 | def test_resource_type_field_name_is_not_string(self, mocker): 28 | class NotStringPolymorphicSerializer(PolymorphicSerializer): 29 | model_serializer_mapping = mocker.MagicMock 30 | resource_type_field_name = 1 31 | 32 | with pytest.raises(ImproperlyConfigured) as excinfo: 33 | NotStringPolymorphicSerializer() 34 | 35 | assert str(excinfo.value) == ( 36 | '`NotStringPolymorphicSerializer.resource_type_field_name` must ' 37 | 'be a string' 38 | ) 39 | 40 | def test_each_serializer_has_context(self, mocker): 41 | context = mocker.MagicMock() 42 | serializer = BlogPolymorphicSerializer(context=context) 43 | for inner_serializer in serializer.model_serializer_mapping.values(): 44 | assert inner_serializer.context == context 45 | 46 | def test_serialize(self): 47 | instance = BlogBase.objects.create(name='blog', slug='blog') 48 | serializer = BlogPolymorphicSerializer(instance) 49 | assert serializer.data == { 50 | 'name': 'blog', 51 | 'slug': 'blog', 52 | 'resourcetype': 'BlogBase', 53 | } 54 | 55 | def test_deserialize(self): 56 | data = { 57 | 'name': 'blog', 58 | 'slug': 'blog', 59 | 'resourcetype': 'BlogBase', 60 | } 61 | serializers = BlogPolymorphicSerializer(data=data) 62 | assert serializers.is_valid() 63 | assert serializers.data == data 64 | 65 | def test_deserialize_with_invalid_resourcetype(self): 66 | data = { 67 | 'name': 'blog', 68 | 'resourcetype': 'Invalid', 69 | } 70 | serializers = BlogPolymorphicSerializer(data=data) 71 | assert not serializers.is_valid() 72 | 73 | def test_create(self): 74 | data = [ 75 | { 76 | 'name': 'a', 77 | 'slug': 'a', 78 | 'resourcetype': 'BlogBase' 79 | }, 80 | { 81 | 'name': 'b', 82 | 'slug': 'b', 83 | 'info': 'info', 84 | 'resourcetype': 'BlogOne' 85 | }, 86 | { 87 | 'name': 'c', 88 | 'slug': 'c', 89 | 'resourcetype': 'BlogTwo' 90 | }, 91 | ] 92 | serializer = BlogPolymorphicSerializer(data=data, many=True) 93 | assert serializer.is_valid() 94 | 95 | instances = serializer.save() 96 | assert len(instances) == 3 97 | assert [item.name for item in instances] == ['a', 'b', 'c'] 98 | 99 | assert BlogBase.objects.count() == 3 100 | assert BlogBase.objects.instance_of(BlogOne).count() == 1 101 | assert BlogBase.objects.instance_of(BlogTwo).count() == 1 102 | 103 | assert serializer.data == data 104 | 105 | def test_update(self): 106 | instance = BlogBase.objects.create(name='blog', slug='blog') 107 | data = { 108 | 'name': 'new-blog', 109 | 'slug': 'blog', 110 | 'resourcetype': 'BlogBase' 111 | } 112 | 113 | serializer = BlogPolymorphicSerializer(instance, data=data) 114 | assert serializer.is_valid() 115 | 116 | serializer.save() 117 | assert instance.name == 'new-blog' 118 | assert instance.slug == 'blog' 119 | 120 | def test_partial_update(self): 121 | instance = BlogBase.objects.create(name='blog', slug='blog') 122 | data = { 123 | 'name': 'new-blog', 124 | 'resourcetype': 'BlogBase' 125 | } 126 | 127 | serializer = BlogPolymorphicSerializer( 128 | instance, data=data, partial=True 129 | ) 130 | assert serializer.is_valid() 131 | 132 | serializer.save() 133 | assert instance.name == 'new-blog' 134 | assert instance.slug == 'blog' 135 | 136 | def test_partial_update_without_resourcetype(self): 137 | instance = BlogBase.objects.create(name='blog', slug='blog') 138 | data = {'name': 'new-blog'} 139 | 140 | serializer = BlogPolymorphicSerializer( 141 | instance, data=data, partial=True 142 | ) 143 | assert serializer.is_valid() 144 | 145 | serializer.save() 146 | assert instance.name == 'new-blog' 147 | assert instance.slug == 'blog' 148 | 149 | def test_object_validators_are_applied(self): 150 | data = { 151 | 'name': 'test-blog', 152 | 'slug': 'test-blog-slug', 153 | 'info': 'test-blog-info', 154 | 'about': 'test-blog-about', 155 | 'resourcetype': 'BlogThree' 156 | } 157 | serializer = BlogPolymorphicSerializer(data=data) 158 | assert serializer.is_valid() 159 | serializer.save() 160 | 161 | data['slug'] = 'test-blog-slug-new' 162 | duplicate = BlogPolymorphicSerializer(data=data) 163 | 164 | assert not duplicate.is_valid() 165 | assert 'non_field_errors' in duplicate.errors 166 | err = duplicate.errors['non_field_errors'] 167 | 168 | assert err == ['The fields info, about must make a unique set.'] 169 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/apirobot/django-rest-polymorphic.svg?branch=master 2 | :target: https://travis-ci.org/apirobot/django-rest-polymorphic 3 | 4 | .. image:: https://codecov.io/gh/apirobot/django-rest-polymorphic/branch/master/graph/badge.svg 5 | :target: https://codecov.io/gh/apirobot/django-rest-polymorphic 6 | 7 | .. image:: https://badge.fury.io/py/django-rest-polymorphic.svg 8 | :target: https://badge.fury.io/py/django-rest-polymorphic 9 | 10 | 11 | ======================= 12 | Django REST Polymorphic 13 | ======================= 14 | 15 | Polymorphic serializers for Django REST Framework. 16 | 17 | 18 | Overview 19 | -------- 20 | 21 | ``django-rest-polymorphic`` allows you to easily define serializers for your inherited models that you have created using ``django-polymorphic`` library. 22 | 23 | 24 | Installation 25 | ------------ 26 | 27 | Install using ``pip``: 28 | 29 | .. code-block:: bash 30 | 31 | $ pip install django-rest-polymorphic 32 | 33 | 34 | Usage 35 | ----- 36 | 37 | Define your polymorphic models: 38 | 39 | .. code-block:: python 40 | 41 | # models.py 42 | from django.db import models 43 | from polymorphic.models import PolymorphicModel 44 | 45 | 46 | class Project(PolymorphicModel): 47 | topic = models.CharField(max_length=30) 48 | 49 | 50 | class ArtProject(Project): 51 | artist = models.CharField(max_length=30) 52 | 53 | 54 | class ResearchProject(Project): 55 | supervisor = models.CharField(max_length=30) 56 | 57 | Define serializers for each polymorphic model the way you did it when you used ``django-rest-framework``: 58 | 59 | .. code-block:: python 60 | 61 | # serializers.py 62 | from rest_framework import serializers 63 | from .models import Project, ArtProject, ResearchProject 64 | 65 | 66 | class ProjectSerializer(serializers.ModelSerializer): 67 | class Meta: 68 | model = Project 69 | fields = ('topic', ) 70 | 71 | 72 | class ArtProjectSerializer(serializers.HyperlinkedModelSerializer): 73 | class Meta: 74 | model = ArtProject 75 | fields = ('topic', 'artist', 'url') 76 | extra_kwargs = { 77 | 'url': {'view_name': 'project-detail', 'lookup_field': 'pk'}, 78 | } 79 | 80 | 81 | class ResearchProjectSerializer(serializers.ModelSerializer): 82 | class Meta: 83 | model = ResearchProject 84 | fields = ('topic', 'supervisor') 85 | 86 | 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. 87 | 88 | Then you have to create a polymorphic serializer that serves as a mapper between models and serializers which you have defined above: 89 | 90 | .. code-block:: python 91 | 92 | # serializers.py 93 | from rest_polymorphic.serializers import PolymorphicSerializer 94 | 95 | 96 | class ProjectPolymorphicSerializer(PolymorphicSerializer): 97 | model_serializer_mapping = { 98 | Project: ProjectSerializer, 99 | ArtProject: ArtProjectSerializer, 100 | ResearchProject: ResearchProjectSerializer 101 | } 102 | 103 | Create viewset with serializer_class equals to your polymorphic serializer: 104 | 105 | .. code-block:: python 106 | 107 | # views.py 108 | from rest_framework import viewsets 109 | from .models import Project 110 | from .serializers import ProjectPolymorphicSerializer 111 | 112 | 113 | class ProjectViewSet(viewsets.ModelViewSet): 114 | queryset = Project.objects.all() 115 | serializer_class = ProjectPolymorphicSerializer 116 | 117 | Test it: 118 | 119 | .. code-block:: bash 120 | 121 | $ http GET "http://localhost:8000/projects/" 122 | 123 | .. code-block:: http 124 | 125 | HTTP/1.0 200 OK 126 | Content-Length: 227 127 | Content-Type: application/json 128 | 129 | [ 130 | { 131 | "resourcetype": "Project", 132 | "topic": "John's gathering" 133 | }, 134 | { 135 | "artist": "T. Turner", 136 | "resourcetype": "ArtProject", 137 | "topic": "Sculpting with Tim", 138 | "url": "http://localhost:8000/projects/2/" 139 | }, 140 | { 141 | "resourcetype": "ResearchProject", 142 | "supervisor": "Dr. Winter", 143 | "topic": "Swallow Aerodynamics" 144 | } 145 | ] 146 | 147 | .. code-block:: bash 148 | 149 | $ http POST "http://localhost:8000/projects/" resourcetype="ArtProject" topic="Guernica" artist="Picasso" 150 | 151 | .. code-block:: http 152 | 153 | HTTP/1.0 201 Created 154 | Content-Length: 67 155 | Content-Type: application/json 156 | 157 | { 158 | "artist": "Picasso", 159 | "resourcetype": "ArtProject", 160 | "topic": "Guernica", 161 | "url": "http://localhost:8000/projects/4/" 162 | } 163 | 164 | 165 | Customize resource type 166 | ----------------------- 167 | 168 | 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. 169 | 170 | If you want to change the resource type field name from ``resourcetype`` to something else, you should override ``resource_type_field_name`` attribute: 171 | 172 | .. code-block:: python 173 | 174 | class ProjectPolymorphicSerializer(PolymorphicSerializer): 175 | resource_type_field_name = 'projecttype' 176 | ... 177 | 178 | If you want to change the behavior of resource type, you should override ``to_resource_type`` method: 179 | 180 | .. code-block:: python 181 | 182 | class ProjectPolymorphicSerializer(PolymorphicSerializer): 183 | ... 184 | 185 | def to_resource_type(self, model_or_instance): 186 | return model_or_instance._meta.object_name.lower() 187 | 188 | Now, the request for creating new object will look like this: 189 | 190 | .. code-block:: bash 191 | 192 | $ http POST "http://localhost:8000/projects/" projecttype="artproject" topic="Guernica" artist="Picasso" 193 | -------------------------------------------------------------------------------- /rest_polymorphic/serializers.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from six import string_types 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.db import models 6 | from rest_framework import serializers 7 | from rest_framework.fields import empty 8 | 9 | 10 | class PolymorphicSerializer(serializers.Serializer): 11 | model_serializer_mapping = None 12 | resource_type_field_name = 'resourcetype' 13 | 14 | def __new__(cls, *args, **kwargs): 15 | if cls.model_serializer_mapping is None: 16 | raise ImproperlyConfigured( 17 | '`{cls}` is missing a ' 18 | '`{cls}.model_serializer_mapping` attribute'.format( 19 | cls=cls.__name__ 20 | ) 21 | ) 22 | if not isinstance(cls.resource_type_field_name, string_types): 23 | raise ImproperlyConfigured( 24 | '`{cls}.resource_type_field_name` must be a string'.format( 25 | cls=cls.__name__ 26 | ) 27 | ) 28 | return super(PolymorphicSerializer, cls).__new__(cls, *args, **kwargs) 29 | 30 | def __init__(self, *args, **kwargs): 31 | super(PolymorphicSerializer, self).__init__(*args, **kwargs) 32 | 33 | model_serializer_mapping = self.model_serializer_mapping 34 | self.model_serializer_mapping = {} 35 | self.resource_type_model_mapping = {} 36 | 37 | for model, serializer in model_serializer_mapping.items(): 38 | resource_type = self.to_resource_type(model) 39 | if callable(serializer): 40 | serializer = serializer(*args, **kwargs) 41 | serializer.parent = self 42 | 43 | self.resource_type_model_mapping[resource_type] = model 44 | self.model_serializer_mapping[model] = serializer 45 | 46 | # ---------- 47 | # Public API 48 | 49 | def to_resource_type(self, model_or_instance): 50 | return model_or_instance._meta.object_name 51 | 52 | def to_representation(self, instance): 53 | if isinstance(instance, Mapping): 54 | resource_type = self._get_resource_type_from_mapping(instance) 55 | serializer = self._get_serializer_from_resource_type(resource_type) 56 | else: 57 | resource_type = self.to_resource_type(instance) 58 | serializer = self._get_serializer_from_model_or_instance(instance) 59 | 60 | ret = serializer.to_representation(instance) 61 | ret[self.resource_type_field_name] = resource_type 62 | return ret 63 | 64 | def to_internal_value(self, data): 65 | if self.partial and self.instance: 66 | resource_type = self.to_resource_type(self.instance) 67 | serializer = self._get_serializer_from_model_or_instance(self.instance) 68 | else: 69 | resource_type = self._get_resource_type_from_mapping(data) 70 | serializer = self._get_serializer_from_resource_type(resource_type) 71 | 72 | ret = serializer.to_internal_value(data) 73 | ret[self.resource_type_field_name] = resource_type 74 | return ret 75 | 76 | def create(self, validated_data): 77 | resource_type = validated_data.pop(self.resource_type_field_name) 78 | serializer = self._get_serializer_from_resource_type(resource_type) 79 | return serializer.create(validated_data) 80 | 81 | def update(self, instance, validated_data): 82 | resource_type = validated_data.pop(self.resource_type_field_name) 83 | serializer = self._get_serializer_from_resource_type(resource_type) 84 | return serializer.update(instance, validated_data) 85 | 86 | def is_valid(self, *args, **kwargs): 87 | valid = super(PolymorphicSerializer, self).is_valid(*args, **kwargs) 88 | try: 89 | if self.partial and self.instance: 90 | resource_type = self.to_resource_type(self.instance) 91 | serializer = self._get_serializer_from_model_or_instance(self.instance) 92 | else: 93 | resource_type = self._get_resource_type_from_mapping(self.initial_data) 94 | serializer = self._get_serializer_from_resource_type(resource_type) 95 | 96 | except serializers.ValidationError: 97 | child_valid = False 98 | else: 99 | child_valid = serializer.is_valid(*args, **kwargs) 100 | self._errors.update(serializer.errors) 101 | return valid and child_valid 102 | 103 | def run_validation(self, data=empty): 104 | if self.partial and self.instance: 105 | resource_type = self.to_resource_type(self.instance) 106 | serializer = self._get_serializer_from_model_or_instance(self.instance) 107 | else: 108 | resource_type = self._get_resource_type_from_mapping(data) 109 | serializer = self._get_serializer_from_resource_type(resource_type) 110 | 111 | validated_data = serializer.run_validation(data) 112 | validated_data[self.resource_type_field_name] = resource_type 113 | return validated_data 114 | 115 | # -------------- 116 | # Implementation 117 | 118 | def _to_model(self, model_or_instance): 119 | return (model_or_instance.__class__ 120 | if isinstance(model_or_instance, models.Model) 121 | else model_or_instance) 122 | 123 | def _get_resource_type_from_mapping(self, mapping): 124 | try: 125 | return mapping[self.resource_type_field_name] 126 | except KeyError: 127 | raise serializers.ValidationError({ 128 | self.resource_type_field_name: 'This field is required', 129 | }) 130 | 131 | def _get_serializer_from_model_or_instance(self, model_or_instance): 132 | model = self._to_model(model_or_instance) 133 | 134 | for klass in model.mro(): 135 | if klass in self.model_serializer_mapping: 136 | return self.model_serializer_mapping[klass] 137 | 138 | raise KeyError( 139 | '`{cls}.model_serializer_mapping` is missing ' 140 | 'a corresponding serializer for `{model}` model'.format( 141 | cls=self.__class__.__name__, 142 | model=model.__name__ 143 | ) 144 | ) 145 | 146 | def _get_serializer_from_resource_type(self, resource_type): 147 | try: 148 | model = self.resource_type_model_mapping[resource_type] 149 | except KeyError: 150 | raise serializers.ValidationError({ 151 | self.resource_type_field_name: 'Invalid {0}'.format( 152 | self.resource_type_field_name 153 | ) 154 | }) 155 | 156 | return self._get_serializer_from_model_or_instance(model) 157 | --------------------------------------------------------------------------------