Initial Commit
This commit is contained in:
@@ -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",
|
||||
]
|
||||
Reference in New Issue
Block a user