Merge branch 'feature/inlines'
This commit is contained in:
@@ -4,12 +4,43 @@ 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
|
||||
from .childadmin import PolymorphicChildModelAdmin
|
||||
|
||||
# Utils
|
||||
from .forms import PolymorphicModelChoiceForm
|
||||
from .filters import PolymorphicChildModelFilter
|
||||
|
||||
__all__ = (
|
||||
'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin',
|
||||
'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter'
|
||||
# Inlines
|
||||
from .inlines import (
|
||||
PolymorphicInlineModelAdmin, # base class
|
||||
StackedPolymorphicInline, # stacked inline
|
||||
)
|
||||
|
||||
# Helpers for the inlines
|
||||
from .helpers import (
|
||||
PolymorphicInlineAdminForm,
|
||||
PolymorphicInlineAdminFormSet,
|
||||
PolymorphicInlineSupportMixin, # mixin for the regular model admin!
|
||||
)
|
||||
|
||||
# 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
|
||||
GenericStackedPolymorphicInline, # stacked inline
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'PolymorphicParentModelAdmin',
|
||||
'PolymorphicChildModelAdmin',
|
||||
'PolymorphicModelChoiceForm',
|
||||
'PolymorphicChildModelFilter',
|
||||
'PolymorphicInlineAdminForm',
|
||||
'PolymorphicInlineAdminFormSet',
|
||||
'PolymorphicInlineSupportMixin',
|
||||
'PolymorphicInlineModelAdmin',
|
||||
'GenericPolymorphicInlineModelAdmin',
|
||||
'GenericStackedPolymorphicInline',
|
||||
)
|
||||
|
||||
@@ -6,6 +6,8 @@ from django.core.urlresolvers import resolve
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .helpers import PolymorphicInlineSupportMixin
|
||||
|
||||
|
||||
class ParentAdminNotRegistered(RuntimeError):
|
||||
"The admin site for the model is not registered."
|
||||
|
||||
@@ -7,6 +7,12 @@ 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'
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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 polymorphic_child_forms_factory, BaseGenericPolymorphicInlineFormSet, GenericPolymorphicFormSetChild
|
||||
from .inlines import PolymorphicInlineModelAdmin
|
||||
|
||||
|
||||
class GenericPolymorphicInlineModelAdmin(PolymorphicInlineModelAdmin, GenericInlineModelAdmin):
|
||||
"""
|
||||
Base class for variation of inlines based on generic foreign keys.
|
||||
"""
|
||||
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)
|
||||
|
||||
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.
|
||||
"""
|
||||
template = 'admin/polymorphic/edit_inline/stacked.html'
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Rendering utils for admin forms;
|
||||
|
||||
This makes sure that admin fieldsets/layout settings are exported to the template.
|
||||
"""
|
||||
import json
|
||||
|
||||
import django
|
||||
from django.contrib.admin.helpers import InlineAdminFormSet, InlineAdminForm, AdminField
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from polymorphic.formsets import BasePolymorphicModelFormSet
|
||||
|
||||
|
||||
class PolymorphicInlineAdminForm(InlineAdminForm):
|
||||
"""
|
||||
Expose the admin configuration for a form
|
||||
"""
|
||||
|
||||
def polymorphic_ctype_field(self):
|
||||
return AdminField(self.form, 'polymorphic_ctype', False)
|
||||
|
||||
|
||||
class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
|
||||
"""
|
||||
Internally used class to expose the formset in the template.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request', None) # Assigned later via PolymorphicInlineSupportMixin later.
|
||||
self.obj = kwargs.pop('obj', None)
|
||||
super(PolymorphicInlineAdminFormSet, self).__init__(*args, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Output all forms using the proper subtype settings.
|
||||
"""
|
||||
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
|
||||
# Output the form
|
||||
model = original.get_real_concrete_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
|
||||
|
||||
# The polymorphic template follows the same method like all other inlines do in Django 1.10.
|
||||
# This method is added for compatibility with older Django versions.
|
||||
def inline_formset_data(self):
|
||||
"""
|
||||
A JavaScript data structure for the JavaScript code
|
||||
"""
|
||||
verbose_name = self.opts.verbose_name
|
||||
return json.dumps({
|
||||
'name': '#%s' % self.formset.prefix,
|
||||
'options': {
|
||||
'prefix': self.formset.prefix,
|
||||
'addText': ugettext('Add another %(verbose_name)s') % {
|
||||
'verbose_name': capfirst(verbose_name),
|
||||
},
|
||||
'childTypes': [
|
||||
{
|
||||
'type': model._meta.model_name,
|
||||
'name': force_text(model._meta.verbose_name)
|
||||
} for model in self.formset.child_forms.keys()
|
||||
],
|
||||
'deleteText': ugettext('Remove'),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class PolymorphicInlineSupportMixin(object):
|
||||
"""
|
||||
A Mixin to add to the regular admin, so it can work with our polymorphic inlines.
|
||||
|
||||
This mixin needs to be included in the admin that hosts the ``inlines``.
|
||||
It makes sure the generated admin forms have different fieldsets/fields
|
||||
depending on the polymorphic type of the form instance.
|
||||
|
||||
This is achieved by overwriting :func:`get_inline_formsets` to return
|
||||
an :class:`PolymorphicInlineAdminFormSet` instead of a standard Django
|
||||
:class:`~django.contrib.admin.helpers.InlineAdminFormSet` for the polymorphic formsets.
|
||||
"""
|
||||
|
||||
def get_inline_formsets(self, request, formsets, inline_instances, obj=None):
|
||||
"""
|
||||
Overwritten version to produce the proper admin wrapping for the
|
||||
polymorphic inline formset. This fixes the media and form appearance
|
||||
of the inline polymorphic models.
|
||||
"""
|
||||
inline_admin_formsets = super(PolymorphicInlineSupportMixin, self).get_inline_formsets(
|
||||
request, formsets, inline_instances, obj=obj)
|
||||
|
||||
for admin_formset in inline_admin_formsets:
|
||||
if isinstance(admin_formset.formset, BasePolymorphicModelFormSet):
|
||||
# This is a polymorphic formset, which belongs to our inline.
|
||||
# Downcast the admin wrapper that generates the form fields.
|
||||
admin_formset.__class__ = PolymorphicInlineAdminFormSet
|
||||
admin_formset.request = request
|
||||
admin_formset.obj = obj
|
||||
return inline_admin_formsets
|
||||
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Django Admin support for polymorphic inlines.
|
||||
|
||||
Each row in the inline can correspond with a different subclass.
|
||||
"""
|
||||
from functools import partial
|
||||
|
||||
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 polymorphic_child_forms_factory, BasePolymorphicInlineFormSet, PolymorphicFormSetChild
|
||||
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=(
|
||||
'polymorphic/js/polymorphic_inlines.js',
|
||||
),
|
||||
css={
|
||||
'all': (
|
||||
'polymorphic/css/polymorphic_inlines.css',
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
#: The extra forms to show
|
||||
#: By default there are no 'extra' forms as the desired type is unknown.
|
||||
#: Instead, add each new item using JavaScript that first offers a type-selection.
|
||||
extra = 0
|
||||
|
||||
#: Inlines for all model sub types that can be displayed in this inline.
|
||||
#: Each row is a :class:`PolymorphicInlineModelAdmin.Child`
|
||||
child_inlines = ()
|
||||
|
||||
def __init__(self, parent_model, admin_site):
|
||||
super(PolymorphicInlineModelAdmin, self).__init__(parent_model, admin_site)
|
||||
|
||||
# Extra check to avoid confusion
|
||||
# While we could monkeypatch the admin here, better stay explicit.
|
||||
parent_admin = admin_site._registry.get(parent_model, None)
|
||||
if parent_admin is not None: # Can be None during check
|
||||
if not isinstance(parent_admin, PolymorphicInlineSupportMixin):
|
||||
raise ImproperlyConfigured(
|
||||
"To use polymorphic inlines, add the `PolymorphicInlineSupportMixin` mixin "
|
||||
"to the ModelAdmin that hosts the inline."
|
||||
)
|
||||
|
||||
# While the inline is created per request, the 'request' object is not known here.
|
||||
# Hence, creating all child inlines unconditionally, without checking permissions.
|
||||
self.child_inline_instances = self.get_child_inline_instances()
|
||||
|
||||
# Create a lookup table
|
||||
self._child_inlines_lookup = {}
|
||||
for child_inline in self.child_inline_instances:
|
||||
self._child_inlines_lookup[child_inline.model] = child_inline
|
||||
|
||||
def get_child_inline_instances(self):
|
||||
"""
|
||||
:rtype List[PolymorphicInlineModelAdmin.Child]
|
||||
"""
|
||||
instances = []
|
||||
for ChildInlineType in self.child_inlines:
|
||||
instances.append(ChildInlineType(parent_inline=self))
|
||||
return instances
|
||||
|
||||
def get_child_inline_instance(self, model):
|
||||
"""
|
||||
Find the child inline for a given model.
|
||||
|
||||
:rtype: PolymorphicInlineModelAdmin.Child
|
||||
"""
|
||||
try:
|
||||
return self._child_inlines_lookup[model]
|
||||
except KeyError:
|
||||
raise ValueError("Model '{0}' not found in child_inlines".format(model.__name__))
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
"""
|
||||
Construct the inline formset class.
|
||||
|
||||
This passes all class attributes to the formset.
|
||||
|
||||
:rtype: type
|
||||
"""
|
||||
# Construct the FormSet class
|
||||
FormSet = super(PolymorphicInlineModelAdmin, self).get_formset(request, obj=obj, **kwargs)
|
||||
|
||||
# Instead of completely redefining super().get_formset(), we use
|
||||
# the regular inlineformset_factory(), and amend that with our extra bits.
|
||||
# This code line is the essence of what polymorphic_inlineformset_factory() does.
|
||||
FormSet.child_forms = polymorphic_child_forms_factory(
|
||||
formset_children=self.get_formset_children(request, obj=obj)
|
||||
)
|
||||
return FormSet
|
||||
|
||||
def get_formset_children(self, request, obj=None):
|
||||
"""
|
||||
The formset 'children' provide the details for all child models that are part of this formset.
|
||||
It provides a stripped version of the modelform/formset factory methods.
|
||||
"""
|
||||
formset_children = []
|
||||
for child_inline in self.child_inline_instances:
|
||||
# TODO: the children can be limited here per request based on permissions.
|
||||
formset_children.append(child_inline.get_formset_child(request, obj=obj))
|
||||
return formset_children
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
"""
|
||||
Hook for specifying fieldsets.
|
||||
"""
|
||||
if self.fieldsets:
|
||||
return self.fieldsets
|
||||
else:
|
||||
return [] # Avoid exposing fields to the child
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
if self.fields:
|
||||
return self.fields
|
||||
else:
|
||||
return [] # Avoid exposing fields to the child
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
# The media of the inline focuses on the admin settings,
|
||||
# whether to expose the scripts for filter_horizontal etc..
|
||||
# The admin helper exposes the inline + formset media.
|
||||
base_media = super(PolymorphicInlineModelAdmin, self).media
|
||||
all_media = Media()
|
||||
add_media(all_media, base_media)
|
||||
|
||||
# Add all media of the child inline instances
|
||||
for child_instance in self.child_inline_instances:
|
||||
child_media = child_instance.media
|
||||
|
||||
# Avoid adding the same media object again and again
|
||||
if child_media._css != base_media._css and child_media._js != base_media._js:
|
||||
add_media(all_media, child_media)
|
||||
|
||||
add_media(all_media, self.polymorphic_media)
|
||||
|
||||
return all_media
|
||||
|
||||
class Child(InlineModelAdmin):
|
||||
"""
|
||||
The child inline; which allows configuring the admin options
|
||||
for the child appearance.
|
||||
|
||||
Note that not all options will be honored by the parent, notably the formset options:
|
||||
* :attr:`extra`
|
||||
* :attr:`min_num`
|
||||
* :attr:`max_num`
|
||||
|
||||
The model form options however, will all be read.
|
||||
"""
|
||||
formset_child = PolymorphicFormSetChild
|
||||
extra = 0 # TODO: currently unused for the children.
|
||||
|
||||
def __init__(self, parent_inline):
|
||||
self.parent_inline = parent_inline
|
||||
super(PolymorphicInlineModelAdmin.Child, self).__init__(parent_inline.parent_model, parent_inline.admin_site)
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
# The child inline is only used to construct the form,
|
||||
# and allow to override the form field attributes.
|
||||
# The formset is created by the parent inline.
|
||||
raise RuntimeError("The child get_formset() is not used.")
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
if self.fields:
|
||||
return self.fields
|
||||
|
||||
# Standard Django logic, use the form to determine the fields.
|
||||
# The form needs to pass through all factory logic so all 'excludes' are set as well.
|
||||
# Default Django does: form = self.get_formset(request, obj, fields=None).form
|
||||
# Use 'fields=None' avoids recursion in the field autodetection.
|
||||
form = self.get_formset_child(request, obj, fields=None).get_form()
|
||||
return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
|
||||
|
||||
def get_formset_child(self, request, obj=None, **kwargs):
|
||||
"""
|
||||
Return the formset child that the parent inline can use to represent us.
|
||||
|
||||
:rtype: PolymorphicFormSetChild
|
||||
"""
|
||||
# Similar to the normal get_formset(), the caller may pass fields to override the defaults settings
|
||||
# in the inline. In Django's GenericInlineModelAdmin.get_formset() this is also used in the same way,
|
||||
# to make sure the 'exclude' also contains the GFK fields.
|
||||
#
|
||||
# Hence this code is almost identical to InlineModelAdmin.get_formset()
|
||||
# and GenericInlineModelAdmin.get_formset()
|
||||
#
|
||||
# Transfer the local inline attributes to the formset child,
|
||||
# this allows overriding settings.
|
||||
if 'fields' in kwargs:
|
||||
fields = kwargs.pop('fields')
|
||||
else:
|
||||
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
|
||||
|
||||
if self.exclude is None:
|
||||
exclude = []
|
||||
else:
|
||||
exclude = list(self.exclude)
|
||||
|
||||
exclude.extend(self.get_readonly_fields(request, obj))
|
||||
|
||||
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.
|
||||
"""
|
||||
template = 'admin/polymorphic/edit_inline/stacked.html'
|
||||
Reference in New Issue
Block a user