mirror of
https://github.com/django-polymorphic/django-polymorphic.git
synced 2026-02-14 02:30:23 +03:00
integrate django-rest-polymorphic
This commit is contained in:
parent
2ca4926a52
commit
6150696b6c
13
AUTHORS.md
13
AUTHORS.md
|
|
@ -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>
|
||||
|
|
|
|||
7
docs/api/polymorphic.contrib/drf.rst
Normal file
7
docs/api/polymorphic.contrib/drf.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
drf.serializers
|
||||
===============
|
||||
|
||||
.. automodule:: polymorphic.contrib.drf.serializers
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
@ -12,3 +12,4 @@ polymorphic.contrib
|
|||
|
||||
extra_views
|
||||
guardian
|
||||
drf
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
129
docs/integrations/djangorestframework.rst
Normal file
129
docs/integrations/djangorestframework.rst
Normal 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"
|
||||
|
|
@ -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
|
||||
|
||||
14
justfile
14
justfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
50
src/polymorphic/contrib/drf/.gitignore
vendored
50
src/polymorphic/contrib/drf/.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
|
|
@ -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/'
|
||||
|
|
@ -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')),
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE=tests.settings
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
-e .
|
||||
-r requirements/codestyle.txt
|
||||
-r requirements/packaging.txt
|
||||
-r requirements/testing.txt
|
||||
|
|
@ -1 +0,0 @@
|
|||
flake8==3.7.9
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# Twine for secured PyPI uploads.
|
||||
twine==3.1.1
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
pytest==5.4.1
|
||||
pytest-django==3.9.0
|
||||
pytest-mock==3.0.0
|
||||
pytest-cov==2.8.1
|
||||
|
|
@ -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'
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
@ -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')
|
||||
|
|
@ -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.']
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
7
src/polymorphic/tests/examples/integrations/drf/apps.py
Normal file
7
src/polymorphic/tests/examples/integrations/drf/apps.py
Normal 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"
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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',),
|
||||
),
|
||||
]
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -21,4 +21,4 @@ class BlogThree(BlogBase):
|
|||
about = models.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('info', 'about'),)
|
||||
unique_together = (("info", "about"),)
|
||||
|
|
@ -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,
|
||||
}
|
||||
441
src/polymorphic/tests/examples/integrations/drf/test.py
Normal file
441
src/polymorphic/tests/examples/integrations/drf/test.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
33
uv.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user