haven't committed in a while oops. A fuck-ton of HTML stuff

This commit is contained in:
tcaxle
2020-04-13 17:09:38 +01:00
parent cbbb32f163
commit b594e8b803
5339 changed files with 264915 additions and 108 deletions

View File

@@ -0,0 +1,74 @@
Main authors (commit rights to the main repository)
===================================================
* Chris Glass
* Diederik van der Boor
* Charlie Denton
* Jerome Leclanche
Contributors
=============
* Abel Daniel
* Adam Chainz
* Adam Wentz
* Andrew Ingram (contributed setup.py)
* Al Johri
* Alex Alvarez
* Andrew Dodd
* Angel Velasquez
* Austin Matsick
* Ben Konrath
* Bert Constantin
* Bertrand Bordage
* Chad Shryock
* Charles Leifer (python 2.4 compatibility)
* Chris Barna
* Chris Brantley
* Christopher Glass
* David Sanders
* Éric Araujo
* Evan Borgstrom
* Frankie Dintino
* Gavin Wahl
* Germán M. Bravo
* Gonzalo Bustos
* Gregory Avery-Weir
* Hugo Osvaldo Barrera
* Jacob Rief
* James Murty
* Jedediah Smith (proxy models support)
* John Furr
* Jonas Haag
* Jonas Obrist
* Julian Wachholz
* Kamil Bar
* Kelsey Gilmore-Innis
* Kevin Armenat
* Krzysztof Gromadzki
* Krzysztof Nazarewski
* Luis Zárate
* Marius Lueck
* Martin Brochhaus
* Martin Maillard
* Michael Fladischer
* Nick Ward
* Oleg Myltsyn
* Omer Strumpf
* Paweł Adamczak
* Petr Dlouhý
* Sander van Leeuwen
* Sobolev Nikita
* Tadas Dailyda
* Tai Lee
* Tomas Peterka
* Tony Narlock
* Vail Gold
Former authors / maintainers
============================
* Bert Constantin 2009/2010 (Original author, disappeared from the internet :( )

View File

@@ -0,0 +1,32 @@
Copyright (c) 2009 or later by the individual contributors.
Please see the AUTHORS file.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,115 @@
Metadata-Version: 2.1
Name: django-polymorphic
Version: 2.1.2
Summary: Seamless polymorphic inheritance for Django models
Home-page: https://github.com/django-polymorphic/django-polymorphic
Author: Bert Constantin
Author-email: bert.constantin@gmx.de
Maintainer: Christopher Glass
Maintainer-email: tribaal@gmail.com
License: UNKNOWN
Download-URL: https://github.com/django-polymorphic/django-polymorphic/tarball/master
Keywords: django,polymorphic
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 1.11
Classifier: Framework :: Django :: 2.0
Classifier: Framework :: Django :: 2.1
Classifier: Framework :: Django :: 2.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Database
Requires-Dist: Django (>=1.11)
.. image:: https://travis-ci.org/django-polymorphic/django-polymorphic.svg?branch=master
:target: http://travis-ci.org/django-polymorphic/django-polymorphic
.. image:: https://img.shields.io/pypi/v/django-polymorphic.svg
:target: https://pypi.python.org/pypi/django-polymorphic/
.. image:: https://img.shields.io/codecov/c/github/django-polymorphic/django-polymorphic/master.svg
:target: https://codecov.io/github/django-polymorphic/django-polymorphic?branch=master
.. image:: https://readthedocs.org/projects/django-polymorphic/badge/?version=stable
:target: https://django-polymorphic.readthedocs.io/en/stable/
Polymorphic Models for Django
=============================
Django-polymorphic simplifies using inherited models in Django projects.
When a query is made at the base model, the inherited model classes are returned.
When we store models that inherit from a ``Project`` model...
.. code-block:: python
>>> Project.objects.create(topic="Department Party")
>>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner")
>>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
...and want to retrieve all our projects, the subclassed models are returned!
.. code-block:: python
>>> Project.objects.all()
[ <Project: id 1, topic "Department Party">,
<ArtProject: id 2, topic "Painting with Tim", artist "T. Turner">,
<ResearchProject: id 3, topic "Swallow Aerodynamics", supervisor "Dr. Winter"> ]
Using vanilla Django, we get the base class objects, which is rarely what we wanted:
.. code-block:: python
>>> Project.objects.all()
[ <Project: id 1, topic "Department Party">,
<Project: id 2, topic "Painting with Tim">,
<Project: id 3, topic "Swallow Aerodynamics"> ]
This also works when the polymorphic model is accessed via
ForeignKeys, ManyToManyFields or OneToOneFields.
Features
--------
* Full admin integration.
* ORM integration:
* support for ForeignKey, ManyToManyField, OneToOneField descriptors.
* Filtering/ordering of inherited models (``ArtProject___artist``).
* Filtering model types: ``instance_of(...)`` and ``not_instance_of(...)``
* Combining querysets of different models (``qs3 = qs1 | qs2``)
* Support for custom user-defined managers.
* Uses the minumum amount of queries needed to fetch the inherited models.
* Disabling polymorphic behavior when needed.
While *django-polymorphic* makes subclassed models easy to use in Django,
we still encourage to use them with caution. Each subclassed model will require
Django to perform an ``INNER JOIN`` to fetch the model fields from the database.
While taking this in mind, there are valid reasons for using subclassed models.
That's what this library is designed for!
The current release of *django-polymorphic* supports Django 1.11, 2.0, 2.1, 2.2 and Python 2.7 and 3.5+ is supported.
For older Django versions, install *django-polymorphic==1.3*.
For more information, see the `documentation at Read the Docs <https://django-polymorphic.readthedocs.io/>`_.
Installation
------------
Install using ``pip``\ ...
.. code:: bash
$ pip install django-polymorphic
License
=======
Django-polymorphic uses the same license as Django (BSD-like).

View File

@@ -0,0 +1,87 @@
django_polymorphic-2.1.2.dist-info/AUTHORS.rst,sha256=c53Mk_y783gPflNMYmWIxJNJVgqd_iry3njjwruQkBM,1318
django_polymorphic-2.1.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
django_polymorphic-2.1.2.dist-info/LICENSE,sha256=ytx-RbmsZDzs1srmPx9FaEFl39W1i7lUK5GPGM1H1VE,1571
django_polymorphic-2.1.2.dist-info/METADATA,sha256=Shl0gSzVRSkQMMr4uhqVC0EMWkjl1mxRJHN_b51OdjY,4493
django_polymorphic-2.1.2.dist-info/RECORD,,
django_polymorphic-2.1.2.dist-info/WHEEL,sha256=h_aVn5OB2IERUjMbi2pucmR_zzWJtk303YXvhh60NJ8,110
django_polymorphic-2.1.2.dist-info/top_level.txt,sha256=xxMuTi2Xsi-PV5ovvJoCMkUWdX8h8-tO-cmQGoulejA,12
polymorphic/__init__.py,sha256=YHXdgvJrZpcbbeETeXFg16v6AKA9_c1SLt-L7lonxSE,426
polymorphic/__pycache__/__init__.cpython-38.pyc,,
polymorphic/__pycache__/base.cpython-38.pyc,,
polymorphic/__pycache__/compat.cpython-38.pyc,,
polymorphic/__pycache__/managers.cpython-38.pyc,,
polymorphic/__pycache__/models.cpython-38.pyc,,
polymorphic/__pycache__/query.cpython-38.pyc,,
polymorphic/__pycache__/query_translate.cpython-38.pyc,,
polymorphic/__pycache__/showfields.cpython-38.pyc,,
polymorphic/__pycache__/utils.cpython-38.pyc,,
polymorphic/admin/__init__.py,sha256=krwQsb4lM9wXC-qL7L4klgVUKsW_uEgTyPuFqOh2byA,1458
polymorphic/admin/__pycache__/__init__.cpython-38.pyc,,
polymorphic/admin/__pycache__/childadmin.cpython-38.pyc,,
polymorphic/admin/__pycache__/filters.cpython-38.pyc,,
polymorphic/admin/__pycache__/forms.cpython-38.pyc,,
polymorphic/admin/__pycache__/generic.cpython-38.pyc,,
polymorphic/admin/__pycache__/helpers.cpython-38.pyc,,
polymorphic/admin/__pycache__/inlines.cpython-38.pyc,,
polymorphic/admin/__pycache__/parentadmin.cpython-38.pyc,,
polymorphic/admin/childadmin.py,sha256=2HRhCO_5HPGGURzCaGLou5gtq6M2LZ5fa5OyBkYsSqY,9971
polymorphic/admin/filters.py,sha256=GxC28qm_K5XY-Cj34_IOQ4uwKSo3ZwoVpgVbb-Ku6nA,1230
polymorphic/admin/forms.py,sha256=s-uNTjDjhNAXllTKyPIIhB_HM6bNM4vd5HarK1EWg2Y,717
polymorphic/admin/generic.py,sha256=xt9PcgDG53XP4qYwbf5Eff9yh07_BLcuxf8-b0HUXPs,2684
polymorphic/admin/helpers.py,sha256=de_YUBmLl-qGPNy5Y4iuxxfado4bfx4U1P1B4bWHQW0,5764
polymorphic/admin/inlines.py,sha256=Otn49x7Iu_rWBGuBQDUrF5wHJ1fI9A5U9rzg_O83SDQ,10632
polymorphic/admin/parentadmin.py,sha256=XD8kmk-As42tlZxAcAVyTI9UR5atADVqgQyzeZJvkso,15830
polymorphic/base.py,sha256=J6rfZgARe_mM8Aka3zv_j9_roGmxig-C8IODi2XrjWI,9177
polymorphic/compat.py,sha256=yTkkkM_tBzLKfTUIN_xMltcbEO2jbyQIQiXYyLVOmpk,1268
polymorphic/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
polymorphic/contrib/__pycache__/__init__.cpython-38.pyc,,
polymorphic/contrib/__pycache__/extra_views.cpython-38.pyc,,
polymorphic/contrib/__pycache__/guardian.cpython-38.pyc,,
polymorphic/contrib/extra_views.py,sha256=R-IimQuO2lzhiLEbjZ3gU0HcLwi1puzFe_OPEhe20wo,4149
polymorphic/contrib/guardian.py,sha256=SdMHwBvTeadXKH3LQULZikOm57Ep7RzYkSJNuak9HKU,1339
polymorphic/formsets/__init__.py,sha256=L7N002YJ0cyiYzVYcnfnOafnfjsPJbUJrV4qWSlcAlU,1391
polymorphic/formsets/__pycache__/__init__.cpython-38.pyc,,
polymorphic/formsets/__pycache__/generic.cpython-38.pyc,,
polymorphic/formsets/__pycache__/models.cpython-38.pyc,,
polymorphic/formsets/__pycache__/utils.cpython-38.pyc,,
polymorphic/formsets/generic.py,sha256=CKxe1sCCXyyCgQyBaPI0ykeEQzM4U1zfPRFkzbftqo0,4308
polymorphic/formsets/models.py,sha256=RuF1aDTQU5W8EXf_LScUlALKAOd0um1tSEDxGgK6CNU,15528
polymorphic/formsets/utils.py,sha256=tbeXzywmXb3GE7ofmPQuW4H46yUTo639Fr5yH6dXZf0,506
polymorphic/locale/en/LC_MESSAGES/django.po,sha256=SvbpsHUL02r6xjgDaLB02O9057o8v5AvMnaH2vKQX5c,711
polymorphic/locale/es/LC_MESSAGES/django.po,sha256=dDAh0LBgWqwoZkeGKKgPFjfI9B--LM37ZWjvmRArEfY,754
polymorphic/locale/fr/LC_MESSAGES/django.po,sha256=XYn4FlF6XVYvgcxqfLjISO2h8opk5HJpPduLPFJrEIY,889
polymorphic/managers.py,sha256=JVRQ5t9xlATM_l25AwbBCS0m0ZAXbj2twVgNvFBShvg,1703
polymorphic/models.py,sha256=2idJ3Rw9frxIMtMBBYyM_Mne6uuAgyptkTT-S_876Ho,11571
polymorphic/query.py,sha256=PV71zWRaYWjUCwU5eajuua5bTsW3T5xSxSleDEFh4ms,22650
polymorphic/query_translate.py,sha256=FxGDWPSaF4FuRgM-ombfZfyN3J8muYnbKirhsDBLoW8,11365
polymorphic/showfields.py,sha256=IntwIFGmi3DWOnCDnD-QIzbiKyHS4wcb6Z28rZx5DdI,6407
polymorphic/static/polymorphic/css/polymorphic_inlines.css,sha256=Wk9FaeCedIpRMn2QllZuzT8HDQrpLxKyf84XIA55Kd4,560
polymorphic/static/polymorphic/js/polymorphic_inlines.js,sha256=NNHyyi2SQdeJ6sEYP1iN0W3xpNmN5b5VdeWCAO2TWW0,15521
polymorphic/templates/admin/polymorphic/add_type_form.html,sha256=apbNTko9uauGkS-zVFanZ4ISNT4yPnePYROo0c-0wp8,291
polymorphic/templates/admin/polymorphic/change_form.html,sha256=OcXpPDD6NxBKN0p1JVvW1NnRSesM19a_aL5cBr9aJDA,190
polymorphic/templates/admin/polymorphic/delete_confirmation.html,sha256=NViKgTfac5yf1uA0oy7ACh-v9VDU3lBZ-9dYwmXz_cw,198
polymorphic/templates/admin/polymorphic/edit_inline/stacked.html,sha256=SQsWCL59Ew8TVp_Io6ZiezETh1idQIMwPjChS9M0pdc,2379
polymorphic/templates/admin/polymorphic/object_history.html,sha256=hU66_oJ1L1Nx9_gW0oOOBTAfdkikKx9c69VAigUCAq0,193
polymorphic/templatetags/__init__.py,sha256=V_qwuTdJVgYHdfZI0hKMjsTk792tO1usmGQIp5l7fIg,2880
polymorphic/templatetags/__pycache__/__init__.cpython-38.pyc,,
polymorphic/templatetags/__pycache__/polymorphic_admin_tags.cpython-38.pyc,,
polymorphic/templatetags/__pycache__/polymorphic_formset_tags.cpython-38.pyc,,
polymorphic/templatetags/polymorphic_admin_tags.py,sha256=9KpcSIbUC09hb2fVMz9iqpl-48cjxpOkYAOLgwbA6og,1791
polymorphic/templatetags/polymorphic_formset_tags.py,sha256=ghvvdPNmJ5MaeRUBFaKltMBmGeoDiD4TO0_tDH4LM60,2139
polymorphic/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
polymorphic/tests/__pycache__/__init__.cpython-38.pyc,,
polymorphic/tests/__pycache__/test_admin.cpython-38.pyc,,
polymorphic/tests/__pycache__/test_multidb.cpython-38.pyc,,
polymorphic/tests/__pycache__/test_orm.cpython-38.pyc,,
polymorphic/tests/__pycache__/test_regression.cpython-38.pyc,,
polymorphic/tests/__pycache__/test_utils.cpython-38.pyc,,
polymorphic/tests/migrations/0001_initial.py,sha256=E02Z71CqGCDS4YbTsaM8LRpcKh29gAGq3NaYA_-xrxQ,73822
polymorphic/tests/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
polymorphic/tests/migrations/__pycache__/0001_initial.cpython-38.pyc,,
polymorphic/tests/migrations/__pycache__/__init__.cpython-38.pyc,,
polymorphic/tests/test_admin.py,sha256=LhBmC3xkCk4CgOAAoXgLoqw0cPHiToz8-qY2hOc9gcM,4799
polymorphic/tests/test_multidb.py,sha256=4xrGbH3TPChXhKgEDLlG9gNpgDxpnJ9-axlltVti0BU,4749
polymorphic/tests/test_orm.py,sha256=yqzLAMMYb8o_jC4akHYn6bANaT4Ju3zx8Np7zd738xY,48950
polymorphic/tests/test_regression.py,sha256=Y-7Q95nbMLctQPiONkA5iaraRLayAi-fmDi2xrh9caI,907
polymorphic/tests/test_utils.py,sha256=C6Jp1MTv4mJ_4q0YjFE9Ri9qpPIOM-21nGGcWyse818,2693
polymorphic/utils.py,sha256=PU-gG9k-YLJ6KPwqOtsh_YgJ4P9gsFph2IiHi2--aPo,2461

View File

@@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.33.4)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
"""
Seamless Polymorphic Inheritance for Django Models
Copyright:
This code and affiliated files are (C) by Bert Constantin and individual contributors.
Please see LICENSE and AUTHORS for more information.
"""
import pkg_resources
try:
__version__ = pkg_resources.require("django-polymorphic")[0].version
except pkg_resources.DistributionNotFound:
__version__ = None # for RTD among others

View File

@@ -0,0 +1,40 @@
"""
ModelAdmin code to display polymorphic models.
The admin consists of a parent admin (which shows in the admin with a list),
and a child admin (which is used internally to show the edit/delete dialog).
"""
# Admins for the regular models
from .parentadmin import PolymorphicParentModelAdmin # noqa
from .childadmin import PolymorphicChildModelAdmin
from .filters import PolymorphicChildModelFilter
# Utils
from .forms import PolymorphicModelChoiceForm
# Expose generic admin features too. There is no need to split those
# as the admin already relies on contenttypes.
from .generic import GenericPolymorphicInlineModelAdmin # base class
from .generic import GenericStackedPolymorphicInline # stacked inline
# Helpers for the inlines
from .helpers import PolymorphicInlineSupportMixin # mixin for the regular model admin!
from .helpers import PolymorphicInlineAdminForm, PolymorphicInlineAdminFormSet
# Inlines
from .inlines import PolymorphicInlineModelAdmin # base class
from .inlines import StackedPolymorphicInline # stacked inline
__all__ = (
"PolymorphicParentModelAdmin",
"PolymorphicChildModelAdmin",
"PolymorphicModelChoiceForm",
"PolymorphicChildModelFilter",
"PolymorphicInlineAdminForm",
"PolymorphicInlineAdminFormSet",
"PolymorphicInlineSupportMixin",
"PolymorphicInlineModelAdmin",
"StackedPolymorphicInline",
"GenericPolymorphicInlineModelAdmin",
"GenericStackedPolymorphicInline",
)

View File

@@ -0,0 +1,246 @@
"""
The child admin displays the change/delete view of the subclass model.
"""
import inspect
from django.contrib import admin
from django.urls import resolve
from django.utils.translation import ugettext_lazy as _
from polymorphic.utils import get_base_polymorphic_model
from ..admin import PolymorphicParentModelAdmin
class ParentAdminNotRegistered(RuntimeError):
"The admin site for the model is not registered."
class PolymorphicChildModelAdmin(admin.ModelAdmin):
"""
The *optional* base class for the admin interface of derived models.
This base class defines some convenience behavior for the admin interface:
* It corrects the breadcrumbs in the admin pages.
* It adds the base model to the template lookup paths.
* It allows to set ``base_form`` so the derived class will automatically include other fields in the form.
* It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields.
"""
#: The base model that the class uses (auto-detected if not set explicitly)
base_model = None
#: By setting ``base_form`` instead of ``form``, any subclass fields are automatically added to the form.
#: This is useful when your model admin class is inherited by others.
base_form = None
#: By setting ``base_fieldsets`` instead of ``fieldsets``,
#: any subclass fields can be automatically added.
#: This is useful when your model admin class is inherited by others.
base_fieldsets = None
#: Default title for extra fieldset
extra_fieldset_title = _("Contents")
#: Whether the child admin model should be visible in the admin index page.
show_in_index = False
def __init__(self, model, admin_site, *args, **kwargs):
super(PolymorphicChildModelAdmin, self).__init__(
model, admin_site, *args, **kwargs
)
if self.base_model is None:
self.base_model = get_base_polymorphic_model(model)
def get_form(self, request, obj=None, **kwargs):
# The django admin validation requires the form to have a 'class Meta: model = ..'
# attribute, or it will complain that the fields are missing.
# However, this enforces all derived ModelAdmin classes to redefine the model as well,
# because they need to explicitly set the model again - it will stick with the base model.
#
# Instead, pass the form unchecked here, because the standard ModelForm will just work.
# If the derived class sets the model explicitly, respect that setting.
kwargs.setdefault("form", self.base_form or self.form)
# prevent infinite recursion when this is called from get_subclass_fields
if not self.fieldsets and not self.fields:
kwargs.setdefault("fields", "__all__")
return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs)
def get_model_perms(self, request):
match = resolve(request.path)
if (
not self.show_in_index
and match.app_name == "admin"
and match.url_name in ("index", "app_list")
):
return {"add": False, "change": False, "delete": False}
return super(PolymorphicChildModelAdmin, self).get_model_perms(request)
@property
def change_form_template(self):
opts = self.model._meta
app_label = opts.app_label
# Pass the base options
base_opts = self.base_model._meta
base_app_label = base_opts.app_label
return [
"admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
"admin/%s/change_form.html" % app_label,
# Added:
"admin/%s/%s/change_form.html"
% (base_app_label, base_opts.object_name.lower()),
"admin/%s/change_form.html" % base_app_label,
"admin/polymorphic/change_form.html",
"admin/change_form.html",
]
@property
def delete_confirmation_template(self):
opts = self.model._meta
app_label = opts.app_label
# Pass the base options
base_opts = self.base_model._meta
base_app_label = base_opts.app_label
return [
"admin/%s/%s/delete_confirmation.html"
% (app_label, opts.object_name.lower()),
"admin/%s/delete_confirmation.html" % app_label,
# Added:
"admin/%s/%s/delete_confirmation.html"
% (base_app_label, base_opts.object_name.lower()),
"admin/%s/delete_confirmation.html" % base_app_label,
"admin/polymorphic/delete_confirmation.html",
"admin/delete_confirmation.html",
]
@property
def object_history_template(self):
opts = self.model._meta
app_label = opts.app_label
# Pass the base options
base_opts = self.base_model._meta
base_app_label = base_opts.app_label
return [
"admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
"admin/%s/object_history.html" % app_label,
# Added:
"admin/%s/%s/object_history.html"
% (base_app_label, base_opts.object_name.lower()),
"admin/%s/object_history.html" % base_app_label,
"admin/polymorphic/object_history.html",
"admin/object_history.html",
]
def _get_parent_admin(self):
# this returns parent admin instance on which to call response_post_save methods
parent_model = self.model._meta.get_field("polymorphic_ctype").model
if parent_model == self.model:
# when parent_model is in among child_models, just return super instance
return super(PolymorphicChildModelAdmin, self)
try:
return self.admin_site._registry[parent_model]
except KeyError:
# Admin is not registered for polymorphic_ctype model, but perhaps it's registered
# for a intermediate proxy model, between the parent_model and this model.
for klass in inspect.getmro(self.model):
if not issubclass(klass, parent_model):
continue # e.g. found a mixin.
# Fetch admin instance for model class, see if it's a possible candidate.
model_admin = self.admin_site._registry.get(klass)
if model_admin is not None and isinstance(
model_admin, PolymorphicParentModelAdmin
):
return model_admin # Success!
# If we get this far without returning there is no admin available
raise ParentAdminNotRegistered(
"No parent admin was registered for a '{0}' model.".format(parent_model)
)
def response_post_save_add(self, request, obj):
return self._get_parent_admin().response_post_save_add(request, obj)
def response_post_save_change(self, request, obj):
return self._get_parent_admin().response_post_save_change(request, obj)
def render_change_form(
self, request, context, add=False, change=False, form_url="", obj=None
):
context.update({"base_opts": self.base_model._meta})
return super(PolymorphicChildModelAdmin, self).render_change_form(
request, context, add=add, change=change, form_url=form_url, obj=obj
)
def delete_view(self, request, object_id, context=None):
extra_context = {"base_opts": self.base_model._meta}
return super(PolymorphicChildModelAdmin, self).delete_view(
request, object_id, extra_context
)
def history_view(self, request, object_id, extra_context=None):
# Make sure the history view can also display polymorphic breadcrumbs
context = {"base_opts": self.base_model._meta}
if extra_context:
context.update(extra_context)
return super(PolymorphicChildModelAdmin, self).history_view(
request, object_id, extra_context=context
)
# ---- Extra: improving the form/fieldset default display ----
def get_base_fieldsets(self, request, obj=None):
return self.base_fieldsets
def get_fieldsets(self, request, obj=None):
base_fieldsets = self.get_base_fieldsets(request, obj)
# If subclass declares fieldsets or fields, this is respected
if self.fieldsets or self.fields or not self.base_fieldsets:
return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj)
# Have a reasonable default fieldsets,
# where the subclass fields are automatically included.
other_fields = self.get_subclass_fields(request, obj)
if other_fields:
return (
base_fieldsets[0],
(self.extra_fieldset_title, {"fields": other_fields}),
) + base_fieldsets[1:]
else:
return base_fieldsets
def get_subclass_fields(self, request, obj=None):
# Find out how many fields would really be on the form,
# if it weren't restricted by declared fields.
exclude = list(self.exclude or [])
exclude.extend(self.get_readonly_fields(request, obj))
# By not declaring the fields/form in the base class,
# get_form() will populate the form with all available fields.
form = self.get_form(request, obj, exclude=exclude)
subclass_fields = list(form.base_fields.keys()) + list(
self.get_readonly_fields(request, obj)
)
# Find which fields are not part of the common fields.
for fieldset in self.get_base_fieldsets(request, obj):
for field in fieldset[1]["fields"]:
try:
subclass_fields.remove(field)
except ValueError:
pass # field not found in form, Django will raise exception later.
return subclass_fields

View File

@@ -0,0 +1,39 @@
from django.contrib import admin
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _
class PolymorphicChildModelFilter(admin.SimpleListFilter):
"""
An admin list filter for the PolymorphicParentModelAdmin which enables
filtering by its child models.
This can be used in the parent admin:
.. code-block:: python
list_filter = (PolymorphicChildModelFilter,)
"""
title = _("Type")
parameter_name = "polymorphic_ctype"
def lookups(self, request, model_admin):
return model_admin.get_child_type_choices(request, "change")
def queryset(self, request, queryset):
try:
value = int(self.value())
except TypeError:
value = None
if value:
# ensure the content type is allowed
for choice_value, _ in self.lookup_choices:
if choice_value == value:
return queryset.filter(polymorphic_ctype_id=choice_value)
raise PermissionDenied(
'Invalid ContentType "{0}". It must be registered as child model.'.format(
value
)
)
return queryset

View File

@@ -0,0 +1,21 @@
from django import forms
from django.contrib.admin.widgets import AdminRadioSelect
from django.utils.translation import ugettext_lazy as _
class PolymorphicModelChoiceForm(forms.Form):
"""
The default form for the ``add_type_form``. Can be overwritten and replaced.
"""
#: Define the label for the radiofield
type_label = _("Type")
ct_id = forms.ChoiceField(
label=type_label, widget=AdminRadioSelect(attrs={"class": "radiolist"})
)
def __init__(self, *args, **kwargs):
# Allow to easily redefine the label (a commonly expected usecase)
super(PolymorphicModelChoiceForm, self).__init__(*args, **kwargs)
self.fields["ct_id"].label = self.type_label

View File

@@ -0,0 +1,74 @@
from django.contrib.contenttypes.admin import GenericInlineModelAdmin
from django.contrib.contenttypes.models import ContentType
from django.utils.functional import cached_property
from polymorphic.formsets import (
BaseGenericPolymorphicInlineFormSet,
GenericPolymorphicFormSetChild,
polymorphic_child_forms_factory,
)
from .inlines import PolymorphicInlineModelAdmin
class GenericPolymorphicInlineModelAdmin(
PolymorphicInlineModelAdmin, GenericInlineModelAdmin
):
"""
Base class for variation of inlines based on generic foreign keys.
"""
#: The formset class
formset = BaseGenericPolymorphicInlineFormSet
def get_formset(self, request, obj=None, **kwargs):
"""
Construct the generic inline formset class.
"""
# Construct the FormSet class. This is almost the same as parent version,
# except that a different super is called so generic_inlineformset_factory() is used.
# NOTE that generic_inlineformset_factory() also makes sure the GFK fields are excluded in the form.
FormSet = GenericInlineModelAdmin.get_formset(self, request, obj=obj, **kwargs)
FormSet.child_forms = polymorphic_child_forms_factory(
formset_children=self.get_formset_children(request, obj=obj)
)
return FormSet
class Child(PolymorphicInlineModelAdmin.Child):
"""
Variation for generic inlines.
"""
# Make sure that the GFK fields are excluded from the child forms
formset_child = GenericPolymorphicFormSetChild
ct_field = "content_type"
ct_fk_field = "object_id"
@cached_property
def content_type(self):
"""
Expose the ContentType that the child relates to.
This can be used for the ``polymorphic_ctype`` field.
"""
return ContentType.objects.get_for_model(
self.model, for_concrete_model=False
)
def get_formset_child(self, request, obj=None, **kwargs):
# Similar to GenericInlineModelAdmin.get_formset(),
# make sure the GFK is automatically excluded from the form
defaults = {"ct_field": self.ct_field, "fk_field": self.ct_fk_field}
defaults.update(kwargs)
return super(
GenericPolymorphicInlineModelAdmin.Child, self
).get_formset_child(request, obj=obj, **defaults)
class GenericStackedPolymorphicInline(GenericPolymorphicInlineModelAdmin):
"""
The stacked layout for generic inlines.
"""
#: The default template to use.
template = "admin/polymorphic/edit_inline/stacked.html"

View File

@@ -0,0 +1,146 @@
"""
Rendering utils for admin forms;
This makes sure that admin fieldsets/layout settings are exported to the template.
"""
import json
from django.contrib.admin.helpers import AdminField, InlineAdminForm, InlineAdminFormSet
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext
from polymorphic.formsets import BasePolymorphicModelFormSet
class PolymorphicInlineAdminForm(InlineAdminForm):
"""
Expose the admin configuration for a form
"""
def polymorphic_ctype_field(self):
return AdminField(self.form, "polymorphic_ctype", False)
@property
def is_empty(self):
return "__prefix__" in self.form.prefix
class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
"""
Internally used class to expose the formset in the template.
"""
def __init__(self, *args, **kwargs):
# Assigned later via PolymorphicInlineSupportMixin later.
self.request = kwargs.pop("request", None)
self.obj = kwargs.pop("obj", None)
super(PolymorphicInlineAdminFormSet, self).__init__(*args, **kwargs)
def __iter__(self):
"""
Output all forms using the proper subtype settings.
"""
for form, original in zip(
self.formset.initial_forms, self.formset.get_queryset()
):
# Output the form
model = original.get_real_instance_class()
child_inline = self.opts.get_child_inline_instance(model)
view_on_site_url = self.opts.get_view_on_site_url(original)
yield PolymorphicInlineAdminForm(
formset=self.formset,
form=form,
fieldsets=self.get_child_fieldsets(child_inline),
prepopulated_fields=self.get_child_prepopulated_fields(child_inline),
original=original,
readonly_fields=self.get_child_readonly_fields(child_inline),
model_admin=child_inline,
view_on_site_url=view_on_site_url,
)
# Extra rows, and empty prefixed forms.
for form in self.formset.extra_forms + self.formset.empty_forms:
model = form._meta.model
child_inline = self.opts.get_child_inline_instance(model)
yield PolymorphicInlineAdminForm(
formset=self.formset,
form=form,
fieldsets=self.get_child_fieldsets(child_inline),
prepopulated_fields=self.get_child_prepopulated_fields(child_inline),
original=None,
readonly_fields=self.get_child_readonly_fields(child_inline),
model_admin=child_inline,
)
def get_child_fieldsets(self, child_inline):
return list(child_inline.get_fieldsets(self.request, self.obj) or ())
def get_child_readonly_fields(self, child_inline):
return list(child_inline.get_readonly_fields(self.request, self.obj))
def get_child_prepopulated_fields(self, child_inline):
fields = self.prepopulated_fields.copy()
fields.update(child_inline.get_prepopulated_fields(self.request, self.obj))
return fields
def inline_formset_data(self):
"""
A JavaScript data structure for the JavaScript code
This overrides the default Django version to add the ``childTypes`` data.
"""
verbose_name = self.opts.verbose_name
return json.dumps(
{
"name": "#%s" % self.formset.prefix,
"options": {
"prefix": self.formset.prefix,
"addText": ugettext("Add another %(verbose_name)s")
% {"verbose_name": capfirst(verbose_name)},
"childTypes": [
{
"type": model._meta.model_name,
"name": force_text(model._meta.verbose_name),
}
for model in self.formset.child_forms.keys()
],
"deleteText": ugettext("Remove"),
},
}
)
class PolymorphicInlineSupportMixin(object):
"""
A Mixin to add to the regular admin, so it can work with our polymorphic inlines.
This mixin needs to be included in the admin that hosts the ``inlines``.
It makes sure the generated admin forms have different fieldsets/fields
depending on the polymorphic type of the form instance.
This is achieved by overwriting :func:`get_inline_formsets` to return
an :class:`PolymorphicInlineAdminFormSet` instead of a standard Django
:class:`~django.contrib.admin.helpers.InlineAdminFormSet` for the polymorphic formsets.
"""
def get_inline_formsets(
self, request, formsets, inline_instances, obj=None, *args, **kwargs
):
"""
Overwritten version to produce the proper admin wrapping for the
polymorphic inline formset. This fixes the media and form appearance
of the inline polymorphic models.
"""
inline_admin_formsets = super(
PolymorphicInlineSupportMixin, self
).get_inline_formsets(request, formsets, inline_instances, obj=obj)
for admin_formset in inline_admin_formsets:
if isinstance(admin_formset.formset, BasePolymorphicModelFormSet):
# This is a polymorphic formset, which belongs to our inline.
# Downcast the admin wrapper that generates the form fields.
admin_formset.__class__ = PolymorphicInlineAdminFormSet
admin_formset.request = request
admin_formset.obj = obj
return inline_admin_formsets

View File

@@ -0,0 +1,275 @@
"""
Django Admin support for polymorphic inlines.
Each row in the inline can correspond with a different subclass.
"""
from functools import partial
from django.conf import settings
from django.contrib.admin.options import InlineModelAdmin
from django.contrib.admin.utils import flatten_fieldsets
from django.core.exceptions import ImproperlyConfigured
from django.forms import Media
from polymorphic.formsets import (
BasePolymorphicInlineFormSet,
PolymorphicFormSetChild,
UnsupportedChildType,
polymorphic_child_forms_factory,
)
from polymorphic.formsets.utils import add_media
from .helpers import PolymorphicInlineSupportMixin
class PolymorphicInlineModelAdmin(InlineModelAdmin):
"""
A polymorphic inline, where each formset row can be a different form.
Note that:
* Permissions are only checked on the base model.
* The child inlines can't override the base model fields, only this parent inline can do that.
"""
formset = BasePolymorphicInlineFormSet
#: The extra media to add for the polymorphic inlines effect.
#: This can be redefined for subclasses.
polymorphic_media = Media(
js=(
"admin/js/vendor/jquery/jquery{}.js".format(
"" if settings.DEBUG else ".min"
),
"admin/js/jquery.init.js",
"polymorphic/js/polymorphic_inlines.js",
),
css={"all": ("polymorphic/css/polymorphic_inlines.css",)},
)
#: The extra forms to show
#: By default there are no 'extra' forms as the desired type is unknown.
#: Instead, add each new item using JavaScript that first offers a type-selection.
extra = 0
#: Inlines for all model sub types that can be displayed in this inline.
#: Each row is a :class:`PolymorphicInlineModelAdmin.Child`
child_inlines = ()
def __init__(self, parent_model, admin_site):
super(PolymorphicInlineModelAdmin, self).__init__(parent_model, admin_site)
# Extra check to avoid confusion
# While we could monkeypatch the admin here, better stay explicit.
parent_admin = admin_site._registry.get(parent_model, None)
if parent_admin is not None: # Can be None during check
if not isinstance(parent_admin, PolymorphicInlineSupportMixin):
raise ImproperlyConfigured(
"To use polymorphic inlines, add the `PolymorphicInlineSupportMixin` mixin "
"to the ModelAdmin that hosts the inline."
)
# While the inline is created per request, the 'request' object is not known here.
# Hence, creating all child inlines unconditionally, without checking permissions.
self.child_inline_instances = self.get_child_inline_instances()
# Create a lookup table
self._child_inlines_lookup = {}
for child_inline in self.child_inline_instances:
self._child_inlines_lookup[child_inline.model] = child_inline
def get_child_inline_instances(self):
"""
:rtype List[PolymorphicInlineModelAdmin.Child]
"""
instances = []
for ChildInlineType in self.child_inlines:
instances.append(ChildInlineType(parent_inline=self))
return instances
def get_child_inline_instance(self, model):
"""
Find the child inline for a given model.
:rtype: PolymorphicInlineModelAdmin.Child
"""
try:
return self._child_inlines_lookup[model]
except KeyError:
raise UnsupportedChildType(
"Model '{0}' not found in child_inlines".format(model.__name__)
)
def get_formset(self, request, obj=None, **kwargs):
"""
Construct the inline formset class.
This passes all class attributes to the formset.
:rtype: type
"""
# Construct the FormSet class
FormSet = super(PolymorphicInlineModelAdmin, self).get_formset(
request, obj=obj, **kwargs
)
# Instead of completely redefining super().get_formset(), we use
# the regular inlineformset_factory(), and amend that with our extra bits.
# This code line is the essence of what polymorphic_inlineformset_factory() does.
FormSet.child_forms = polymorphic_child_forms_factory(
formset_children=self.get_formset_children(request, obj=obj)
)
return FormSet
def get_formset_children(self, request, obj=None):
"""
The formset 'children' provide the details for all child models that are part of this formset.
It provides a stripped version of the modelform/formset factory methods.
"""
formset_children = []
for child_inline in self.child_inline_instances:
# TODO: the children can be limited here per request based on permissions.
formset_children.append(child_inline.get_formset_child(request, obj=obj))
return formset_children
def get_fieldsets(self, request, obj=None):
"""
Hook for specifying fieldsets.
"""
if self.fieldsets:
return self.fieldsets
else:
return [] # Avoid exposing fields to the child
def get_fields(self, request, obj=None):
if self.fields:
return self.fields
else:
return [] # Avoid exposing fields to the child
@property
def media(self):
# The media of the inline focuses on the admin settings,
# whether to expose the scripts for filter_horizontal etc..
# The admin helper exposes the inline + formset media.
base_media = super(PolymorphicInlineModelAdmin, self).media
all_media = Media()
add_media(all_media, base_media)
# Add all media of the child inline instances
for child_instance in self.child_inline_instances:
child_media = child_instance.media
# Avoid adding the same media object again and again
if (
child_media._css != base_media._css
and child_media._js != base_media._js
):
add_media(all_media, child_media)
add_media(all_media, self.polymorphic_media)
return all_media
class Child(InlineModelAdmin):
"""
The child inline; which allows configuring the admin options
for the child appearance.
Note that not all options will be honored by the parent, notably the formset options:
* :attr:`extra`
* :attr:`min_num`
* :attr:`max_num`
The model form options however, will all be read.
"""
formset_child = PolymorphicFormSetChild
extra = 0 # TODO: currently unused for the children.
def __init__(self, parent_inline):
self.parent_inline = parent_inline
super(PolymorphicInlineModelAdmin.Child, self).__init__(
parent_inline.parent_model, parent_inline.admin_site
)
def get_formset(self, request, obj=None, **kwargs):
# The child inline is only used to construct the form,
# and allow to override the form field attributes.
# The formset is created by the parent inline.
raise RuntimeError("The child get_formset() is not used.")
def get_fields(self, request, obj=None):
if self.fields:
return self.fields
# Standard Django logic, use the form to determine the fields.
# The form needs to pass through all factory logic so all 'excludes' are set as well.
# Default Django does: form = self.get_formset(request, obj, fields=None).form
# Use 'fields=None' avoids recursion in the field autodetection.
form = self.get_formset_child(request, obj, fields=None).get_form()
return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
def get_formset_child(self, request, obj=None, **kwargs):
"""
Return the formset child that the parent inline can use to represent us.
:rtype: PolymorphicFormSetChild
"""
# Similar to the normal get_formset(), the caller may pass fields to override the defaults settings
# in the inline. In Django's GenericInlineModelAdmin.get_formset() this is also used in the same way,
# to make sure the 'exclude' also contains the GFK fields.
#
# Hence this code is almost identical to InlineModelAdmin.get_formset()
# and GenericInlineModelAdmin.get_formset()
#
# Transfer the local inline attributes to the formset child,
# this allows overriding settings.
if "fields" in kwargs:
fields = kwargs.pop("fields")
else:
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
if self.exclude is None:
exclude = []
else:
exclude = list(self.exclude)
exclude.extend(self.get_readonly_fields(request, obj))
# Add forcefully, as Django 1.10 doesn't include readonly fields.
exclude.append("polymorphic_ctype")
if (
self.exclude is None
and hasattr(self.form, "_meta")
and self.form._meta.exclude
):
# Take the custom ModelForm's Meta.exclude into account only if the
# InlineModelAdmin doesn't define its own.
exclude.extend(self.form._meta.exclude)
# can_delete = self.can_delete and self.has_delete_permission(request, obj)
defaults = {
"form": self.form,
"fields": fields,
"exclude": exclude or None,
"formfield_callback": partial(
self.formfield_for_dbfield, request=request
),
}
defaults.update(kwargs)
# This goes through the same logic that get_formset() calls
# by passing the inline class attributes to modelform_factory()
FormSetChildClass = self.formset_child
return FormSetChildClass(self.model, **defaults)
class StackedPolymorphicInline(PolymorphicInlineModelAdmin):
"""
Stacked inline for django-polymorphic models.
Since tabular doesn't make much sense with changed fields, just offer this one.
"""
#: The default template to use.
template = "admin/polymorphic/edit_inline/stacked.html"

View File

@@ -0,0 +1,406 @@
"""
The parent admin displays the list view of the base model.
"""
import sys
from django.contrib import admin
from django.contrib.admin.helpers import AdminErrorList, AdminForm
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.db import models
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from polymorphic.utils import get_base_polymorphic_model
from .forms import PolymorphicModelChoiceForm
try:
# Django 2.0+
from django.urls import URLResolver
except ImportError:
# Django < 2.0
from django.urls import RegexURLResolver as URLResolver
if sys.version_info[0] >= 3:
long = int
class RegistrationClosed(RuntimeError):
"The admin model can't be registered anymore at this point."
class ChildAdminNotRegistered(RuntimeError):
"The admin site for the model is not registered."
class PolymorphicParentModelAdmin(admin.ModelAdmin):
"""
A admin interface that can displays different change/delete pages, depending on the polymorphic model.
To use this class, one attribute need to be defined:
* :attr:`child_models` should be a list models.
Alternatively, the following methods can be implemented:
* :func:`get_child_models` should return a list of models.
* optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog.
This class needs to be inherited by the model admin base class that is registered in the site.
The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`.
"""
#: The base model that the class uses (auto-detected if not set explicitly)
base_model = None
#: The child models that should be displayed
child_models = None
#: Whether the list should be polymorphic too, leave to ``False`` to optimize
polymorphic_list = False
add_type_template = None
add_type_form = PolymorphicModelChoiceForm
#: The regular expression to filter the primary key in the URL.
#: This accepts only numbers as defensive measure against catch-all URLs.
#: If your primary key consists of string values, update this regular expression.
pk_regex = r"(\d+|__fk__)"
def __init__(self, model, admin_site, *args, **kwargs):
super(PolymorphicParentModelAdmin, self).__init__(
model, admin_site, *args, **kwargs
)
self._is_setup = False
if self.base_model is None:
self.base_model = get_base_polymorphic_model(model)
def _lazy_setup(self):
if self._is_setup:
return
self._child_models = self.get_child_models()
# Make absolutely sure that the child models don't use the old 0.9 format,
# as of polymorphic 1.4 this deprecated configuration is no longer supported.
# Instead, register the child models in the admin too.
if self._child_models and not issubclass(self._child_models[0], models.Model):
raise ImproperlyConfigured(
"Since django-polymorphic 1.4, the `child_models` attribute "
"and `get_child_models()` method should be a list of models only.\n"
"The model-admin class should be registered in the regular Django admin."
)
self._child_admin_site = self.admin_site
self._is_setup = True
def register_child(self, model, model_admin):
"""
Register a model with admin to display.
"""
# After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf,
# which also means that a "Save and continue editing" button won't work.
if self._is_setup:
raise RegistrationClosed(
"The admin model can't be registered anymore at this point."
)
if not issubclass(model, self.base_model):
raise TypeError(
"{0} should be a subclass of {1}".format(
model.__name__, self.base_model.__name__
)
)
if not issubclass(model_admin, admin.ModelAdmin):
raise TypeError(
"{0} should be a subclass of {1}".format(
model_admin.__name__, admin.ModelAdmin.__name__
)
)
self._child_admin_site.register(model, model_admin)
def get_child_models(self):
"""
Return the derived model classes which this admin should handle.
This should return a list of tuples, exactly like :attr:`child_models` is.
The model classes can be retrieved as ``base_model.__subclasses__()``,
a setting in a config file, or a query of a plugin registration system at your option
"""
if self.child_models is None:
raise NotImplementedError("Implement get_child_models() or child_models")
return self.child_models
def get_child_type_choices(self, request, action):
"""
Return a list of polymorphic types for which the user has the permission to perform the given action.
"""
self._lazy_setup()
choices = []
for model in self.get_child_models():
perm_function_name = "has_{0}_permission".format(action)
model_admin = self._get_real_admin_by_model(model)
perm_function = getattr(model_admin, perm_function_name)
if not perm_function(request):
continue
ct = ContentType.objects.get_for_model(model, for_concrete_model=False)
choices.append((ct.id, model._meta.verbose_name))
return choices
def _get_real_admin(self, object_id, super_if_self=True):
try:
obj = (
self.model.objects.non_polymorphic()
.values("polymorphic_ctype")
.get(pk=object_id)
)
except self.model.DoesNotExist:
raise Http404
return self._get_real_admin_by_ct(
obj["polymorphic_ctype"], super_if_self=super_if_self
)
def _get_real_admin_by_ct(self, ct_id, super_if_self=True):
try:
ct = ContentType.objects.get_for_id(ct_id)
except ContentType.DoesNotExist as e:
raise Http404(e) # Handle invalid GET parameters
model_class = ct.model_class()
if not model_class:
# Handle model deletion
raise Http404("No model found for '{0}.{1}'.".format(*ct.natural_key()))
return self._get_real_admin_by_model(model_class, super_if_self=super_if_self)
def _get_real_admin_by_model(self, model_class, super_if_self=True):
# In case of a ?ct_id=### parameter, the view is already checked for permissions.
# Hence, make sure this is a derived object, or risk exposing other admin interfaces.
if model_class not in self._child_models:
raise PermissionDenied(
"Invalid model '{0}', it must be registered as child model.".format(
model_class
)
)
try:
# HACK: the only way to get the instance of an model admin,
# is to read the registry of the AdminSite.
real_admin = self._child_admin_site._registry[model_class]
except KeyError:
raise ChildAdminNotRegistered(
"No child admin site was registered for a '{0}' model.".format(
model_class
)
)
if super_if_self and real_admin is self:
return super(PolymorphicParentModelAdmin, self)
else:
return real_admin
def get_queryset(self, request):
# optimize the list display.
qs = super(PolymorphicParentModelAdmin, self).get_queryset(request)
if not self.polymorphic_list:
qs = qs.non_polymorphic()
return qs
def add_view(self, request, form_url="", extra_context=None):
"""Redirect the add view to the real admin."""
ct_id = int(request.GET.get("ct_id", 0))
if not ct_id:
# Display choices
return self.add_type_view(request)
else:
real_admin = self._get_real_admin_by_ct(ct_id)
# rebuild form_url, otherwise libraries below will override it.
form_url = add_preserved_filters(
{
"preserved_filters": urlencode({"ct_id": ct_id}),
"opts": self.model._meta,
},
form_url,
)
return real_admin.add_view(request, form_url, extra_context)
def change_view(self, request, object_id, *args, **kwargs):
"""Redirect the change view to the real admin."""
real_admin = self._get_real_admin(object_id)
return real_admin.change_view(request, object_id, *args, **kwargs)
def changeform_view(self, request, object_id=None, *args, **kwargs):
# The `changeform_view` is available as of Django 1.7, combining the add_view and change_view.
# As it's directly called by django-reversion, this method is also overwritten to make sure it
# also redirects to the child admin.
if object_id:
real_admin = self._get_real_admin(object_id)
return real_admin.changeform_view(request, object_id, *args, **kwargs)
else:
# Add view. As it should already be handled via `add_view`, this means something custom is done here!
return super(PolymorphicParentModelAdmin, self).changeform_view(
request, object_id, *args, **kwargs
)
def history_view(self, request, object_id, extra_context=None):
"""Redirect the history view to the real admin."""
real_admin = self._get_real_admin(object_id)
return real_admin.history_view(request, object_id, extra_context=extra_context)
def delete_view(self, request, object_id, extra_context=None):
"""Redirect the delete view to the real admin."""
real_admin = self._get_real_admin(object_id)
return real_admin.delete_view(request, object_id, extra_context)
def get_preserved_filters(self, request):
if "_changelist_filters" in request.GET:
request.GET = request.GET.copy()
filters = request.GET.get("_changelist_filters")
f = filters.split("&")
for x in f:
c = x.split("=")
request.GET[c[0]] = c[1]
del request.GET["_changelist_filters"]
return super(PolymorphicParentModelAdmin, self).get_preserved_filters(request)
def get_urls(self):
"""
Expose the custom URLs for the subclasses and the URL resolver.
"""
urls = super(PolymorphicParentModelAdmin, self).get_urls()
# At this point. all admin code needs to be known.
self._lazy_setup()
return urls
def subclass_view(self, request, path):
"""
Forward any request to a custom view of the real admin.
"""
ct_id = int(request.GET.get("ct_id", 0))
if not ct_id:
# See if the path started with an ID.
try:
pos = path.find("/")
if pos == -1:
object_id = long(path)
else:
object_id = long(path[0:pos])
except ValueError:
raise Http404(
"No ct_id parameter, unable to find admin subclass for path '{0}'.".format(
path
)
)
ct_id = self.model.objects.values_list(
"polymorphic_ctype_id", flat=True
).get(pk=object_id)
real_admin = self._get_real_admin_by_ct(ct_id)
resolver = URLResolver("^", real_admin.urls)
resolvermatch = resolver.resolve(path) # May raise Resolver404
if not resolvermatch:
raise Http404("No match for path '{0}' in admin subclass.".format(path))
return resolvermatch.func(request, *resolvermatch.args, **resolvermatch.kwargs)
def add_type_view(self, request, form_url=""):
"""
Display a choice form to select which page type to add.
"""
if not self.has_add_permission(request):
raise PermissionDenied
extra_qs = ""
if request.META["QUERY_STRING"]:
# QUERY_STRING is bytes in Python 3, using force_text() to decode it as string.
# See QueryDict how Django deals with that.
extra_qs = "&{0}".format(force_text(request.META["QUERY_STRING"]))
choices = self.get_child_type_choices(request, "add")
if len(choices) == 1:
return HttpResponseRedirect("?ct_id={0}{1}".format(choices[0][0], extra_qs))
# Create form
form = self.add_type_form(
data=request.POST if request.method == "POST" else None,
initial={"ct_id": choices[0][0]},
)
form.fields["ct_id"].choices = choices
if form.is_valid():
return HttpResponseRedirect(
"?ct_id={0}{1}".format(form.cleaned_data["ct_id"], extra_qs)
)
# Wrap in all admin layout
fieldsets = ((None, {"fields": ("ct_id",)}),)
adminForm = AdminForm(form, fieldsets, {}, model_admin=self)
media = self.media + adminForm.media
opts = self.model._meta
context = {
"title": _("Add %s") % force_text(opts.verbose_name),
"adminform": adminForm,
"is_popup": ("_popup" in request.POST or "_popup" in request.GET),
"media": mark_safe(media),
"errors": AdminErrorList(form, ()),
"app_label": opts.app_label,
}
return self.render_add_type_form(request, context, form_url)
def render_add_type_form(self, request, context, form_url=""):
"""
Render the page type choice form.
"""
opts = self.model._meta
app_label = opts.app_label
context.update(
{
"has_change_permission": self.has_change_permission(request),
"form_url": mark_safe(form_url),
"opts": opts,
"add": True,
"save_on_top": self.save_on_top,
}
)
templates = self.add_type_template or [
"admin/%s/%s/add_type_form.html" % (app_label, opts.object_name.lower()),
"admin/%s/add_type_form.html" % app_label,
"admin/polymorphic/add_type_form.html", # added default here
"admin/add_type_form.html",
]
request.current_app = self.admin_site.name
return TemplateResponse(request, templates, context)
@property
def change_list_template(self):
opts = self.model._meta
app_label = opts.app_label
# Pass the base options
base_opts = self.base_model._meta
base_app_label = base_opts.app_label
return [
"admin/%s/%s/change_list.html" % (app_label, opts.object_name.lower()),
"admin/%s/change_list.html" % app_label,
# Added base class:
"admin/%s/%s/change_list.html"
% (base_app_label, base_opts.object_name.lower()),
"admin/%s/change_list.html" % base_app_label,
"admin/change_list.html",
]

View File

@@ -0,0 +1,219 @@
# -*- coding: utf-8 -*-
"""
PolymorphicModel Meta Class
"""
from __future__ import absolute_import
import inspect
import os
import sys
import warnings
import django
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.manager import ManagerDescriptor
from .managers import PolymorphicManager
from .query import PolymorphicQuerySet
# PolymorphicQuerySet Q objects (and filter()) support these additional key words.
# These are forbidden as field names (a descriptive exception is raised)
POLYMORPHIC_SPECIAL_Q_KWORDS = ["instance_of", "not_instance_of"]
DUMPDATA_COMMAND = os.path.join(
"django", "core", "management", "commands", "dumpdata.py"
)
class ManagerInheritanceWarning(RuntimeWarning):
pass
###################################################################################
# PolymorphicModel meta class
class PolymorphicModelBase(ModelBase):
"""
Manager inheritance is a pretty complex topic which may need
more thought regarding how this should be handled for polymorphic
models.
In any case, we probably should propagate 'objects' and 'base_objects'
from PolymorphicModel to every subclass. We also want to somehow
inherit/propagate _default_manager as well, as it needs to be polymorphic.
The current implementation below is an experiment to solve this
problem with a very simplistic approach: We unconditionally
inherit/propagate any and all managers (using _copy_to_model),
as long as they are defined on polymorphic models
(the others are left alone).
Like Django ModelBase, we special-case _default_manager:
if there are any user-defined managers, it is set to the first of these.
We also require that _default_manager as well as any user defined
polymorphic managers produce querysets that are derived from
PolymorphicQuerySet.
"""
def __new__(self, model_name, bases, attrs):
# print; print '###', model_name, '- bases:', bases
# Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
if not attrs and model_name == "NewBase":
return super(PolymorphicModelBase, self).__new__(
self, model_name, bases, attrs
)
# Make sure that manager_inheritance_from_future is set, since django-polymorphic 1.x already
# simulated that behavior on the polymorphic manager to all subclasses behave like polymorphics
if django.VERSION < (2, 0):
if "Meta" in attrs:
if not hasattr(attrs["Meta"], "manager_inheritance_from_future"):
attrs["Meta"].manager_inheritance_from_future = True
else:
attrs["Meta"] = type(
"Meta", (object,), {"manager_inheritance_from_future": True}
)
# create new model
new_class = self.call_superclass_new_method(model_name, bases, attrs)
# check if the model fields are all allowed
self.validate_model_fields(new_class)
# validate resulting default manager
if not new_class._meta.abstract and not new_class._meta.swapped:
self.validate_model_manager(new_class.objects, model_name, "objects")
# for __init__ function of this class (monkeypatching inheritance accessors)
new_class.polymorphic_super_sub_accessors_replaced = False
# determine the name of the primary key field and store it into the class variable
# polymorphic_primary_key_name (it is needed by query.py)
for f in new_class._meta.fields:
if f.primary_key and type(f) != models.OneToOneField:
new_class.polymorphic_primary_key_name = f.name
break
return new_class
@classmethod
def call_superclass_new_method(self, model_name, bases, attrs):
"""call __new__ method of super class and return the newly created class.
Also work around a limitation in Django's ModelBase."""
# There seems to be a general limitation in Django's app_label handling
# regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django
# We run into this problem if polymorphic.py is located in a top-level directory
# which is directly in the python path. To work around this we temporarily set
# app_label here for PolymorphicModel.
meta = attrs.get("Meta", None)
do_app_label_workaround = (
meta
and attrs["__module__"] == "polymorphic"
and model_name == "PolymorphicModel"
and getattr(meta, "app_label", None) is None
)
if do_app_label_workaround:
meta.app_label = "poly_dummy_app_label"
new_class = super(PolymorphicModelBase, self).__new__(
self, model_name, bases, attrs
)
if do_app_label_workaround:
del meta.app_label
return new_class
@classmethod
def validate_model_fields(self, new_class):
"check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)"
for f in new_class._meta.fields:
if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS:
e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models'
raise AssertionError(e % (new_class.__name__, f.name))
@classmethod
def validate_model_manager(self, manager, model_name, manager_name):
"""check if the manager is derived from PolymorphicManager
and its querysets from PolymorphicQuerySet - throw AssertionError if not"""
if not issubclass(type(manager), PolymorphicManager):
if django.VERSION < (2, 0):
extra = "\nConsider using Meta.manager_inheritance_from_future = True for Django 1.x projects"
else:
extra = ""
e = (
'PolymorphicModel: "{0}.{1}" manager is of type "{2}", but must be a subclass of'
" PolymorphicManager.{extra} to support retrieving subclasses".format(
model_name, manager_name, type(manager).__name__, extra=extra
)
)
warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
return manager
if not getattr(manager, "queryset_class", None) or not issubclass(
manager.queryset_class, PolymorphicQuerySet
):
e = (
'PolymorphicModel: "{0}.{1}" has been instantiated with a queryset class '
"which is not a subclass of PolymorphicQuerySet (which is required)".format(
model_name, manager_name
)
)
warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
return manager
@property
def base_objects(self):
warnings.warn(
"Using PolymorphicModel.base_objects is deprecated.\n"
"Use {0}.objects.non_polymorphic() instead.".format(
self.__class__.__name__
),
DeprecationWarning,
stacklevel=2,
)
return self._base_objects
@property
def _base_objects(self):
# Create a manager so the API works as expected. Just don't register it
# anymore in the Model Meta, so it doesn't substitute our polymorphic
# manager as default manager for the third level of inheritance when
# that third level doesn't define a manager at all.
manager = models.Manager()
manager.name = "base_objects"
manager.model = self
return manager
@property
def _default_manager(self):
if len(sys.argv) > 1 and sys.argv[1] == "dumpdata":
# TODO: investigate Django how this can be avoided
# hack: a small patch to Django would be a better solution.
# Django's management command 'dumpdata' relies on non-polymorphic
# behaviour of the _default_manager. Therefore, we catch any access to _default_manager
# here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py'
# Otherwise, the base objects will be upcasted to polymorphic models, and be outputted as such.
# (non-polymorphic default manager is 'base_objects' for polymorphic models).
# This way we don't need to patch django.core.management.commands.dumpdata
# for all supported Django versions.
frm = inspect.stack()[
1
] # frm[1] is caller file name, frm[3] is caller function name
if DUMPDATA_COMMAND in frm[1]:
return self._base_objects
manager = super(PolymorphicModelBase, self)._default_manager
if not isinstance(manager, PolymorphicManager):
warnings.warn(
"{0}._default_manager is not a PolymorphicManager".format(
self.__class__.__name__
),
ManagerInheritanceWarning,
)
return manager

View File

@@ -0,0 +1,45 @@
"""Compatibility with Python 2 (taken from 'django.utils.six')"""
import sys
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
if PY3:
string_types = (str,)
integer_types = (int,)
class_types = (type,)
text_type = str
binary_type = bytes
MAXSIZE = sys.maxsize
else:
string_types = (basestring,)
integer_types = (int, long)
def with_metaclass(meta, *bases):
class metaclass(type):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, "temporary_class", (), {})
def python_2_unicode_compatible(klass):
"""
A decorator that defines __unicode__ and __str__ methods under Python 2.
Under Python 3 it does nothing.
To support Python 2 and 3 with a single code base, define a __str__ method
returning text and apply this decorator to the class.
"""
if PY2:
if "__str__" not in klass.__dict__:
raise ValueError(
"@python_2_unicode_compatible cannot be applied "
"to %s because it doesn't define __str__()." % klass.__name__
)
klass.__unicode__ = klass.__str__
klass.__str__ = lambda self: self.__unicode__().encode("utf-8")
return klass

View File

@@ -0,0 +1,142 @@
"""
The ``extra_views.formsets`` provides a simple way to handle formsets.
The ``extra_views.advanced`` provides a method to combine that with a create/update form.
This package provides classes that support both options for polymorphic formsets.
"""
from __future__ import absolute_import
import extra_views
from django.core.exceptions import ImproperlyConfigured
from polymorphic.formsets import (
BasePolymorphicInlineFormSet,
BasePolymorphicModelFormSet,
polymorphic_child_forms_factory,
)
__all__ = (
"PolymorphicFormSetView",
"PolymorphicInlineFormSetView",
"PolymorphicInlineFormSet",
)
class PolymorphicFormSetMixin(object):
"""
Internal Mixin, that provides polymorphic integration with the ``extra_views`` package.
"""
formset_class = BasePolymorphicModelFormSet
#: Default 0 extra forms
factory_kwargs = {"extra": 0}
#: Define the children
# :type: list[PolymorphicFormSetChild]
formset_children = None
def get_formset_children(self):
"""
:rtype: list[PolymorphicFormSetChild]
"""
if not self.formset_children:
raise ImproperlyConfigured(
"Define 'formset_children' as list of `PolymorphicFormSetChild`"
)
return self.formset_children
def get_formset_child_kwargs(self):
return {}
def get_formset(self):
"""
Returns the formset class from the inline formset factory
"""
# Implementation detail:
# Since `polymorphic_modelformset_factory` and `polymorphic_inlineformset_factory` mainly
# reuse the standard factories, and then add `child_forms`, the same can be done here.
# This makes sure the base class construction is completely honored.
FormSet = super(PolymorphicFormSetMixin, self).get_formset()
FormSet.child_forms = polymorphic_child_forms_factory(
self.get_formset_children(), **self.get_formset_child_kwargs()
)
return FormSet
class PolymorphicFormSetView(PolymorphicFormSetMixin, extra_views.ModelFormSetView):
"""
A view that displays a single polymorphic formset.
.. code-block:: python
from polymorphic.formsets import PolymorphicFormSetChild
class ItemsView(PolymorphicFormSetView):
model = Item
formset_children = [
PolymorphicFormSetChild(ItemSubclass1),
PolymorphicFormSetChild(ItemSubclass2),
]
"""
formset_class = BasePolymorphicModelFormSet
class PolymorphicInlineFormSetView(
PolymorphicFormSetMixin, extra_views.InlineFormSetView
):
"""
A view that displays a single polymorphic formset - with one parent object.
This is a variation of the :mod:`extra_views` package classes for django-polymorphic.
.. code-block:: python
from polymorphic.formsets import PolymorphicFormSetChild
class OrderItemsView(PolymorphicInlineFormSetView):
model = Order
inline_model = Item
formset_children = [
PolymorphicFormSetChild(ItemSubclass1),
PolymorphicFormSetChild(ItemSubclass2),
]
"""
formset_class = BasePolymorphicInlineFormSet
class PolymorphicInlineFormSet(
PolymorphicFormSetMixin, extra_views.InlineFormSetFactory
):
"""
An inline to add to the ``inlines`` of
the :class:`~extra_views.advanced.CreateWithInlinesView`
and :class:`~extra_views.advanced.UpdateWithInlinesView` class.
.. code-block:: python
from polymorphic.formsets import PolymorphicFormSetChild
class ItemsInline(PolymorphicInlineFormSet):
model = Item
formset_children = [
PolymorphicFormSetChild(ItemSubclass1),
PolymorphicFormSetChild(ItemSubclass2),
]
class OrderCreateView(CreateWithInlinesView):
model = Order
inlines = [ItemsInline]
def get_success_url(self):
return self.object.get_absolute_url()
"""
formset_class = BasePolymorphicInlineFormSet

View File

@@ -0,0 +1,35 @@
from django.contrib.contenttypes.models import ContentType
def get_polymorphic_base_content_type(obj):
"""
Helper function to return the base polymorphic content type id. This should used with django-guardian and the
GUARDIAN_GET_CONTENT_TYPE option.
See the django-guardian documentation for more information:
https://django-guardian.readthedocs.io/en/latest/configuration.html#guardian-get-content-type
"""
if hasattr(obj, "polymorphic_model_marker"):
try:
superclasses = list(obj.__class__.mro())
except TypeError:
# obj is an object so mro() need to be called with the obj.
superclasses = list(obj.__class__.mro(obj))
polymorphic_superclasses = list()
for sclass in superclasses:
if hasattr(sclass, "polymorphic_model_marker"):
polymorphic_superclasses.append(sclass)
# PolymorphicMPTT adds an additional class between polymorphic and base class.
if hasattr(obj, "can_have_children"):
root_polymorphic_class = polymorphic_superclasses[-3]
else:
root_polymorphic_class = polymorphic_superclasses[-2]
ctype = ContentType.objects.get_for_model(root_polymorphic_class)
else:
ctype = ContentType.objects.get_for_model(obj)
return ctype

View File

@@ -0,0 +1,37 @@
"""
This allows creating formsets where each row can be a different form type.
The logic of the formsets work similar to the standard Django formsets;
there are factory methods to construct the classes with the proper form settings.
The "parent" formset hosts the entire model and their child model.
For every child type, there is an :class:`PolymorphicFormSetChild` instance
that describes how to display and construct the child.
It's parameters are very similar to the parent's factory method.
"""
from .generic import ( # Can import generic here, as polymorphic already depends on the 'contenttypes' app.
BaseGenericPolymorphicInlineFormSet,
GenericPolymorphicFormSetChild,
generic_polymorphic_inlineformset_factory,
)
from .models import (
BasePolymorphicInlineFormSet,
BasePolymorphicModelFormSet,
PolymorphicFormSetChild,
UnsupportedChildType,
polymorphic_child_forms_factory,
polymorphic_inlineformset_factory,
polymorphic_modelformset_factory,
)
__all__ = (
"BasePolymorphicModelFormSet",
"BasePolymorphicInlineFormSet",
"PolymorphicFormSetChild",
"UnsupportedChildType",
"polymorphic_modelformset_factory",
"polymorphic_inlineformset_factory",
"polymorphic_child_forms_factory",
"BaseGenericPolymorphicInlineFormSet",
"GenericPolymorphicFormSetChild",
"generic_polymorphic_inlineformset_factory",
)

View File

@@ -0,0 +1,136 @@
from django.contrib.contenttypes.forms import (
BaseGenericInlineFormSet,
generic_inlineformset_factory,
)
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.forms.models import ModelForm
from .models import (
BasePolymorphicModelFormSet,
PolymorphicFormSetChild,
polymorphic_child_forms_factory,
)
class GenericPolymorphicFormSetChild(PolymorphicFormSetChild):
"""
Formset child for generic inlines
"""
def __init__(self, *args, **kwargs):
self.ct_field = kwargs.pop("ct_field", "content_type")
self.fk_field = kwargs.pop("fk_field", "object_id")
super(GenericPolymorphicFormSetChild, self).__init__(*args, **kwargs)
def get_form(self, ct_field="content_type", fk_field="object_id", **kwargs):
"""
Construct the form class for the formset child.
"""
exclude = list(self.exclude)
extra_exclude = kwargs.pop("extra_exclude", None)
if extra_exclude:
exclude += list(extra_exclude)
# Make sure the GFK fields are excluded by default
# This is similar to what generic_inlineformset_factory() does
# if there is no field called `ct_field` let the exception propagate
opts = self.model._meta
ct_field = opts.get_field(self.ct_field)
if (
not isinstance(ct_field, models.ForeignKey)
or ct_field.remote_field.model != ContentType
):
raise Exception(
"fk_name '%s' is not a ForeignKey to ContentType" % ct_field
)
fk_field = opts.get_field(self.fk_field) # let the exception propagate
exclude.extend([ct_field.name, fk_field.name])
kwargs["exclude"] = exclude
return super(GenericPolymorphicFormSetChild, self).get_form(**kwargs)
class BaseGenericPolymorphicInlineFormSet(
BaseGenericInlineFormSet, BasePolymorphicModelFormSet
):
"""
Polymorphic formset variation for inline generic formsets
"""
def generic_polymorphic_inlineformset_factory(
model,
formset_children,
form=ModelForm,
formset=BaseGenericPolymorphicInlineFormSet,
ct_field="content_type",
fk_field="object_id",
# Base form
# TODO: should these fields be removed in favor of creating
# the base form as a formset child too?
fields=None,
exclude=None,
extra=1,
can_order=False,
can_delete=True,
max_num=None,
formfield_callback=None,
validate_max=False,
for_concrete_model=True,
min_num=None,
validate_min=False,
child_form_kwargs=None,
):
"""
Construct the class for a generic inline polymorphic formset.
All arguments are identical to :func:`~django.contrib.contenttypes.forms.generic_inlineformset_factory`,
with the exception of the ``formset_children`` argument.
:param formset_children: A list of all child :class:`PolymorphicFormSetChild` objects
that tell the inline how to render the child model types.
:type formset_children: Iterable[PolymorphicFormSetChild]
:rtype: type
"""
kwargs = {
"model": model,
"form": form,
"formfield_callback": formfield_callback,
"formset": formset,
"ct_field": ct_field,
"fk_field": fk_field,
"extra": extra,
"can_delete": can_delete,
"can_order": can_order,
"fields": fields,
"exclude": exclude,
"min_num": min_num,
"max_num": max_num,
"validate_min": validate_min,
"validate_max": validate_max,
"for_concrete_model": for_concrete_model,
# 'localized_fields': localized_fields,
# 'labels': labels,
# 'help_texts': help_texts,
# 'error_messages': error_messages,
# 'field_classes': field_classes,
}
if child_form_kwargs is None:
child_form_kwargs = {}
child_kwargs = {
# 'exclude': exclude,
"ct_field": ct_field,
"fk_field": fk_field,
}
if child_form_kwargs:
child_kwargs.update(child_form_kwargs)
FormSet = generic_inlineformset_factory(**kwargs)
FormSet.child_forms = polymorphic_child_forms_factory(
formset_children, **child_kwargs
)
return FormSet

View File

@@ -0,0 +1,456 @@
from collections import OrderedDict
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.forms.models import (
BaseInlineFormSet,
BaseModelFormSet,
ModelForm,
inlineformset_factory,
modelform_factory,
modelformset_factory,
)
from django.utils.functional import cached_property
from polymorphic.models import PolymorphicModel
from .utils import add_media
class UnsupportedChildType(LookupError):
pass
class PolymorphicFormSetChild(object):
"""
Metadata to define the inline of a polymorphic child.
Provide this information in the :func:'polymorphic_inlineformset_factory' construction.
"""
def __init__(
self,
model,
form=ModelForm,
fields=None,
exclude=None,
formfield_callback=None,
widgets=None,
localized_fields=None,
labels=None,
help_texts=None,
error_messages=None,
):
self.model = model
# Instead of initializing the form here right away,
# the settings are saved so get_form() can receive additional exclude kwargs.
# This is mostly needed for the generic inline formsets
self._form_base = form
self.fields = fields
self.exclude = exclude or ()
self.formfield_callback = formfield_callback
self.widgets = widgets
self.localized_fields = localized_fields
self.labels = labels
self.help_texts = help_texts
self.error_messages = error_messages
@cached_property
def content_type(self):
"""
Expose the ContentType that the child relates to.
This can be used for the ''polymorphic_ctype'' field.
"""
return ContentType.objects.get_for_model(self.model, for_concrete_model=False)
def get_form(self, **kwargs):
"""
Construct the form class for the formset child.
"""
# Do what modelformset_factory() / inlineformset_factory() does to the 'form' argument;
# Construct the form with the given ModelFormOptions values
# Fields can be overwritten. To support the global 'polymorphic_child_forms_factory' kwargs,
# that doesn't completely replace all 'exclude' settings defined per child type,
# we allow to define things like 'extra_...' fields that are amended to the current child settings.
exclude = list(self.exclude)
extra_exclude = kwargs.pop("extra_exclude", None)
if extra_exclude:
exclude += list(extra_exclude)
defaults = {
"form": self._form_base,
"formfield_callback": self.formfield_callback,
"fields": self.fields,
"exclude": exclude,
# 'for_concrete_model': for_concrete_model,
"localized_fields": self.localized_fields,
"labels": self.labels,
"help_texts": self.help_texts,
"error_messages": self.error_messages,
# 'field_classes': field_classes,
}
defaults.update(kwargs)
return modelform_factory(self.model, **defaults)
def polymorphic_child_forms_factory(formset_children, **kwargs):
"""
Construct the forms for the formset children.
This is mostly used internally, and rarely needs to be used by external projects.
When using the factory methods (:func:'polymorphic_inlineformset_factory'),
this feature is called already for you.
"""
child_forms = OrderedDict()
for formset_child in formset_children:
child_forms[formset_child.model] = formset_child.get_form(**kwargs)
return child_forms
class BasePolymorphicModelFormSet(BaseModelFormSet):
"""
A formset that can produce different forms depending on the object type.
Note that the 'add' feature is therefore more complex,
as all variations need ot be exposed somewhere.
When switching existing formsets to the polymorphic formset,
note that the ID field will no longer be named ''model_ptr'',
but just appear as ''id''.
"""
# Assigned by the factory
child_forms = OrderedDict()
def __init__(self, *args, **kwargs):
super(BasePolymorphicModelFormSet, self).__init__(*args, **kwargs)
self.queryset_data = self.get_queryset()
def _construct_form(self, i, **kwargs):
"""
Create the form, depending on the model that's behind it.
"""
# BaseModelFormSet logic
if self.is_bound and i < self.initial_form_count():
pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name)
pk = self.data[pk_key]
pk_field = self.model._meta.pk
to_python = self._get_to_python(pk_field)
pk = to_python(pk)
kwargs["instance"] = self._existing_object(pk)
if i < self.initial_form_count() and "instance" not in kwargs:
kwargs["instance"] = self.get_queryset()[i]
if i >= self.initial_form_count() and self.initial_extra:
# Set initial values for extra forms
try:
kwargs["initial"] = self.initial_extra[i - self.initial_form_count()]
except IndexError:
pass
# BaseFormSet logic, with custom formset_class
defaults = {
"auto_id": self.auto_id,
"prefix": self.add_prefix(i),
"error_class": self.error_class,
}
if self.is_bound:
defaults["data"] = self.data
defaults["files"] = self.files
if self.initial and "initial" not in kwargs:
try:
defaults["initial"] = self.initial[i]
except IndexError:
pass
# Allow extra forms to be empty, unless they're part of
# the minimum forms.
if i >= self.initial_form_count() and i >= self.min_num:
defaults["empty_permitted"] = True
defaults["use_required_attribute"] = False
defaults.update(kwargs)
# Need to find the model that will be displayed in this form.
# Hence, peeking in the self.queryset_data beforehand.
if self.is_bound:
if "instance" in defaults:
# Object is already bound to a model, won't change the content type
model = defaults[
"instance"
].get_real_instance_class() # allow proxy models
else:
# Extra or empty form, use the provided type.
# Note this completely tru
prefix = defaults["prefix"]
try:
ct_id = int(self.data["{0}-polymorphic_ctype".format(prefix)])
except (KeyError, ValueError):
raise ValidationError(
"Formset row {0} has no 'polymorphic_ctype' defined!".format(
prefix
)
)
model = ContentType.objects.get_for_id(ct_id).model_class()
if model not in self.child_forms:
# Perform basic validation, as we skip the ChoiceField here.
raise UnsupportedChildType(
"Child model type {0} is not part of the formset".format(model)
)
else:
if "instance" in defaults:
model = defaults[
"instance"
].get_real_instance_class() # allow proxy models
elif "polymorphic_ctype" in defaults.get("initial", {}):
model = defaults["initial"]["polymorphic_ctype"].model_class()
elif i < len(self.queryset_data):
model = self.queryset_data[i].__class__
else:
# Extra forms, cycle between all types
# TODO: take the 'extra' value of each child formset into account.
total_known = len(self.queryset_data)
child_models = list(self.child_forms.keys())
model = child_models[(i - total_known) % len(child_models)]
form_class = self.get_form_class(model)
form = form_class(**defaults)
self.add_fields(form, i)
return form
def add_fields(self, form, index):
"""Add a hidden field for the content type."""
ct = ContentType.objects.get_for_model(
form._meta.model, for_concrete_model=False
)
choices = [(ct.pk, ct)] # Single choice, existing forms can't change the value.
form.fields["polymorphic_ctype"] = forms.ChoiceField(
choices=choices, initial=ct.pk, required=False, widget=forms.HiddenInput
)
super(BasePolymorphicModelFormSet, self).add_fields(form, index)
def get_form_class(self, model):
"""
Return the proper form class for the given model.
"""
if not self.child_forms:
raise ImproperlyConfigured(
"No 'child_forms' defined in {0}".format(self.__class__.__name__)
)
if not issubclass(model, PolymorphicModel):
raise TypeError("Expect polymorphic model type, not {0}".format(model))
try:
return self.child_forms[model]
except KeyError:
# This may happen when the query returns objects of a type that was not handled by the formset.
raise UnsupportedChildType(
"The '{0}' found a '{1}' model in the queryset, "
"but no form class is registered to display it.".format(
self.__class__.__name__, model.__name__
)
)
def is_multipart(self):
"""
Returns True if the formset needs to be multipart, i.e. it
has FileInput. Otherwise, False.
"""
return any(f.is_multipart() for f in self.empty_forms)
@property
def media(self):
# Include the media of all form types.
# The form media includes all form widget media
media = forms.Media()
for form in self.empty_forms:
add_media(media, form.media)
return media
@cached_property
def empty_forms(self):
"""
Return all possible empty forms
"""
forms = []
for model, form_class in self.child_forms.items():
kwargs = self.get_form_kwargs(None)
form = form_class(
auto_id=self.auto_id,
prefix=self.add_prefix("__prefix__"),
empty_permitted=True,
use_required_attribute=False,
**kwargs
)
self.add_fields(form, None)
forms.append(form)
return forms
@property
def empty_form(self):
# TODO: make an exception when can_add_base is defined?
raise RuntimeError(
"'empty_form' is not used in polymorphic formsets, use 'empty_forms' instead."
)
def polymorphic_modelformset_factory(
model,
formset_children,
formset=BasePolymorphicModelFormSet,
# Base field
# TODO: should these fields be removed in favor of creating
# the base form as a formset child too?
form=ModelForm,
fields=None,
exclude=None,
extra=1,
can_order=False,
can_delete=True,
max_num=None,
formfield_callback=None,
widgets=None,
validate_max=False,
localized_fields=None,
labels=None,
help_texts=None,
error_messages=None,
min_num=None,
validate_min=False,
field_classes=None,
child_form_kwargs=None,
):
"""
Construct the class for an polymorphic model formset.
All arguments are identical to :func:'~django.forms.models.modelformset_factory',
with the exception of the ''formset_children'' argument.
:param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects
that tell the inline how to render the child model types.
:type formset_children: Iterable[PolymorphicFormSetChild]
:rtype: type
"""
kwargs = {
"model": model,
"form": form,
"formfield_callback": formfield_callback,
"formset": formset,
"extra": extra,
"can_delete": can_delete,
"can_order": can_order,
"fields": fields,
"exclude": exclude,
"min_num": min_num,
"max_num": max_num,
"widgets": widgets,
"validate_min": validate_min,
"validate_max": validate_max,
"localized_fields": localized_fields,
"labels": labels,
"help_texts": help_texts,
"error_messages": error_messages,
"field_classes": field_classes,
}
FormSet = modelformset_factory(**kwargs)
child_kwargs = {
# 'exclude': exclude,
}
if child_form_kwargs:
child_kwargs.update(child_form_kwargs)
FormSet.child_forms = polymorphic_child_forms_factory(
formset_children, **child_kwargs
)
return FormSet
class BasePolymorphicInlineFormSet(BaseInlineFormSet, BasePolymorphicModelFormSet):
"""
Polymorphic formset variation for inline formsets
"""
def _construct_form(self, i, **kwargs):
return super(BasePolymorphicInlineFormSet, self)._construct_form(i, **kwargs)
def polymorphic_inlineformset_factory(
parent_model,
model,
formset_children,
formset=BasePolymorphicInlineFormSet,
fk_name=None,
# Base field
# TODO: should these fields be removed in favor of creating
# the base form as a formset child too?
form=ModelForm,
fields=None,
exclude=None,
extra=1,
can_order=False,
can_delete=True,
max_num=None,
formfield_callback=None,
widgets=None,
validate_max=False,
localized_fields=None,
labels=None,
help_texts=None,
error_messages=None,
min_num=None,
validate_min=False,
field_classes=None,
child_form_kwargs=None,
):
"""
Construct the class for an inline polymorphic formset.
All arguments are identical to :func:'~django.forms.models.inlineformset_factory',
with the exception of the ''formset_children'' argument.
:param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects
that tell the inline how to render the child model types.
:type formset_children: Iterable[PolymorphicFormSetChild]
:rtype: type
"""
kwargs = {
"parent_model": parent_model,
"model": model,
"form": form,
"formfield_callback": formfield_callback,
"formset": formset,
"fk_name": fk_name,
"extra": extra,
"can_delete": can_delete,
"can_order": can_order,
"fields": fields,
"exclude": exclude,
"min_num": min_num,
"max_num": max_num,
"widgets": widgets,
"validate_min": validate_min,
"validate_max": validate_max,
"localized_fields": localized_fields,
"labels": labels,
"help_texts": help_texts,
"error_messages": error_messages,
"field_classes": field_classes,
}
FormSet = inlineformset_factory(**kwargs)
child_kwargs = {
# 'exclude': exclude,
}
if child_form_kwargs:
child_kwargs.update(child_form_kwargs)
FormSet.child_forms = polymorphic_child_forms_factory(
formset_children, **child_kwargs
)
return FormSet

View File

@@ -0,0 +1,20 @@
"""
Internal utils
"""
import django
def add_media(dest, media):
"""
Optimized version of django.forms.Media.__add__() that doesn't create new objects.
"""
if django.VERSION >= (2, 2):
dest._css_lists.extend(media._css_lists)
dest._js_lists.extend(media._js_lists)
elif django.VERSION >= (2, 0):
combined = dest + media
dest._css = combined._css
dest._js = combined._js
else:
dest.add_css(media._css)
dest.add_js(media._js)

View File

@@ -0,0 +1,31 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-11-29 18:12+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: admin.py:41
msgid "Type"
msgstr ""
#: admin.py:56
msgid "Content type"
msgstr ""
#: admin.py:403
msgid "Contents"
msgstr ""

View File

@@ -0,0 +1,31 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Gonzalo Bustos, 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-11-29 18:12+0100\n"
"PO-Revision-Date: 2015-10-12 11:42-0300\n"
"Last-Translator: Gonzalo Bustos\n"
"Language-Team: Spanish <LL@li.org>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.6.10\n"
#: admin.py:41
msgid "Type"
msgstr "Tipo"
#: admin.py:56
msgid "Content type"
msgstr "Tipo de contenido"
#: admin.py:333 admin.py:403
#, python-format
msgid "Contents"
msgstr "Contenidos"

View File

@@ -0,0 +1,37 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-11-29 18:12+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:41
msgid "Type"
msgstr "Type"
#: admin.py:56
msgid "Content type"
msgstr "Type de contenu"
# This is already translated in Django
# #: admin.py:333
# #, python-format
# msgid "Add %s"
# msgstr ""
#: admin.py:403
msgid "Contents"
msgstr "Contenus"

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""
The manager class for use in the models.
"""
from __future__ import unicode_literals
from django.db import models
from polymorphic.compat import python_2_unicode_compatible
from polymorphic.query import PolymorphicQuerySet
__all__ = ("PolymorphicManager", "PolymorphicQuerySet")
@python_2_unicode_compatible
class PolymorphicManager(models.Manager):
"""
Manager for PolymorphicModel
Usually not explicitly needed, except if a custom manager or
a custom queryset class is to be used.
"""
queryset_class = PolymorphicQuerySet
@classmethod
def from_queryset(cls, queryset_class, class_name=None):
manager = super(PolymorphicManager, cls).from_queryset(
queryset_class, class_name=class_name
)
# also set our version, Django uses _queryset_class
manager.queryset_class = queryset_class
return manager
def get_queryset(self):
qs = self.queryset_class(self.model, using=self._db, hints=self._hints)
if self.model._meta.proxy:
qs = qs.instance_of(self.model)
return qs
def __str__(self):
return "%s (PolymorphicManager) using %s" % (
self.__class__.__name__,
self.queryset_class.__name__,
)
# Proxied methods
def non_polymorphic(self):
return self.all().non_polymorphic()
def instance_of(self, *args):
return self.all().instance_of(*args)
def not_instance_of(self, *args):
return self.all().not_instance_of(*args)
def get_real_instances(self, base_result_objects=None):
return self.all().get_real_instances(base_result_objects=base_result_objects)

View File

@@ -0,0 +1,280 @@
# -*- coding: utf-8 -*-
"""
Seamless Polymorphic Inheritance for Django Models
"""
from __future__ import absolute_import
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.fields.related import (
ForwardManyToOneDescriptor,
ReverseOneToOneDescriptor,
)
from django.db.utils import DEFAULT_DB_ALIAS
from polymorphic.compat import with_metaclass
from .base import PolymorphicModelBase
from .managers import PolymorphicManager
from .query_translate import translate_polymorphic_Q_object
###################################################################################
# PolymorphicModel
class PolymorphicTypeUndefined(LookupError):
pass
class PolymorphicTypeInvalid(RuntimeError):
pass
class PolymorphicModel(with_metaclass(PolymorphicModelBase, models.Model)):
"""
Abstract base class that provides polymorphic behaviour
for any model directly or indirectly derived from it.
PolymorphicModel declares one field for internal use (:attr:`polymorphic_ctype`)
and provides a polymorphic manager as the default manager (and as 'objects').
"""
# for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing)
polymorphic_model_marker = True
# for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery
polymorphic_query_multiline_output = False
# avoid ContentType related field accessor clash (an error emitted by model validation)
#: The model field that stores the :class:`~django.contrib.contenttypes.models.ContentType` reference to the actual class.
polymorphic_ctype = models.ForeignKey(
ContentType,
null=True,
editable=False,
on_delete=models.CASCADE,
related_name="polymorphic_%(app_label)s.%(class)s_set+",
)
# some applications want to know the name of the fields that are added to its models
polymorphic_internal_model_fields = ["polymorphic_ctype"]
# Note that Django 1.5 removes these managers because the model is abstract.
# They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers()
objects = PolymorphicManager()
class Meta:
abstract = True
base_manager_name = "objects"
@classmethod
def translate_polymorphic_Q_object(cls, q):
return translate_polymorphic_Q_object(cls, q)
def pre_save_polymorphic(self, using=DEFAULT_DB_ALIAS):
"""
Make sure the ``polymorphic_ctype`` value is correctly set on this model.
"""
# This function may be called manually in special use-cases. When the object
# is saved for the first time, we store its real class in polymorphic_ctype.
# When the object later is retrieved by PolymorphicQuerySet, it uses this
# field to figure out the real class of this object
# (used by PolymorphicQuerySet._get_real_instances)
if not self.polymorphic_ctype_id:
self.polymorphic_ctype = ContentType.objects.db_manager(
using
).get_for_model(self, for_concrete_model=False)
pre_save_polymorphic.alters_data = True
def save(self, *args, **kwargs):
"""Calls :meth:`pre_save_polymorphic` and saves the model."""
using = kwargs.get("using", self._state.db or DEFAULT_DB_ALIAS)
self.pre_save_polymorphic(using=using)
return super(PolymorphicModel, self).save(*args, **kwargs)
save.alters_data = True
def get_real_instance_class(self):
"""
Return the actual model type of the object.
If a non-polymorphic manager (like base_objects) has been used to
retrieve objects, then the real class/type of these objects may be
determined using this method.
"""
if self.polymorphic_ctype_id is None:
raise PolymorphicTypeUndefined(
(
"The model {}#{} does not have a `polymorphic_ctype_id` value defined.\n"
"If you created models outside polymorphic, e.g. through an import or migration, "
"make sure the `polymorphic_ctype_id` field points to the ContentType ID of the model subclass."
).format(self.__class__.__name__, self.pk)
)
# the following line would be the easiest way to do this, but it produces sql queries
# return self.polymorphic_ctype.model_class()
# so we use the following version, which uses the ContentType manager cache.
# Note that model_class() can return None for stale content types;
# when the content type record still exists but no longer refers to an existing model.
model = (
ContentType.objects.db_manager(self._state.db)
.get_for_id(self.polymorphic_ctype_id)
.model_class()
)
# Protect against bad imports (dumpdata without --natural) or other
# issues missing with the ContentType models.
if (
model is not None
and not issubclass(model, self.__class__)
and (
self.__class__._meta.proxy_for_model is None
or not issubclass(model, self.__class__._meta.proxy_for_model)
)
):
raise PolymorphicTypeInvalid(
"ContentType {0} for {1} #{2} does not point to a subclass!".format(
self.polymorphic_ctype_id, model, self.pk
)
)
return model
def get_real_concrete_instance_class_id(self):
model_class = self.get_real_instance_class()
if model_class is None:
return None
return (
ContentType.objects.db_manager(self._state.db)
.get_for_model(model_class, for_concrete_model=True)
.pk
)
def get_real_concrete_instance_class(self):
model_class = self.get_real_instance_class()
if model_class is None:
return None
return (
ContentType.objects.db_manager(self._state.db)
.get_for_model(model_class, for_concrete_model=True)
.model_class()
)
def get_real_instance(self):
"""
Upcast an object to it's actual type.
If a non-polymorphic manager (like base_objects) has been used to
retrieve objects, then the complete object with it's real class/type
and all fields may be retrieved with this method.
.. note::
Each method call executes one db query (if necessary).
Use the :meth:`~polymorphic.managers.PolymorphicQuerySet.get_real_instances`
to upcast a complete list in a single efficient query.
"""
real_model = self.get_real_instance_class()
if real_model == self.__class__:
return self
return real_model.objects.db_manager(self._state.db).get(pk=self.pk)
def __init__(self, *args, **kwargs):
"""Replace Django's inheritance accessor member functions for our model
(self.__class__) with our own versions.
We monkey patch them until a patch can be added to Django
(which would probably be very small and make all of this obsolete).
If we have inheritance of the form ModelA -> ModelB ->ModelC then
Django creates accessors like this:
- ModelA: modelb
- ModelB: modela_ptr, modelb, modelc
- ModelC: modela_ptr, modelb, modelb_ptr, modelc
These accessors allow Django (and everyone else) to travel up and down
the inheritance tree for the db object at hand.
The original Django accessors use our polymorphic manager.
But they should not. So we replace them with our own accessors that use
our appropriate base_objects manager.
"""
super(PolymorphicModel, self).__init__(*args, **kwargs)
if self.__class__.polymorphic_super_sub_accessors_replaced:
return
self.__class__.polymorphic_super_sub_accessors_replaced = True
def create_accessor_function_for_model(model, accessor_name):
def accessor_function(self):
attr = model._base_objects.get(pk=self.pk)
return attr
return accessor_function
subclasses_and_superclasses_accessors = (
self._get_inheritance_relation_fields_and_models()
)
for name, model in subclasses_and_superclasses_accessors.items():
# Here be dragons.
orig_accessor = getattr(self.__class__, name, None)
if issubclass(
type(orig_accessor),
(ReverseOneToOneDescriptor, ForwardManyToOneDescriptor),
):
setattr(
self.__class__,
name,
property(create_accessor_function_for_model(model, name)),
)
def _get_inheritance_relation_fields_and_models(self):
"""helper function for __init__:
determine names of all Django inheritance accessor member functions for type(self)"""
def add_model(model, field_name, result):
result[field_name] = model
def add_model_if_regular(model, field_name, result):
if (
issubclass(model, models.Model)
and model != models.Model
and model != self.__class__
and model != PolymorphicModel
):
add_model(model, field_name, result)
def add_all_super_models(model, result):
for super_cls, field_to_super in model._meta.parents.items():
if field_to_super is not None:
# if not a link to a proxy model, the field on model can have
# a different name to super_cls._meta.module_name, when the field
# is created manually using 'parent_link'
field_name = field_to_super.name
add_model_if_regular(super_cls, field_name, result)
add_all_super_models(super_cls, result)
def add_all_sub_models(super_cls, result):
# go through all subclasses of model
for sub_cls in super_cls.__subclasses__():
# super_cls may not be in sub_cls._meta.parents if super_cls is a proxy model
if super_cls in sub_cls._meta.parents:
# get the field that links sub_cls to super_cls
field_to_super = sub_cls._meta.parents[super_cls]
# if filed_to_super is not a link to a proxy model
if field_to_super is not None:
super_to_sub_related_field = field_to_super.remote_field
if super_to_sub_related_field.related_name is None:
# if related name is None the related field is the name of the subclass
to_subclass_fieldname = sub_cls.__name__.lower()
else:
# otherwise use the given related name
to_subclass_fieldname = (
super_to_sub_related_field.related_name
)
add_model_if_regular(sub_cls, to_subclass_fieldname, result)
result = {}
add_all_super_models(self.__class__, result)
add_all_sub_models(self.__class__, result)
return result

View File

@@ -0,0 +1,527 @@
# -*- coding: utf-8 -*-
"""
QuerySet for PolymorphicModel
"""
from __future__ import absolute_import
import copy
from collections import defaultdict
from django.contrib.contenttypes.models import ContentType
from django.db.models import FieldDoesNotExist
from django.db.models.query import ModelIterable, Q, QuerySet
from . import compat
from .query_translate import (
translate_polymorphic_field_path,
translate_polymorphic_filter_definitions_in_args,
translate_polymorphic_filter_definitions_in_kwargs,
translate_polymorphic_Q_object,
)
# chunk-size: maximum number of objects requested per db-request
# by the polymorphic queryset.iterator() implementation
Polymorphic_QuerySet_objects_per_request = 100
class PolymorphicModelIterable(ModelIterable):
"""
ModelIterable for PolymorphicModel
Yields real instances if qs.polymorphic_disabled is False,
otherwise acts like a regular ModelIterable.
"""
def __iter__(self):
base_iter = super(PolymorphicModelIterable, self).__iter__()
if self.queryset.polymorphic_disabled:
return base_iter
return self._polymorphic_iterator(base_iter)
def _polymorphic_iterator(self, base_iter):
"""
Here we do the same as::
real_results = queryset._get_real_instances(list(base_iter))
for o in real_results: yield o
but it requests the objects in chunks from the database,
with Polymorphic_QuerySet_objects_per_request per chunk
"""
while True:
base_result_objects = []
reached_end = False
# Make sure the base iterator is read in chunks instead of
# reading it completely, in case our caller read only a few objects.
for i in range(Polymorphic_QuerySet_objects_per_request):
try:
o = next(base_iter)
base_result_objects.append(o)
except StopIteration:
reached_end = True
break
real_results = self.queryset._get_real_instances(base_result_objects)
for o in real_results:
yield o
if reached_end:
return
def transmogrify(cls, obj):
"""
Upcast a class to a different type without asking questions.
"""
if "__init__" not in obj.__dict__:
# Just assign __class__ to a different value.
new = obj
new.__class__ = cls
else:
# Run constructor, reassign values
new = cls()
for k, v in obj.__dict__.items():
new.__dict__[k] = v
return new
###################################################################################
# PolymorphicQuerySet
class PolymorphicQuerySet(QuerySet):
"""
QuerySet for PolymorphicModel
Contains the core functionality for PolymorphicModel
Usually not explicitly needed, except if a custom queryset class
is to be used.
"""
def __init__(self, *args, **kwargs):
super(PolymorphicQuerySet, self).__init__(*args, **kwargs)
self._iterable_class = PolymorphicModelIterable
self.polymorphic_disabled = False
# A parallel structure to django.db.models.query.Query.deferred_loading,
# which we maintain with the untranslated field names passed to
# .defer() and .only() in order to be able to retranslate them when
# retrieving the real instance (so that the deferred fields apply
# to that queryset as well).
self.polymorphic_deferred_loading = (set([]), True)
def _clone(self, *args, **kwargs):
# Django's _clone only copies its own variables, so we need to copy ours here
new = super(PolymorphicQuerySet, self)._clone(*args, **kwargs)
new.polymorphic_disabled = self.polymorphic_disabled
new.polymorphic_deferred_loading = (
copy.copy(self.polymorphic_deferred_loading[0]),
self.polymorphic_deferred_loading[1],
)
return new
def as_manager(cls):
from .managers import PolymorphicManager
manager = PolymorphicManager.from_queryset(cls)()
manager._built_with_as_manager = True
return manager
as_manager.queryset_only = True
as_manager = classmethod(as_manager)
def bulk_create(self, objs, batch_size=None):
objs = list(objs)
for obj in objs:
obj.pre_save_polymorphic()
return super(PolymorphicQuerySet, self).bulk_create(objs, batch_size)
def non_polymorphic(self):
"""switch off polymorphic behaviour for this query.
When the queryset is evaluated, only objects of the type of the
base class used for this query are returned."""
qs = self._clone()
qs.polymorphic_disabled = True
if issubclass(qs._iterable_class, PolymorphicModelIterable):
qs._iterable_class = ModelIterable
return qs
def instance_of(self, *args):
"""Filter the queryset to only include the classes in args (and their subclasses)."""
# Implementation in _translate_polymorphic_filter_defnition.
return self.filter(instance_of=args)
def not_instance_of(self, *args):
"""Filter the queryset to exclude the classes in args (and their subclasses)."""
# Implementation in _translate_polymorphic_filter_defnition."""
return self.filter(not_instance_of=args)
def _filter_or_exclude(self, negate, *args, **kwargs):
# We override this internal Django functon as it is used for all filter member functions.
q_objects = translate_polymorphic_filter_definitions_in_args(
self.model, args, using=self.db
)
# filter_field='data'
additional_args = translate_polymorphic_filter_definitions_in_kwargs(
self.model, kwargs, using=self.db
)
return super(PolymorphicQuerySet, self)._filter_or_exclude(
negate, *(list(q_objects) + additional_args), **kwargs
)
def order_by(self, *field_names):
"""translate the field paths in the args, then call vanilla order_by."""
field_names = [
translate_polymorphic_field_path(self.model, a)
if isinstance(a, compat.string_types)
else a # allow expressions to pass unchanged
for a in field_names
]
return super(PolymorphicQuerySet, self).order_by(*field_names)
def defer(self, *fields):
"""
Translate the field paths in the args, then call vanilla defer.
Also retain a copy of the original fields passed, which we'll need
when we're retrieving the real instance (since we'll need to translate
them again, as the model will have changed).
"""
new_fields = [translate_polymorphic_field_path(self.model, a) for a in fields]
clone = super(PolymorphicQuerySet, self).defer(*new_fields)
clone._polymorphic_add_deferred_loading(fields)
return clone
def only(self, *fields):
"""
Translate the field paths in the args, then call vanilla only.
Also retain a copy of the original fields passed, which we'll need
when we're retrieving the real instance (since we'll need to translate
them again, as the model will have changed).
"""
new_fields = [translate_polymorphic_field_path(self.model, a) for a in fields]
clone = super(PolymorphicQuerySet, self).only(*new_fields)
clone._polymorphic_add_immediate_loading(fields)
return clone
def _polymorphic_add_deferred_loading(self, field_names):
"""
Follows the logic of django.db.models.query.Query.add_deferred_loading(),
but for the non-translated field names that were passed to self.defer().
"""
existing, defer = self.polymorphic_deferred_loading
if defer:
# Add to existing deferred names.
self.polymorphic_deferred_loading = existing.union(field_names), True
else:
# Remove names from the set of any existing "immediate load" names.
self.polymorphic_deferred_loading = existing.difference(field_names), False
def _polymorphic_add_immediate_loading(self, field_names):
"""
Follows the logic of django.db.models.query.Query.add_immediate_loading(),
but for the non-translated field names that were passed to self.only()
"""
existing, defer = self.polymorphic_deferred_loading
field_names = set(field_names)
if "pk" in field_names:
field_names.remove("pk")
field_names.add(self.model._meta.pk.name)
if defer:
# Remove any existing deferred names from the current set before
# setting the new names.
self.polymorphic_deferred_loading = field_names.difference(existing), False
else:
# Replace any existing "immediate load" field names.
self.polymorphic_deferred_loading = field_names, False
def _process_aggregate_args(self, args, kwargs):
"""for aggregate and annotate kwargs: allow ModelX___field syntax for kwargs, forbid it for args.
Modifies kwargs if needed (these are Aggregate objects, we translate the lookup member variable)"""
___lookup_assert_msg = "PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only"
def patch_lookup(a):
# The field on which the aggregate operates is
# stored inside a complex query expression.
if isinstance(a, Q):
translate_polymorphic_Q_object(self.model, a)
elif hasattr(a, "get_source_expressions"):
for source_expression in a.get_source_expressions():
if source_expression is not None:
patch_lookup(source_expression)
else:
a.name = translate_polymorphic_field_path(self.model, a.name)
def test___lookup(a):
""" *args might be complex expressions too in django 1.8 so
the testing for a '___' is rather complex on this one """
if isinstance(a, Q):
def tree_node_test___lookup(my_model, node):
" process all children of this Q node "
for i in range(len(node.children)):
child = node.children[i]
if type(child) == tuple:
# this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
assert "___" not in child[0], ___lookup_assert_msg
else:
# this Q object child is another Q object, recursively process this as well
tree_node_test___lookup(my_model, child)
tree_node_test___lookup(self.model, a)
elif hasattr(a, "get_source_expressions"):
for source_expression in a.get_source_expressions():
test___lookup(source_expression)
else:
assert "___" not in a.name, ___lookup_assert_msg
for a in args:
test___lookup(a)
for a in kwargs.values():
patch_lookup(a)
def annotate(self, *args, **kwargs):
"""translate the polymorphic field paths in the kwargs, then call vanilla annotate.
_get_real_instances will do the rest of the job after executing the query."""
self._process_aggregate_args(args, kwargs)
return super(PolymorphicQuerySet, self).annotate(*args, **kwargs)
def aggregate(self, *args, **kwargs):
"""translate the polymorphic field paths in the kwargs, then call vanilla aggregate.
We need no polymorphic object retrieval for aggregate => switch it off."""
self._process_aggregate_args(args, kwargs)
qs = self.non_polymorphic()
return super(PolymorphicQuerySet, qs).aggregate(*args, **kwargs)
# Starting with Django 1.9, the copy returned by 'qs.values(...)' has the
# same class as 'qs', so our polymorphic modifications would apply.
# We want to leave values queries untouched, so we set 'polymorphic_disabled'.
def _values(self, *args, **kwargs):
clone = super(PolymorphicQuerySet, self)._values(*args, **kwargs)
clone.polymorphic_disabled = True
return clone
# Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.
# The resulting objects are required to have a unique primary key within the result set
# (otherwise an error is thrown).
# The "polymorphic" keyword argument is not supported anymore.
# def extra(self, *args, **kwargs):
def _get_real_instances(self, base_result_objects):
"""
Polymorphic object loader
Does the same as:
return [ o.get_real_instance() for o in base_result_objects ]
but more efficiently.
The list base_result_objects contains the objects from the executed
base class query. The class of all of them is self.model (our base model).
Some, many or all of these objects were not created and stored as
class self.model, but as a class derived from self.model. We want to re-fetch
these objects from the db as their original class so we can return them
just as they were created/saved.
We identify these objects by looking at o.polymorphic_ctype, which specifies
the real class of these objects (the class at the time they were saved).
First, we sort the result objects in base_result_objects for their
subclass (from o.polymorphic_ctype), and then we execute one db query per
subclass of objects. Here, we handle any annotations from annotate().
Finally we re-sort the resulting objects into the correct order and
return them as a list.
"""
resultlist = [] # polymorphic list of result-objects
# dict contains one entry per unique model type occurring in result,
# in the format idlist_per_model[modelclass]=[list-of-object-ids]
idlist_per_model = defaultdict(list)
indexlist_per_model = defaultdict(list)
# django's automatic ".pk" field does not always work correctly for
# custom fields in derived objects (unclear yet who to put the blame on).
# We get different type(o.pk) in this case.
# We work around this by using the real name of the field directly
# for accessing the primary key of the the derived objects.
# We might assume that self.model._meta.pk.name gives us the name of the primary key field,
# but it doesn't. Therefore we use polymorphic_primary_key_name, which we set up in base.py.
pk_name = self.model.polymorphic_primary_key_name
# - sort base_result_object ids into idlist_per_model lists, depending on their real class;
# - store objects that already have the correct class into "results"
content_type_manager = ContentType.objects.db_manager(self.db)
self_model_class_id = content_type_manager.get_for_model(
self.model, for_concrete_model=False
).pk
self_concrete_model_class_id = content_type_manager.get_for_model(
self.model, for_concrete_model=True
).pk
for i, base_object in enumerate(base_result_objects):
if base_object.polymorphic_ctype_id == self_model_class_id:
# Real class is exactly the same as base class, go straight to results
resultlist.append(base_object)
else:
real_concrete_class = base_object.get_real_instance_class()
real_concrete_class_id = (
base_object.get_real_concrete_instance_class_id()
)
if real_concrete_class_id is None:
# Dealing with a stale content type
continue
elif real_concrete_class_id == self_concrete_model_class_id:
# Real and base classes share the same concrete ancestor,
# upcast it and put it in the results
resultlist.append(transmogrify(real_concrete_class, base_object))
else:
# This model has a concrete derived class, track it for bulk retrieval.
real_concrete_class = content_type_manager.get_for_id(
real_concrete_class_id
).model_class()
idlist_per_model[real_concrete_class].append(
getattr(base_object, pk_name)
)
indexlist_per_model[real_concrete_class].append(
(i, len(resultlist))
)
resultlist.append(None)
# For each model in "idlist_per_model" request its objects (the real model)
# from the db and store them in results[].
# Then we copy the annotate fields from the base objects to the real objects.
# Then we copy the extra() select fields from the base objects to the real objects.
# TODO: defer(), only(): support for these would be around here
for real_concrete_class, idlist in idlist_per_model.items():
indices = indexlist_per_model[real_concrete_class]
real_objects = real_concrete_class._base_objects.db_manager(self.db).filter(
**{("%s__in" % pk_name): idlist}
)
# copy select related configuration to new qs
real_objects.query.select_related = self.query.select_related
# Copy deferred fields configuration to the new queryset
deferred_loading_fields = []
existing_fields = self.polymorphic_deferred_loading[0]
for field in existing_fields:
try:
translated_field_name = translate_polymorphic_field_path(
real_concrete_class, field
)
except AssertionError:
if "___" in field:
# The originally passed argument to .defer() or .only()
# was in the form Model2B___field2, where Model2B is
# now a superclass of real_concrete_class. Thus it's
# sufficient to just use the field name.
translated_field_name = field.rpartition("___")[-1]
# Check if the field does exist.
# Ignore deferred fields that don't exist in this subclass type.
try:
real_concrete_class._meta.get_field(translated_field_name)
except FieldDoesNotExist:
continue
else:
raise
deferred_loading_fields.append(translated_field_name)
real_objects.query.deferred_loading = (
set(deferred_loading_fields),
self.query.deferred_loading[1],
)
real_objects_dict = {
getattr(real_object, pk_name): real_object
for real_object in real_objects
}
for i, j in indices:
base_object = base_result_objects[i]
o_pk = getattr(base_object, pk_name)
real_object = real_objects_dict.get(o_pk)
if real_object is None:
continue
# need shallow copy to avoid duplication in caches (see PR #353)
real_object = copy.copy(real_object)
real_class = real_object.get_real_instance_class()
# If the real class is a proxy, upcast it
if real_class != real_concrete_class:
real_object = transmogrify(real_class, real_object)
if self.query.annotations:
for anno_field_name in self.query.annotations.keys():
attr = getattr(base_object, anno_field_name)
setattr(real_object, anno_field_name, attr)
if self.query.extra_select:
for select_field_name in self.query.extra_select.keys():
attr = getattr(base_object, select_field_name)
setattr(real_object, select_field_name, attr)
resultlist[j] = real_object
resultlist = [i for i in resultlist if i]
# set polymorphic_annotate_names in all objects (currently just used for debugging/printing)
if self.query.annotations:
# get annotate field list
annotate_names = list(self.query.annotations.keys())
for real_object in resultlist:
real_object.polymorphic_annotate_names = annotate_names
# set polymorphic_extra_select_names in all objects (currently just used for debugging/printing)
if self.query.extra_select:
# get extra select field list
extra_select_names = list(self.query.extra_select.keys())
for real_object in resultlist:
real_object.polymorphic_extra_select_names = extra_select_names
return resultlist
def __repr__(self, *args, **kwargs):
if self.model.polymorphic_query_multiline_output:
result = [repr(o) for o in self.all()]
return "[ " + ",\n ".join(result) + " ]"
else:
return super(PolymorphicQuerySet, self).__repr__(*args, **kwargs)
class _p_list_class(list):
def __repr__(self, *args, **kwargs):
result = [repr(o) for o in self]
return "[ " + ",\n ".join(result) + " ]"
def get_real_instances(self, base_result_objects=None):
"""
Cast a list of objects to their actual classes.
This does roughly the same as::
return [ o.get_real_instance() for o in base_result_objects ]
but more efficiently.
:rtype: PolymorphicQuerySet
"""
"same as _get_real_instances, but make sure that __repr__ for ShowField... creates correct output"
if base_result_objects is None:
base_result_objects = self
olist = self._get_real_instances(base_result_objects)
if not self.model.polymorphic_query_multiline_output:
return olist
clist = PolymorphicQuerySet._p_list_class(olist)
return clist

View File

@@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
"""
PolymorphicQuerySet support functions
"""
from __future__ import absolute_import
import copy
from collections import deque
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.db import models
from django.db.models import Q
from django.db.models.fields.related import ForeignObjectRel, RelatedField
from django.db.utils import DEFAULT_DB_ALIAS
# These functions implement the additional filter- and Q-object functionality.
# They form a kind of small framework for easily adding more
# functionality to filters and Q objects.
# Probably a more general queryset enhancement class could be made out of them.
from polymorphic import compat
###################################################################################
# PolymorphicQuerySet support functions
def translate_polymorphic_filter_definitions_in_kwargs(
queryset_model, kwargs, using=DEFAULT_DB_ALIAS
):
"""
Translate the keyword argument list for PolymorphicQuerySet.filter()
Any kwargs with special polymorphic functionality are replaced in the kwargs
dict with their vanilla django equivalents.
For some kwargs a direct replacement is not possible, as a Q object is needed
instead to implement the required functionality. In these cases the kwarg is
deleted from the kwargs dict and a Q object is added to the return list.
Modifies: kwargs dict
Returns: a list of non-keyword-arguments (Q objects) to be added to the filter() query.
"""
additional_args = []
for field_path, val in kwargs.copy().items(): # Python 3 needs copy
new_expr = _translate_polymorphic_filter_definition(
queryset_model, field_path, val, using=using
)
if type(new_expr) == tuple:
# replace kwargs element
del kwargs[field_path]
kwargs[new_expr[0]] = new_expr[1]
elif isinstance(new_expr, models.Q):
del kwargs[field_path]
additional_args.append(new_expr)
return additional_args
def translate_polymorphic_Q_object(
queryset_model, potential_q_object, using=DEFAULT_DB_ALIAS
):
def tree_node_correct_field_specs(my_model, node):
" process all children of this Q node "
for i in range(len(node.children)):
child = node.children[i]
if type(child) == tuple:
# this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
key, val = child
new_expr = _translate_polymorphic_filter_definition(
my_model, key, val, using=using
)
if new_expr:
node.children[i] = new_expr
else:
# this Q object child is another Q object, recursively process this as well
tree_node_correct_field_specs(my_model, child)
if isinstance(potential_q_object, models.Q):
tree_node_correct_field_specs(queryset_model, potential_q_object)
return potential_q_object
def translate_polymorphic_filter_definitions_in_args(
queryset_model, args, using=DEFAULT_DB_ALIAS
):
"""
Translate the non-keyword argument list for PolymorphicQuerySet.filter()
In the args list, we return all kwargs to Q-objects that contain special
polymorphic functionality with their vanilla django equivalents.
We traverse the Q object tree for this (which is simple).
Returns: modified Q objects
"""
return [
translate_polymorphic_Q_object(queryset_model, copy.deepcopy(q), using=using)
for q in args
]
def _translate_polymorphic_filter_definition(
queryset_model, field_path, field_val, using=DEFAULT_DB_ALIAS
):
"""
Translate a keyword argument (field_path=field_val), as used for
PolymorphicQuerySet.filter()-like functions (and Q objects).
A kwarg with special polymorphic functionality is translated into
its vanilla django equivalent, which is returned, either as tuple
(field_path, field_val) or as Q object.
Returns: kwarg tuple or Q object or None (if no change is required)
"""
# handle instance_of expressions or alternatively,
# if this is a normal Django filter expression, return None
if field_path == "instance_of":
return create_instanceof_q(field_val, using=using)
elif field_path == "not_instance_of":
return create_instanceof_q(field_val, not_instance_of=True, using=using)
elif "___" not in field_path:
return None # no change
# filter expression contains '___' (i.e. filter for polymorphic field)
# => get the model class specified in the filter expression
newpath = translate_polymorphic_field_path(queryset_model, field_path)
return (newpath, field_val)
def translate_polymorphic_field_path(queryset_model, field_path):
"""
Translate a field path from a keyword argument, as used for
PolymorphicQuerySet.filter()-like functions (and Q objects).
Supports leading '-' (for order_by args).
E.g.: if queryset_model is ModelA, then "ModelC___field3" is translated
into modela__modelb__modelc__field3.
Returns: translated path (unchanged, if no translation needed)
"""
if not isinstance(field_path, compat.string_types):
raise ValueError("Expected field name as string: {0}".format(field_path))
classname, sep, pure_field_path = field_path.partition("___")
if not sep:
return field_path
assert classname, "PolymorphicModel: %s: bad field specification" % field_path
negated = False
if classname[0] == "-":
negated = True
classname = classname.lstrip("-")
if "__" in classname:
# the user has app label prepended to class name via __ => use Django's get_model function
appname, sep, classname = classname.partition("__")
model = apps.get_model(appname, classname)
assert model, "PolymorphicModel: model %s (in app %s) not found!" % (
model.__name__,
appname,
)
if not issubclass(model, queryset_model):
e = (
'PolymorphicModel: queryset filter error: "'
+ model.__name__
+ '" is not derived from "'
+ queryset_model.__name__
+ '"'
)
raise AssertionError(e)
else:
# the user has only given us the class name via ___
# => select the model from the sub models of the queryset base model
# Test whether it's actually a regular relation__ _fieldname (the field starting with an _)
# so no tripple ClassName___field was intended.
try:
# This also retreives M2M relations now (including reverse foreign key relations)
field = queryset_model._meta.get_field(classname)
if isinstance(field, (RelatedField, ForeignObjectRel)):
# Can also test whether the field exists in the related object to avoid ambiguity between
# class names and field names, but that never happens when your class names are in CamelCase.
return field_path # No exception raised, field does exist.
except models.FieldDoesNotExist:
pass
submodels = _get_all_sub_models(queryset_model)
model = submodels.get(classname, None)
assert model, "PolymorphicModel: model %s not found (not a subclass of %s)!" % (
classname,
queryset_model.__name__,
)
basepath = _create_base_path(queryset_model, model)
if negated:
newpath = "-"
else:
newpath = ""
newpath += basepath
if basepath:
newpath += "__"
newpath += pure_field_path
return newpath
def _get_all_sub_models(base_model):
"""#Collect all sub-models, this should be optimized (cached)"""
result = {}
queue = deque([base_model])
while queue:
model = queue.popleft()
if issubclass(model, models.Model) and model != models.Model:
# model name is occurring twice in submodel inheritance tree => Error
if model.__name__ in result and model != result[model.__name__]:
raise FieldError(
"PolymorphicModel: model name alone is ambiguous: %s.%s and %s.%s match!\n"
"In this case, please use the syntax: applabel__ModelName___field"
% (
model._meta.app_label,
model.__name__,
result[model.__name__]._meta.app_label,
result[model.__name__].__name__,
)
)
result[model.__name__] = model
queue.extend(model.__subclasses__())
return result
def _create_base_path(baseclass, myclass):
# create new field path for expressions, e.g. for baseclass=ModelA, myclass=ModelC
# 'modelb__modelc" is returned
for b in myclass.__bases__:
if b == baseclass:
return _get_query_related_name(myclass)
path = _create_base_path(baseclass, b)
if path:
if b._meta.abstract or b._meta.proxy:
return _get_query_related_name(myclass)
else:
return path + "__" + _get_query_related_name(myclass)
return ""
def _get_query_related_name(myclass):
for f in myclass._meta.local_fields:
if isinstance(f, models.OneToOneField) and f.remote_field.parent_link:
return f.related_query_name()
# Fallback to undetected name,
# this happens on proxy models (e.g. SubclassSelectorProxyModel)
return myclass.__name__.lower()
def create_instanceof_q(modellist, not_instance_of=False, using=DEFAULT_DB_ALIAS):
"""
Helper function for instance_of / not_instance_of
Creates and returns a Q object that filters for the models in modellist,
including all subclasses of these models (as we want to do the same
as pythons isinstance() ).
.
We recursively collect all __subclasses__(), create a Q filter for each,
and or-combine these Q objects. This could be done much more
efficiently however (regarding the resulting sql), should an optimization
be needed.
"""
if not modellist:
return None
if not isinstance(modellist, (list, tuple)):
from .models import PolymorphicModel
if issubclass(modellist, PolymorphicModel):
modellist = [modellist]
else:
raise TypeError(
"PolymorphicModel: instance_of expects a list of (polymorphic) "
"models or a single (polymorphic) model"
)
contenttype_ids = _get_mro_content_type_ids(modellist, using)
q = Q(polymorphic_ctype__in=sorted(contenttype_ids))
if not_instance_of:
q = ~q
return q
def _get_mro_content_type_ids(models, using):
contenttype_ids = set()
for model in models:
ct = ContentType.objects.db_manager(using).get_for_model(
model, for_concrete_model=False
)
contenttype_ids.add(ct.pk)
subclasses = model.__subclasses__()
if subclasses:
contenttype_ids.update(_get_mro_content_type_ids(subclasses, using))
return contenttype_ids

View File

@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
import re
from django.db import models
from . import compat
from .compat import python_2_unicode_compatible
RE_DEFERRED = re.compile("_Deferred_.*")
@python_2_unicode_compatible
class ShowFieldBase(object):
""" base class for the ShowField... model mixins, does the work """
# cause nicer multiline PolymorphicQuery output
polymorphic_query_multiline_output = True
polymorphic_showfield_type = False
polymorphic_showfield_content = False
polymorphic_showfield_deferred = False
# these may be overridden by the user
polymorphic_showfield_max_line_width = None
polymorphic_showfield_max_field_width = 20
polymorphic_showfield_old_format = False
def __repr__(self):
return self.__str__()
def _showfields_get_content(self, field_name, field_type=type(None)):
"helper for __unicode__"
content = getattr(self, field_name)
if self.polymorphic_showfield_old_format:
out = ": "
else:
out = " "
if issubclass(field_type, models.ForeignKey):
if content is None:
out += "None"
else:
out += content.__class__.__name__
elif issubclass(field_type, models.ManyToManyField):
out += "%d" % content.count()
elif isinstance(content, compat.integer_types):
out += str(content)
elif content is None:
out += "None"
else:
txt = str(content)
if len(txt) > self.polymorphic_showfield_max_field_width:
txt = txt[: self.polymorphic_showfield_max_field_width - 2] + ".."
out += '"' + txt + '"'
return out
def _showfields_add_regular_fields(self, parts):
"helper for __unicode__"
done_fields = set()
for field in self._meta.fields + self._meta.many_to_many:
if (
field.name in self.polymorphic_internal_model_fields
or "_ptr" in field.name
):
continue
if field.name in done_fields:
continue # work around django diamond inheritance problem
done_fields.add(field.name)
out = field.name
# if this is the standard primary key named "id", print it as we did with older versions of django_polymorphic
if (
field.primary_key
and field.name == "id"
and type(field) == models.AutoField
):
out += " " + str(getattr(self, field.name))
# otherwise, display it just like all other fields (with correct type, shortened content etc.)
else:
if self.polymorphic_showfield_type:
out += " (" + type(field).__name__
if field.primary_key:
out += "/pk"
out += ")"
if self.polymorphic_showfield_content:
out += self._showfields_get_content(field.name, type(field))
parts.append((False, out, ","))
def _showfields_add_dynamic_fields(self, field_list, title, parts):
"helper for __unicode__"
parts.append((True, "- " + title, ":"))
for field_name in field_list:
out = field_name
content = getattr(self, field_name)
if self.polymorphic_showfield_type:
out += " (" + type(content).__name__ + ")"
if self.polymorphic_showfield_content:
out += self._showfields_get_content(field_name)
parts.append((False, out, ","))
def __str__(self):
# create list ("parts") containing one tuple for each title/field:
# ( bool: new section , item-text , separator to use after item )
# start with model name
parts = [(True, RE_DEFERRED.sub("", self.__class__.__name__), ":")]
# add all regular fields
self._showfields_add_regular_fields(parts)
# add annotate fields
if hasattr(self, "polymorphic_annotate_names"):
self._showfields_add_dynamic_fields(
self.polymorphic_annotate_names, "Ann", parts
)
# add extra() select fields
if hasattr(self, "polymorphic_extra_select_names"):
self._showfields_add_dynamic_fields(
self.polymorphic_extra_select_names, "Extra", parts
)
if self.polymorphic_showfield_deferred:
fields = self.get_deferred_fields()
if fields:
parts.append(
(False, "deferred[{0}]".format(",".join(sorted(fields))), "")
)
# format result
indent = len(self.__class__.__name__) + 5
indentstr = "".rjust(indent)
out = ""
xpos = 0
possible_line_break_pos = None
for i in range(len(parts)):
new_section, p, separator = parts[i]
final = i == len(parts) - 1
if not final:
next_new_section, _, _ = parts[i + 1]
if (
self.polymorphic_showfield_max_line_width
and xpos + len(p) > self.polymorphic_showfield_max_line_width
and possible_line_break_pos is not None
):
rest = out[possible_line_break_pos:]
out = out[:possible_line_break_pos]
out += "\n" + indentstr + rest
xpos = indent + len(rest)
out += p
xpos += len(p)
if not final:
if not next_new_section:
out += separator
xpos += len(separator)
out += " "
xpos += 1
if not new_section:
possible_line_break_pos = len(out)
return "<" + out + ">"
class ShowFieldType(ShowFieldBase):
""" model mixin that shows the object's class and it's field types """
polymorphic_showfield_type = True
class ShowFieldContent(ShowFieldBase):
""" model mixin that shows the object's class, it's fields and field contents """
polymorphic_showfield_content = True
class ShowFieldTypeAndContent(ShowFieldBase):
""" model mixin, like ShowFieldContent, but also show field types """
polymorphic_showfield_type = True
polymorphic_showfield_content = True

View File

@@ -0,0 +1,34 @@
.polymorphic-add-choice {
position: relative;
clear: left;
}
.polymorphic-add-choice a:focus {
text-decoration: none;
}
.polymorphic-type-menu {
position: absolute;
top: 2.2em;
left: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
padding: 2px;
background-color: #fff;
z-index: 1000;
}
.polymorphic-type-menu ul {
padding: 2px;
margin: 0;
}
.polymorphic-type-menu li {
list-style: none inside none;
padding: 4px 8px;
}
.inline-related.empty-form {
/* needed for grapelli, which uses grp-empty-form */
display: none;
}

View File

@@ -0,0 +1,338 @@
/*global DateTimeShortcuts, SelectFilter*/
// This is a slightly adapted version of Django's inlines.js
// Forked for polymorphic by Diederik van der Boor
/**
* Django admin inlines
*
* Based on jQuery Formset 1.1
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
* Copyright (c) 2009, Stanislaus Madueke
* All rights reserved.
*
* Spiced up with Code from Zain Memon's GSoC project 2009
* and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip.
*
* Licensed under the New BSD License
* See: http://www.opensource.org/licenses/bsd-license.php
*/
(function($) {
'use strict';
$.fn.polymorphicFormset = function(opts) {
var options = $.extend({}, $.fn.polymorphicFormset.defaults, opts);
var $this = $(this);
var $parent = $this.parent();
var updateElementIndex = function(el, prefix, ndx) {
var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
var replacement = prefix + "-" + ndx;
if ($(el).prop("for")) {
$(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
el.name = el.name.replace(id_regex, replacement);
}
};
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off");
var nextIndex = parseInt(totalForms.val(), 10);
var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off");
// only show the add button if we are allowed to add more items,
// note that max_num = None translates to a blank string.
var showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0;
$this.each(function(i) {
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
});
if ($this.length && showAddButton) {
var addContainer;
var menuButton;
var addButtons;
if(options.childTypes == null) {
throw Error("The polymorphic fieldset options.childTypes is not defined!");
}
// For Polymorphic inlines, the add button opens a menu.
var menu = '<div class="polymorphic-type-menu" style="display: none;"><ul>';
for (var i = 0; i < options.childTypes.length; i++) {
var obj = options.childTypes[i];
menu += '<li><a href="#" data-type="' + obj.type + '">' + obj.name + '</a></li>';
}
menu += '</ul></div>';
if ($this.prop("tagName") === "TR") {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = this.eq(-1).children().length;
$parent.append('<tr class="' + options.addCssClass + ' polymorphic-add-choice"><td colspan="' + numCols + '"><a href="#">' + options.addText + "</a>" + menu + "</tr>");
addContainer = $parent.find("tr:last > td");
menuButton = addContainer.children('a');
addButtons = addContainer.find("li a");
} else {
// Otherwise, insert it immediately after the last form:
$this.filter(":last").after('<div class="' + options.addCssClass + ' polymorphic-add-choice"><a href="#">' + options.addText + "</a>" + menu + "</div>");
addContainer = $this.filter(":last").next();
menuButton = addContainer.children('a');
addButtons = addContainer.find("li a");
}
menuButton.click(function(event) {
event.preventDefault();
event.stopPropagation(); // for menu hide
var $menu = $(event.target).next('.polymorphic-type-menu');
if(! $menu.is(':visible')) {
var hideMenu = function() {
$menu.slideUp(50);
$(document).unbind('click', hideMenu);
};
$(document).click(hideMenu);
}
$menu.slideToggle(50);
});
addButtons.click(function(event) {
event.preventDefault();
var polymorphicType = $(event.target).attr('data-type'); // Select polymorphic type.
var template = $("#" + polymorphicType + "-empty");
var row = template.clone(true);
row.removeClass(options.emptyCssClass)
.addClass(options.formCssClass)
.attr("id", options.prefix + "-" + nextIndex);
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
} else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
}
row.find("*").each(function() {
updateElementIndex(this, options.prefix, totalForms.val());
});
// Insert the new form when it has been fully edited
row.insertBefore($(template));
// Update number of total forms
$(totalForms).val(parseInt(totalForms.val(), 10) + 1);
nextIndex += 1;
// Hide add button in case we've hit the max, except we want to add infinitely
if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
addContainer.hide();
}
// The delete button of each row triggers a bunch of other things
row.find("a." + options.deleteCssClass).click(function(e1) {
e1.preventDefault();
// Remove the parent form containing this button:
row.remove();
nextIndex -= 1;
// If a post-delete callback was provided, call it with the deleted form:
if (options.removed) {
options.removed(row);
}
$(document).trigger('formset:removed', [row, options.prefix]);
// Update the TOTAL_FORMS form count.
var forms = $("." + options.formCssClass);
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
// Show add button again once we drop below max
if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) {
addContainer.show();
}
// Also, update names and ids for all remaining form controls
// so they remain in sequence:
var i, formCount;
var updateElementCallback = function() {
updateElementIndex(this, options.prefix, i);
};
for (i = 0, formCount = forms.length; i < formCount; i++) {
updateElementIndex($(forms).get(i), options.prefix, i);
$(forms.get(i)).find("*").each(updateElementCallback);
}
});
// If a post-add callback was supplied, call it with the added form:
if (options.added) {
options.added(row);
}
$(document).trigger('formset:added', [row, options.prefix]);
});
}
return this;
};
/* Setup plugin defaults */
$.fn.polymorphicFormset.defaults = {
prefix: "form", // The form prefix for your django formset
addText: "add another", // Text for the add link
childTypes: null, // defined by the client.
deleteText: "remove", // Text for the delete link
addCssClass: "add-row", // CSS class applied to the add link
deleteCssClass: "delete-row", // CSS class applied to the delete link
emptyCssClass: "empty-row", // CSS class applied to the empty row
formCssClass: "dynamic-form", // CSS class applied to each form in a formset
added: null, // Function called each time a new form is added
removed: null, // Function called each time a form is deleted
addButton: null // Existing add button to use
};
// Tabular inlines ---------------------------------------------------------
$.fn.tabularPolymorphicFormset = function(options) {
var $rows = $(this);
var alternatingRows = function(row) {
$($rows.selector).not(".add-row").removeClass("row1 row2")
.filter(":even").addClass("row1").end()
.filter(":odd").addClass("row2");
};
var reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
var updateSelectFilter = function() {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof SelectFilter !== 'undefined') {
$('.selectfilter').each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], false);
});
$('.selectfilterstacked').each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], true);
});
}
};
var initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
var field = $(this),
input = field.find('input, select, textarea'),
dependency_list = input.data('dependency_list') || [],
dependencies = [];
$.each(dependency_list, function(i, field_name) {
dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id'));
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
};
$rows.polymorphicFormset({
prefix: options.prefix,
addText: options.addText,
childTypes: options.childTypes,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: alternatingRows,
added: function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
alternatingRows(row);
},
addButton: options.addButton
});
return $rows;
};
// Stacked inlines ---------------------------------------------------------
$.fn.stackedPolymorphicFormset = function(options) {
var $rows = $(this);
var updateInlineLabel = function(row) {
$($rows.selector).find(".inline_label").each(function(i) {
var count = i + 1;
$(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
});
};
var reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force, yuck.
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
var updateSelectFilter = function() {
// If any SelectFilter widgets were added, instantiate a new instance.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], false);
});
$(".selectfilterstacked").each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], true);
});
}
};
var initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
var field = $(this),
input = field.find('input, select, textarea'),
dependency_list = input.data('dependency_list') || [],
dependencies = [];
$.each(dependency_list, function(i, field_name) {
dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id'));
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
};
$rows.polymorphicFormset({
prefix: options.prefix,
addText: options.addText,
childTypes: options.childTypes,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: updateInlineLabel,
added: function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
updateInlineLabel(row);
},
addButton: options.addButton
});
return $rows;
};
$(document).ready(function() {
$(".js-inline-polymorphic-admin-formset").each(function() {
var data = $(this).data(),
inlineOptions = data.inlineFormset;
switch(data.inlineType) {
case "stacked":
$(inlineOptions.name + "-group .inline-related").stackedPolymorphicFormset(inlineOptions.options);
break;
case "tabular":
$(inlineOptions.name + "-group .tabular.inline-related tbody tr").tabularPolymorphicFormset(inlineOptions.options);
break;
}
});
});
})(django.jQuery);

View File

@@ -0,0 +1,11 @@
{% extends "admin/change_form.html" %}
{% if save_on_top %}
{% block submit_buttons_top %}
{% include 'admin/submit_line.html' with show_save=1 %}
{% endblock %}
{% endif %}
{% block submit_buttons_bottom %}
{% include 'admin/submit_line.html' with show_save=1 %}
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "admin/change_form.html" %}
{% load polymorphic_admin_tags %}
{% block breadcrumbs %}
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "admin/delete_confirmation.html" %}
{% load polymorphic_admin_tags %}
{% block breadcrumbs %}
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% load i18n admin_urls static %}
<div class="js-inline-polymorphic-admin-formset inline-group"
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }}">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}
<div class="inline-related inline-{{ inline_admin_form.model_admin.opts.model_name }}{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if inline_admin_form.is_empty %} empty-form {% endif %}{% if forloop.last %} last-related{% endif %}"
id="{% if inline_admin_form.original.pk %}{{ inline_admin_formset.formset.prefix }}-{{ forloop.counter0 }}{% else %}{{ inline_admin_form.model_admin.opts.model_name }}-empty{% endif %}">
<h3><b>{{ inline_admin_form.model_admin.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
{% if inline_admin_form.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
</h3>
{% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }}
{{ inline_admin_form.polymorphic_ctype_field.field }}
</div>
{% endfor %}
</fieldset>
</div>

View File

@@ -0,0 +1,6 @@
{% extends "admin/object_history.html" %}
{% load polymorphic_admin_tags %}
{% block breadcrumbs %}
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
{% endblock %}

View File

@@ -0,0 +1,77 @@
"""
Template tags for polymorphic
The ``polymorphic_formset_tags`` Library
----------------------------------------
.. versionadded:: 1.1
To render formsets in the frontend, the ``polymorphic_tags`` provides extra
filters to implement HTML rendering of polymorphic formsets.
The following filters are provided;
* ``{{ formset|as_script_options }}`` render the ``data-options`` for a JavaScript formset library.
* ``{{ formset|include_empty_form }}`` provide the placeholder form for an add button.
* ``{{ form|as_form_type }}`` return the model name that the form instance uses.
* ``{{ model|as_model_name }}`` performs the same, for a model class or instance.
.. code-block:: html+django
{% load i18n polymorphic_formset_tags %}
<div class="inline-group" id="{{ formset.prefix }}-group" data-options="{{ formset|as_script_options }}">
{% block add_button %}
{% if formset.show_add_button|default_if_none:'1' %}
{% if formset.empty_forms %}
{# django-polymorphic formset (e.g. PolymorphicInlineFormSetView) #}
<div class="btn-group" role="group">
{% for model in formset.child_forms %}
<a type="button" data-type="{{ model|as_model_name }}" class="js-add-form btn btn-default">{% glyphicon 'plus' %} {{ model|as_verbose_name }}</a>
{% endfor %}
</div>
{% else %}
<a class="btn btn-default js-add-form">{% trans "Add" %}</a>
{% endif %}
{% endif %}
{% endblock %}
{{ formset.management_form }}
{% for form in formset|include_empty_form %}
{% block formset_form_wrapper %}
<div id="{{ form.prefix }}" data-inline-type="{{ form|as_form_type|lower }}" class="inline-related{% if '__prefix__' in form.prefix %} empty-form{% endif %}">
{{ form.non_field_errors }}
{# Add the 'pk' field that is not mentioned in crispy #}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
{% block formset_form %}
{% crispy form %}
{% endblock %}
</div>
{% endblock %}
{% endfor %}
</div>
The ``polymorphic_admin_tags`` Library
--------------------------------------
The ``{% breadcrumb_scope ... %}`` tag makes sure the ``{{ opts }}`` and ``{{ app_label }}``
values are temporary based on the provided ``{{ base_opts }}``.
This allows fixing the breadcrumb in admin templates:
.. code-block:: html+django
{% extends "admin/change_form.html" %}
{% load polymorphic_admin_tags %}
{% block breadcrumbs %}
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
{% endblock %}
"""

View File

@@ -0,0 +1,52 @@
from django.template import Library, Node, TemplateSyntaxError
from polymorphic import compat
register = Library()
class BreadcrumbScope(Node):
def __init__(self, base_opts, nodelist):
self.base_opts = base_opts
self.nodelist = nodelist # Note, takes advantage of Node.child_nodelists
@classmethod
def parse(cls, parser, token):
bits = token.split_contents()
if len(bits) == 2:
(tagname, base_opts) = bits
base_opts = parser.compile_filter(base_opts)
nodelist = parser.parse(("endbreadcrumb_scope",))
parser.delete_first_token()
return cls(base_opts=base_opts, nodelist=nodelist)
else:
raise TemplateSyntaxError(
"{0} tag expects 1 argument".format(token.contents[0])
)
def render(self, context):
# app_label is really hard to overwrite in the standard Django ModelAdmin.
# To insert it in the template, the entire render_change_form() and delete_view() have to copied and adjusted.
# Instead, have an assignment tag that inserts that in the template.
base_opts = self.base_opts.resolve(context)
new_vars = {}
if base_opts and not isinstance(base_opts, compat.string_types):
new_vars = {
"app_label": base_opts.app_label, # What this is all about
"opts": base_opts,
}
new_scope = context.push()
new_scope.update(new_vars)
html = self.nodelist.render(context)
context.pop()
return html
@register.tag
def breadcrumb_scope(parser, token):
"""
Easily allow the breadcrumb to be generated in the admin change templates.
"""
return BreadcrumbScope.parse(parser, token)

View File

@@ -0,0 +1,79 @@
import json
from django.template import Library
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext
from polymorphic.formsets import BasePolymorphicModelFormSet
register = Library()
@register.filter()
def include_empty_form(formset):
"""
Make sure the "empty form" is included when displaying a formset (typically table with input rows)
"""
for form in formset:
yield form
if hasattr(formset, "empty_forms"):
# BasePolymorphicModelFormSet
for form in formset.empty_forms:
yield form
else:
# Standard Django formset
yield formset.empty_form
@register.filter
def as_script_options(formset):
"""
A JavaScript data structure for the JavaScript code
This generates the ``data-options`` attribute for ``jquery.django-inlines.js``
The formset may define the following extra attributes:
- ``verbose_name``
- ``add_text``
- ``show_add_button``
"""
verbose_name = getattr(formset, "verbose_name", formset.model._meta.verbose_name)
options = {
"prefix": formset.prefix,
"pkFieldName": formset.model._meta.pk.name,
"addText": getattr(formset, "add_text", None)
or ugettext("Add another %(verbose_name)s")
% {"verbose_name": capfirst(verbose_name)},
"showAddButton": getattr(formset, "show_add_button", True),
"deleteText": ugettext("Delete"),
}
if isinstance(formset, BasePolymorphicModelFormSet):
# Allow to add different types
options["childTypes"] = [
{
"name": force_text(model._meta.verbose_name),
"type": model._meta.model_name,
}
for model in formset.child_forms.keys()
]
return json.dumps(options)
@register.filter
def as_form_type(form):
"""
Usage: ``{{ form|as_form_type }}``
"""
return form._meta.model._meta.model_name
@register.filter
def as_model_name(model):
"""
Usage: ``{{ model|as_model_name }}``
"""
return model._meta.model_name

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,141 @@
from django.contrib import admin
from django.contrib.contenttypes.models import ContentType
from django.utils.html import escape
from polymorphic.admin import (
PolymorphicChildModelAdmin,
PolymorphicChildModelFilter,
PolymorphicInlineSupportMixin,
PolymorphicParentModelAdmin,
StackedPolymorphicInline,
)
from polymorphic.tests.admintestcase import AdminTestCase
from polymorphic.tests.models import (
InlineModelA,
InlineModelB,
InlineParent,
Model2A,
Model2B,
Model2C,
Model2D,
)
class PolymorphicAdminTests(AdminTestCase):
def test_admin_registration(self):
"""
Test how the registration works
"""
@self.register(Model2A)
class Model2Admin(PolymorphicParentModelAdmin):
base_model = Model2A
list_filter = (PolymorphicChildModelFilter,)
child_models = (Model2B, Model2C, Model2D)
@self.register(Model2B)
@self.register(Model2C)
@self.register(Model2D)
class Model2ChildAdmin(PolymorphicChildModelAdmin):
base_model = Model2A
base_fieldsets = (("Base fields", {"fields": ("field1",)}),)
# -- add page
ct_id = ContentType.objects.get_for_model(Model2D).pk
self.admin_get_add(Model2A) # shows type page
self.admin_get_add(Model2A, qs="?ct_id={}".format(ct_id)) # shows type page
self.admin_get_add(Model2A) # shows type page
self.admin_post_add(
Model2A,
{"field1": "A", "field2": "B", "field3": "C", "field4": "D"},
qs="?ct_id={}".format(ct_id),
)
d_obj = Model2A.objects.all()[0]
self.assertEqual(d_obj.__class__, Model2D)
self.assertEqual(d_obj.field1, "A")
self.assertEqual(d_obj.field2, "B")
# -- list page
self.admin_get_changelist(Model2A) # asserts 200
# -- edit
response = self.admin_get_change(Model2A, d_obj.pk)
self.assertContains(response, "field4")
self.admin_post_change(
Model2A,
d_obj.pk,
{"field1": "A2", "field2": "B2", "field3": "C2", "field4": "D2"},
)
d_obj.refresh_from_db()
self.assertEqual(d_obj.field1, "A2")
self.assertEqual(d_obj.field2, "B2")
self.assertEqual(d_obj.field3, "C2")
self.assertEqual(d_obj.field4, "D2")
# -- history
self.admin_get_history(Model2A, d_obj.pk)
# -- delete
self.admin_get_delete(Model2A, d_obj.pk)
self.admin_post_delete(Model2A, d_obj.pk)
self.assertRaises(Model2A.DoesNotExist, lambda: d_obj.refresh_from_db())
def test_admin_inlines(self):
"""
Test the registration of inline models.
"""
class InlineModelAChild(StackedPolymorphicInline.Child):
model = InlineModelA
class InlineModelBChild(StackedPolymorphicInline.Child):
model = InlineModelB
class Inline(StackedPolymorphicInline):
model = InlineModelA
child_inlines = (InlineModelAChild, InlineModelBChild)
@self.register(InlineParent)
class InlineParentAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin):
inlines = (Inline,)
parent = InlineParent.objects.create(title="FOO")
self.assertEqual(parent.inline_children.count(), 0)
# -- get edit page
response = self.admin_get_change(InlineParent, parent.pk)
# Make sure the fieldset has the right data exposed in data-inline-formset
self.assertContains(response, "childTypes")
self.assertContains(response, escape('"type": "inlinemodela"'))
self.assertContains(response, escape('"type": "inlinemodelb"'))
# -- post edit page
self.admin_post_change(
InlineParent,
parent.pk,
{
"title": "FOO2",
"inline_children-INITIAL_FORMS": 0,
"inline_children-TOTAL_FORMS": 1,
"inline_children-MIN_NUM_FORMS": 0,
"inline_children-MAX_NUM_FORMS": 1000,
"inline_children-0-parent": parent.pk,
"inline_children-0-polymorphic_ctype": ContentType.objects.get_for_model(
InlineModelB
).pk,
"inline_children-0-field1": "A2",
"inline_children-0-field2": "B2",
},
)
parent.refresh_from_db()
self.assertEqual(parent.title, "FOO2")
self.assertEqual(parent.inline_children.count(), 1)
child = parent.inline_children.all()[0]
self.assertEqual(child.__class__, InlineModelB)
self.assertEqual(child.field1, "A2")
self.assertEqual(child.field2, "B2")

View File

@@ -0,0 +1,130 @@
from __future__ import print_function
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.test import TestCase
from polymorphic.tests.models import (
Base,
BlogA,
BlogEntry,
Model2A,
Model2B,
Model2C,
Model2D,
ModelX,
ModelY,
One2OneRelatingModel,
RelatingModel,
)
class MultipleDatabasesTests(TestCase):
multi_db = True
def test_save_to_non_default_database(self):
Model2A.objects.db_manager("secondary").create(field1="A1")
Model2C(field1="C1", field2="C2", field3="C3").save(using="secondary")
Model2B.objects.create(field1="B1", field2="B2")
Model2D(field1="D1", field2="D2", field3="D3", field4="D4").save()
self.assertQuerysetEqual(
Model2A.objects.order_by("id"),
[Model2B, Model2D],
transform=lambda o: o.__class__,
)
self.assertQuerysetEqual(
Model2A.objects.db_manager("secondary").order_by("id"),
[Model2A, Model2C],
transform=lambda o: o.__class__,
)
def test_instance_of_filter_on_non_default_database(self):
Base.objects.db_manager("secondary").create(field_b="B1")
ModelX.objects.db_manager("secondary").create(field_b="B", field_x="X")
ModelY.objects.db_manager("secondary").create(field_b="Y", field_y="Y")
objects = Base.objects.db_manager("secondary").filter(instance_of=Base)
self.assertQuerysetEqual(
objects,
[Base, ModelX, ModelY],
transform=lambda o: o.__class__,
ordered=False,
)
self.assertQuerysetEqual(
Base.objects.db_manager("secondary").filter(instance_of=ModelX),
[ModelX],
transform=lambda o: o.__class__,
)
self.assertQuerysetEqual(
Base.objects.db_manager("secondary").filter(instance_of=ModelY),
[ModelY],
transform=lambda o: o.__class__,
)
self.assertQuerysetEqual(
Base.objects.db_manager("secondary").filter(
Q(instance_of=ModelX) | Q(instance_of=ModelY)
),
[ModelX, ModelY],
transform=lambda o: o.__class__,
ordered=False,
)
def test_forward_many_to_one_descriptor_on_non_default_database(self):
def func():
blog = BlogA.objects.db_manager("secondary").create(
name="Blog", info="Info"
)
entry = BlogEntry.objects.db_manager("secondary").create(
blog=blog, text="Text"
)
ContentType.objects.clear_cache()
entry = BlogEntry.objects.db_manager("secondary").get(pk=entry.id)
self.assertEqual(blog, entry.blog)
# Ensure no queries are made using the default database.
self.assertNumQueries(0, func)
def test_reverse_many_to_one_descriptor_on_non_default_database(self):
def func():
blog = BlogA.objects.db_manager("secondary").create(
name="Blog", info="Info"
)
entry = BlogEntry.objects.db_manager("secondary").create(
blog=blog, text="Text"
)
ContentType.objects.clear_cache()
blog = BlogA.objects.db_manager("secondary").get(pk=blog.id)
self.assertEqual(entry, blog.blogentry_set.using("secondary").get())
# Ensure no queries are made using the default database.
self.assertNumQueries(0, func)
def test_reverse_one_to_one_descriptor_on_non_default_database(self):
def func():
m2a = Model2A.objects.db_manager("secondary").create(field1="A1")
one2one = One2OneRelatingModel.objects.db_manager("secondary").create(
one2one=m2a, field1="121"
)
ContentType.objects.clear_cache()
m2a = Model2A.objects.db_manager("secondary").get(pk=m2a.id)
self.assertEqual(one2one, m2a.one2onerelatingmodel)
# Ensure no queries are made using the default database.
self.assertNumQueries(0, func)
def test_many_to_many_descriptor_on_non_default_database(self):
def func():
m2a = Model2A.objects.db_manager("secondary").create(field1="A1")
rm = RelatingModel.objects.db_manager("secondary").create()
rm.many2many.add(m2a)
ContentType.objects.clear_cache()
m2a = Model2A.objects.db_manager("secondary").get(pk=m2a.id)
self.assertEqual(rm, m2a.relatingmodel_set.using("secondary").get())
# Ensure no queries are made using the default database.
self.assertNumQueries(0, func)

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More