Initial Commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
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
|
||||
@@ -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",
|
||||
)
|
||||
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
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 gettext_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().__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().get_form(request, obj, **kwargs)
|
||||
|
||||
def get_model_perms(self, request):
|
||||
match = resolve(request.path_info)
|
||||
|
||||
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().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 [
|
||||
f"admin/{app_label}/{opts.object_name.lower()}/change_form.html",
|
||||
"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 [
|
||||
f"admin/{app_label}/{opts.object_name.lower()}/object_history.html",
|
||||
"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()
|
||||
|
||||
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(
|
||||
f"No parent admin was registered for a '{parent_model}' 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().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().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().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().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
|
||||
@@ -0,0 +1,37 @@
|
||||
from django.contrib import admin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.translation import gettext_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 "{}". It must be registered as child model.'.format(value)
|
||||
)
|
||||
return queryset
|
||||
@@ -0,0 +1,21 @@
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import AdminRadioSelect
|
||||
from django.utils.translation import gettext_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().__init__(*args, **kwargs)
|
||||
self.fields["ct_id"].label = self.type_label
|
||||
@@ -0,0 +1,70 @@
|
||||
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"
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
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_str
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext
|
||||
|
||||
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().__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": gettext("Add another %(verbose_name)s")
|
||||
% {"verbose_name": capfirst(verbose_name)},
|
||||
"childTypes": [
|
||||
{
|
||||
"type": model._meta.model_name,
|
||||
"name": force_str(model._meta.verbose_name),
|
||||
}
|
||||
for model in self.formset.child_forms.keys()
|
||||
],
|
||||
"deleteText": gettext("Remove"),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class PolymorphicInlineSupportMixin:
|
||||
"""
|
||||
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().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
|
||||
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
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().__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(f"Model '{model.__name__}' not found in child_inlines")
|
||||
|
||||
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().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().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"
|
||||
@@ -0,0 +1,378 @@
|
||||
"""
|
||||
The parent admin displays the list view of the base model.
|
||||
"""
|
||||
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.urls import URLResolver
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from polymorphic.utils import get_base_polymorphic_model
|
||||
|
||||
from .forms import PolymorphicModelChoiceForm
|
||||
|
||||
|
||||
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().__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(
|
||||
"{} should be a subclass of {}".format(model.__name__, self.base_model.__name__)
|
||||
)
|
||||
if not issubclass(model_admin, admin.ModelAdmin):
|
||||
raise TypeError(
|
||||
"{} should be a subclass of {}".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 = []
|
||||
content_types = ContentType.objects.get_for_models(
|
||||
*self.get_child_models(), for_concrete_models=False
|
||||
)
|
||||
|
||||
for model, ct in content_types.items():
|
||||
perm_function_name = f"has_{action}_permission"
|
||||
model_admin = self._get_real_admin_by_model(model)
|
||||
perm_function = getattr(model_admin, perm_function_name)
|
||||
if not perm_function(request):
|
||||
continue
|
||||
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 '{}.{}'.".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 '{}', 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 '{}' model.".format(model_class)
|
||||
)
|
||||
|
||||
if super_if_self and real_admin is self:
|
||||
return super()
|
||||
else:
|
||||
return real_admin
|
||||
|
||||
def get_queryset(self, request):
|
||||
# optimize the list display.
|
||||
qs = super().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().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().get_preserved_filters(request)
|
||||
|
||||
def get_urls(self):
|
||||
"""
|
||||
Expose the custom URLs for the subclasses and the URL resolver.
|
||||
"""
|
||||
urls = super().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 = int(path)
|
||||
else:
|
||||
object_id = int(path[0:pos])
|
||||
except ValueError:
|
||||
raise Http404(
|
||||
"No ct_id parameter, unable to find admin subclass for path '{}'.".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(f"No match for path '{path}' in admin subclass.")
|
||||
|
||||
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_str() to decode it as string.
|
||||
# See QueryDict how Django deals with that.
|
||||
extra_qs = "&{}".format(force_str(request.META["QUERY_STRING"]))
|
||||
|
||||
choices = self.get_child_type_choices(request, "add")
|
||||
if len(choices) == 0:
|
||||
raise PermissionDenied
|
||||
if len(choices) == 1:
|
||||
return HttpResponseRedirect(f"?ct_id={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={}{}".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_str(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 [
|
||||
f"admin/{app_label}/{opts.object_name.lower()}/add_type_form.html",
|
||||
"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 [
|
||||
f"admin/{app_label}/{opts.object_name.lower()}/change_list.html",
|
||||
"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",
|
||||
]
|
||||
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
PolymorphicModel Meta Class
|
||||
"""
|
||||
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, **kwargs):
|
||||
# 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().__new__(self, model_name, bases, attrs, **kwargs)
|
||||
|
||||
# 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, **kwargs)
|
||||
|
||||
# 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, **kwargs):
|
||||
"""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().__new__(self, model_name, bases, attrs, **kwargs)
|
||||
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: "{}.{}" 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 {}.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()._default_manager
|
||||
if not isinstance(manager, PolymorphicManager):
|
||||
warnings.warn(
|
||||
"{}._default_manager is not a PolymorphicManager".format(self.__class__.__name__),
|
||||
ManagerInheritanceWarning,
|
||||
)
|
||||
|
||||
return manager
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Compatibility with Python 2 (taken from 'django.utils.six')"""
|
||||
|
||||
|
||||
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", (), {})
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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().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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
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().__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().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
|
||||
@@ -0,0 +1,447 @@
|
||||
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:
|
||||
"""
|
||||
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,
|
||||
"widgets": self.widgets,
|
||||
# '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().__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 = f"{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[f"{prefix}-polymorphic_ctype"])
|
||||
except (KeyError, ValueError):
|
||||
raise ValidationError(
|
||||
"Formset row {} 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(
|
||||
f"Child model type {model} is not part of the formset"
|
||||
)
|
||||
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.TypedChoiceField(
|
||||
choices=choices,
|
||||
initial=ct.pk,
|
||||
required=False,
|
||||
widget=forms.HiddenInput,
|
||||
coerce=int,
|
||||
)
|
||||
super().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(f"No 'child_forms' defined in {self.__class__.__name__}")
|
||||
if not issubclass(model, PolymorphicModel):
|
||||
raise TypeError(f"Expect polymorphic model type, not {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 '{}' found a '{}' 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()._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
|
||||
@@ -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)
|
||||
Binary file not shown.
@@ -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 ""
|
||||
Binary file not shown.
@@ -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"
|
||||
Binary file not shown.
@@ -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"
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
The manager class for use in the models.
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
from polymorphic.query import PolymorphicQuerySet
|
||||
|
||||
__all__ = ("PolymorphicManager", "PolymorphicQuerySet")
|
||||
|
||||
|
||||
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().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 "{} (PolymorphicManager) using {}".format(
|
||||
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)
|
||||
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
Seamless Polymorphic Inheritance for Django Models
|
||||
"""
|
||||
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().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 {} for {} #{} 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().__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):
|
||||
objects = getattr(model, "_base_objects", model.objects)
|
||||
attr = 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
|
||||
@@ -0,0 +1,537 @@
|
||||
"""
|
||||
QuerySet for PolymorphicModel
|
||||
"""
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
from django import get_version as get_django_version
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db.models import FilteredRelation
|
||||
from django.db.models.query import ModelIterable, Q, QuerySet
|
||||
|
||||
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().__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().__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()._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, ignore_conflicts=False):
|
||||
objs = list(objs)
|
||||
for obj in objs:
|
||||
obj.pre_save_polymorphic()
|
||||
return super().bulk_create(objs, batch_size, ignore_conflicts=ignore_conflicts)
|
||||
|
||||
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)
|
||||
|
||||
# Makes _filter_or_exclude compatible with the change in signature introduced in django at 9c9a3fe
|
||||
if get_django_version() >= "3.2":
|
||||
|
||||
def _filter_or_exclude(self, negate, args, kwargs):
|
||||
# We override this internal Django function as it is used for all filter member functions.
|
||||
q_objects = translate_polymorphic_filter_definitions_in_args(
|
||||
queryset_model=self.model, args=args, using=self.db
|
||||
)
|
||||
# filter_field='data'
|
||||
additional_args = translate_polymorphic_filter_definitions_in_kwargs(
|
||||
queryset_model=self.model, kwargs=kwargs, using=self.db
|
||||
)
|
||||
args = list(q_objects) + additional_args
|
||||
return super()._filter_or_exclude(negate=negate, args=args, kwargs=kwargs)
|
||||
|
||||
else:
|
||||
|
||||
def _filter_or_exclude(self, negate, *args, **kwargs):
|
||||
# We override this internal Django function 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()._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, str)
|
||||
else a # allow expressions to pass unchanged
|
||||
for a in field_names
|
||||
]
|
||||
return super().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().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().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 isinstance(a, FilteredRelation):
|
||||
patch_lookup(a.condition)
|
||||
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().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()._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().__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
|
||||
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
PolymorphicQuerySet support functions
|
||||
"""
|
||||
import copy
|
||||
from collections import deque
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldDoesNotExist, 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 isinstance(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 isinstance(child, (tuple, list)):
|
||||
# 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, str):
|
||||
raise ValueError(f"Expected field name as string: {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 {} (in app {}) not found!".format(
|
||||
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 FieldDoesNotExist:
|
||||
pass
|
||||
|
||||
submodels = _get_all_sub_models(queryset_model)
|
||||
model = submodels.get(classname, None)
|
||||
assert model, "PolymorphicModel: model {} not found (not a subclass of {})!".format(
|
||||
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
|
||||
@@ -0,0 +1,174 @@
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
|
||||
RE_DEFERRED = re.compile("_Deferred_.*")
|
||||
|
||||
|
||||
class ShowFieldBase:
|
||||
"""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, int):
|
||||
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[{}]".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
|
||||
+34
@@ -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;
|
||||
}
|
||||
+338
@@ -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);
|
||||
+11
@@ -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 %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "admin/change_form.html" %}
|
||||
{% load polymorphic_admin_tags %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
|
||||
{% endblock %}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{% extends "admin/delete_confirmation.html" %}
|
||||
{% load polymorphic_admin_tags %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
|
||||
{% endblock %}
|
||||
+36
@@ -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> <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>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{% extends "admin/object_history.html" %}
|
||||
{% load polymorphic_admin_tags %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,48 @@
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
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(f"{token.contents[0]} tag expects 1 argument")
|
||||
|
||||
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, str):
|
||||
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)
|
||||
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
|
||||
from django.template import Library
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext
|
||||
|
||||
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)
|
||||
"""
|
||||
yield from formset
|
||||
|
||||
if hasattr(formset, "empty_forms"):
|
||||
# BasePolymorphicModelFormSet
|
||||
yield from formset.empty_forms
|
||||
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 gettext("Add another %(verbose_name)s") % {"verbose_name": capfirst(verbose_name)},
|
||||
"showAddButton": getattr(formset, "show_add_button", True),
|
||||
"deleteText": gettext("Delete"),
|
||||
}
|
||||
|
||||
if isinstance(formset, BasePolymorphicModelFormSet):
|
||||
# Allow to add different types
|
||||
options["childTypes"] = [
|
||||
{
|
||||
"name": force_str(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
|
||||
@@ -0,0 +1,76 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
|
||||
from polymorphic.base import PolymorphicModelBase
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
|
||||
def reset_polymorphic_ctype(*models, **filters):
|
||||
"""
|
||||
Set the polymorphic content-type ID field to the proper model
|
||||
Sort the ``*models`` from base class to descending class,
|
||||
to make sure the content types are properly assigned.
|
||||
|
||||
Add ``ignore_existing=True`` to skip models which already
|
||||
have a polymorphic content type.
|
||||
"""
|
||||
using = filters.pop("using", DEFAULT_DB_ALIAS)
|
||||
ignore_existing = filters.pop("ignore_existing", False)
|
||||
|
||||
models = sort_by_subclass(*models)
|
||||
if ignore_existing:
|
||||
# When excluding models, make sure we don't ignore the models we
|
||||
# just assigned the an content type to. hence, start with child first.
|
||||
models = reversed(models)
|
||||
|
||||
for new_model in models:
|
||||
new_ct = ContentType.objects.db_manager(using).get_for_model(
|
||||
new_model, for_concrete_model=False
|
||||
)
|
||||
|
||||
qs = new_model.objects.db_manager(using)
|
||||
if ignore_existing:
|
||||
qs = qs.filter(polymorphic_ctype__isnull=True)
|
||||
if filters:
|
||||
qs = qs.filter(**filters)
|
||||
qs.update(polymorphic_ctype=new_ct)
|
||||
|
||||
|
||||
def _compare_mro(cls1, cls2):
|
||||
if cls1 is cls2:
|
||||
return 0
|
||||
|
||||
try:
|
||||
index1 = cls1.mro().index(cls2)
|
||||
except ValueError:
|
||||
return -1 # cls2 not inherited by 1
|
||||
|
||||
try:
|
||||
index2 = cls2.mro().index(cls1)
|
||||
except ValueError:
|
||||
return 1 # cls1 not inherited by 2
|
||||
|
||||
return (index1 > index2) - (index1 < index2) # python 3 compatible cmp.
|
||||
|
||||
|
||||
def sort_by_subclass(*classes):
|
||||
"""
|
||||
Sort a series of models by their inheritance order.
|
||||
"""
|
||||
from functools import cmp_to_key
|
||||
|
||||
return sorted(classes, key=cmp_to_key(_compare_mro))
|
||||
|
||||
|
||||
def get_base_polymorphic_model(ChildModel, allow_abstract=False):
|
||||
"""
|
||||
First the first concrete model in the inheritance chain that inherited from the PolymorphicModel.
|
||||
"""
|
||||
for Model in reversed(ChildModel.mro()):
|
||||
if (
|
||||
isinstance(Model, PolymorphicModelBase)
|
||||
and Model is not PolymorphicModel
|
||||
and (allow_abstract or not Model._meta.abstract)
|
||||
):
|
||||
return Model
|
||||
return None
|
||||
Reference in New Issue
Block a user