1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
|
"""
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",
f"admin/{app_label}/change_form.html",
# Added:
f"admin/{base_app_label}/{base_opts.object_name.lower()}/change_form.html",
f"admin/{base_app_label}/change_form.html",
"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 [
f"admin/{app_label}/{opts.object_name.lower()}/delete_confirmation.html",
f"admin/{app_label}/delete_confirmation.html",
# Added:
f"admin/{base_app_label}/{base_opts.object_name.lower()}/delete_confirmation.html",
f"admin/{base_app_label}/delete_confirmation.html",
"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",
f"admin/{app_label}/object_history.html",
# Added:
f"admin/{base_app_label}/{base_opts.object_name.lower()}/object_history.html",
f"admin/{base_app_label}/object_history.html",
"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"]:
# multiple elements in single line
if isinstance(field, tuple):
for line_field in field:
try:
subclass_fields.remove(line_field)
except ValueError:
pass # field not found in form, Django will raise exception later.
else:
# regular one-element-per-line
try:
subclass_fields.remove(field)
except ValueError:
pass # field not found in form, Django will raise exception later.
return subclass_fields
|