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 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
|
"""
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=(
f"admin/js/vendor/jquery/{'jquery' if settings.DEBUG else 'jquery.min'}.js",
"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"
|