Upstream support for polymorphic formsets and and inline models.
Originally written in django-fluent-contents to support polymorphic generic inlines;
72d816b8bb
fix_request_path_info
parent
2a599b5f99
commit
a07ce7260c
|
|
@ -4,12 +4,44 @@ ModelAdmin code to display polymorphic models.
|
||||||
The admin consists of a parent admin (which shows in the admin with a list),
|
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).
|
and a child admin (which is used internally to show the edit/delete dialog).
|
||||||
"""
|
"""
|
||||||
|
# Admins for the regular models
|
||||||
from .parentadmin import PolymorphicParentModelAdmin
|
from .parentadmin import PolymorphicParentModelAdmin
|
||||||
from .childadmin import PolymorphicChildModelAdmin
|
from .childadmin import PolymorphicChildModelAdmin
|
||||||
|
|
||||||
|
# Utils
|
||||||
from .forms import PolymorphicModelChoiceForm
|
from .forms import PolymorphicModelChoiceForm
|
||||||
from .filters import PolymorphicChildModelFilter
|
from .filters import PolymorphicChildModelFilter
|
||||||
|
|
||||||
__all__ = (
|
# Inlines
|
||||||
'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin',
|
from .inlines import (
|
||||||
'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter'
|
PolymorphicParentInlineModelAdmin,
|
||||||
|
PolymorphicChildInlineModelAdmin,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Helpers for the inlines
|
||||||
|
from .helpers import (
|
||||||
|
InlinePolymorphicAdminForm,
|
||||||
|
InlinePolymorphicAdminFormSet,
|
||||||
|
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 (
|
||||||
|
PolymorphicParentGenericInlineModelAdmin,
|
||||||
|
PolymorphicChildGenericInlineModelAdmin,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'PolymorphicParentModelAdmin',
|
||||||
|
'PolymorphicChildModelAdmin',
|
||||||
|
'PolymorphicModelChoiceForm',
|
||||||
|
'PolymorphicChildModelFilter',
|
||||||
|
'InlinePolymorphicAdminForm',
|
||||||
|
'InlinePolymorphicAdminFormSet',
|
||||||
|
'PolymorphicInlineSupportMixin',
|
||||||
|
'PolymorphicParentInlineModelAdmin',
|
||||||
|
'PolymorphicChildInlineModelAdmin',
|
||||||
|
'PolymorphicParentGenericInlineModelAdmin',
|
||||||
|
'PolymorphicChildGenericInlineModelAdmin',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
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, BasePolymorphicGenericInlineFormSet, PolymorphicGenericFormSetChild
|
||||||
|
from .inlines import PolymorphicParentInlineModelAdmin, PolymorphicChildInlineModelAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicParentGenericInlineModelAdmin(PolymorphicParentInlineModelAdmin, GenericInlineModelAdmin):
|
||||||
|
"""
|
||||||
|
Variation for inlines based on generic foreign keys.
|
||||||
|
"""
|
||||||
|
formset = BasePolymorphicGenericInlineFormSet
|
||||||
|
|
||||||
|
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 PolymorphicChildGenericInlineModelAdmin(PolymorphicChildInlineModelAdmin):
|
||||||
|
"""
|
||||||
|
Variation for generic inlines.
|
||||||
|
"""
|
||||||
|
# Make sure that the GFK fields are excluded from the child forms
|
||||||
|
formset_child = PolymorphicGenericFormSetChild
|
||||||
|
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(PolymorphicChildGenericInlineModelAdmin, self).get_formset_child(request, obj=obj, **defaults)
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""
|
||||||
|
Rendering utils for admin forms;
|
||||||
|
|
||||||
|
This makes sure that admin fieldsets/layout settings are exported to the template.
|
||||||
|
"""
|
||||||
|
from django.contrib.admin.helpers import InlineAdminFormSet, InlineAdminForm
|
||||||
|
|
||||||
|
from ..formsets import BasePolymorphicModelFormSet
|
||||||
|
|
||||||
|
|
||||||
|
class InlinePolymorphicAdminForm(InlineAdminForm):
|
||||||
|
"""
|
||||||
|
Expose the admin configuration for a form
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InlinePolymorphicAdminFormSet(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(InlinePolymorphicAdminFormSet, 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 InlinePolymorphicAdminForm(
|
||||||
|
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 InlinePolymorphicAdminForm(
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicInlineSupportMixin(object):
|
||||||
|
"""
|
||||||
|
A Mixin to add to the regular admin, so it can work with our polymorphic inlines.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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):
|
||||||
|
# Downcast the admin
|
||||||
|
admin_formset.__class__ = InlinePolymorphicAdminFormSet
|
||||||
|
admin_formset.request = request
|
||||||
|
admin_formset.obj = obj
|
||||||
|
return inline_admin_formsets
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
"""
|
||||||
|
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.forms import Media
|
||||||
|
|
||||||
|
from polymorphic.formsets import polymorphic_child_forms_factory, BasePolymorphicInlineFormSet, PolymorphicFormSetChild
|
||||||
|
from polymorphic.formsets.utils import add_media
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicParentInlineModelAdmin(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.
|
||||||
|
* Child formset media is not yet processed.
|
||||||
|
"""
|
||||||
|
formset = BasePolymorphicInlineFormSet
|
||||||
|
|
||||||
|
#: 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:`PolymorphicChildInlineModelAdmin`
|
||||||
|
child_inlines = ()
|
||||||
|
|
||||||
|
def __init__(self, parent_model, admin_site):
|
||||||
|
super(PolymorphicParentInlineModelAdmin, self).__init__(parent_model, admin_site)
|
||||||
|
|
||||||
|
# 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[PolymorphicChildInlineModelAdmin]
|
||||||
|
"""
|
||||||
|
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: PolymorphicChildInlineModelAdmin
|
||||||
|
"""
|
||||||
|
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(PolymorphicParentInlineModelAdmin, 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 is identical to 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(PolymorphicParentInlineModelAdmin, 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)
|
||||||
|
|
||||||
|
return all_media
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicChildInlineModelAdmin(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(PolymorphicChildInlineModelAdmin, 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)
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""
|
||||||
|
Polymorphic formsets support.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
See:
|
||||||
|
* :func:`polymorphic_inlineformset_factory`
|
||||||
|
* :class:`PolymorphicFormSetChild`
|
||||||
|
|
||||||
|
The internal machinery can be used to extend the formset classes. This includes:
|
||||||
|
* :class:`BasePolymorphicModelFormSet`
|
||||||
|
* :class:`BasePolymorphicInlineFormSet`
|
||||||
|
* :func:`polymorphic_child_forms_factory`
|
||||||
|
|
||||||
|
For generic relations, a similar set is available:
|
||||||
|
* :class:`BasePolymorphicGenericInlineFormSet`
|
||||||
|
* :class:`PolymorphicGenericFormSetChild`
|
||||||
|
* :func:`polymorphic_generic_inlineformset_factory`
|
||||||
|
|
||||||
|
"""
|
||||||
|
from .models import (
|
||||||
|
BasePolymorphicModelFormSet,
|
||||||
|
BasePolymorphicInlineFormSet,
|
||||||
|
PolymorphicFormSetChild,
|
||||||
|
polymorphic_inlineformset_factory,
|
||||||
|
polymorphic_child_forms_factory,
|
||||||
|
)
|
||||||
|
from .generic import (
|
||||||
|
# Can import generic here, as polymorphic already depends on the 'contenttypes' app.
|
||||||
|
BasePolymorphicGenericInlineFormSet,
|
||||||
|
PolymorphicGenericFormSetChild,
|
||||||
|
polymorphic_generic_inlineformset_factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'BasePolymorphicModelFormSet',
|
||||||
|
'BasePolymorphicInlineFormSet',
|
||||||
|
'BasePolymorphicGenericInlineFormSet',
|
||||||
|
'PolymorphicFormSetChild',
|
||||||
|
'PolymorphicGenericFormSetChild',
|
||||||
|
'polymorphic_inlineformset_factory',
|
||||||
|
'polymorphic_generic_inlineformset_factory',
|
||||||
|
'polymorphic_child_forms_factory',
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import django
|
||||||
|
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, polymorphic_child_forms_factory, PolymorphicFormSetChild
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicGenericFormSetChild(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(PolymorphicGenericFormSetChild, self).__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 django.VERSION >= (1, 9):
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != 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(PolymorphicGenericFormSetChild, self).get_form(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePolymorphicGenericInlineFormSet(BaseGenericInlineFormSet, BasePolymorphicModelFormSet):
|
||||||
|
"""
|
||||||
|
Polymorphic formset variation for inline generic formsets
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def polymorphic_generic_inlineformset_factory(model, formset_children, form=ModelForm,
|
||||||
|
formset=BasePolymorphicGenericInlineFormSet,
|
||||||
|
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,305 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||||
|
from django.forms.models import ModelForm, BaseModelFormSet, BaseInlineFormSet, modelform_factory, inlineformset_factory
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from .utils import add_media
|
||||||
|
|
||||||
|
|
||||||
|
class PolymorphicFormSetChild(object):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
#'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.
|
||||||
|
"""
|
||||||
|
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(BasePolymorphicModelFormSet, self).__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 = "%s-%s" % (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.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_concrete_instance_class() # respect proxy models
|
||||||
|
else:
|
||||||
|
# Extra or empty form, use the provided type.
|
||||||
|
# Note this completely tru
|
||||||
|
prefix = defaults['prefix']
|
||||||
|
try:
|
||||||
|
ct_id = int(self.data["{0}-polymorphic_ctype".format(prefix)])
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
raise ValidationError("Formset row {0} 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 ValidationError("Child model type {0} is not part of the formset".format(model))
|
||||||
|
else:
|
||||||
|
if 'instance' in defaults:
|
||||||
|
model = defaults['instance'].get_real_concrete_instance_class() # respect 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 = 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)
|
||||||
|
choices = [(ct.pk, ct)] # Single choice, existing forms can't change the value.
|
||||||
|
form.fields['polymorphic_ctype'] = forms.ChoiceField(choices=choices, initial=ct.pk, required=False, widget=forms.HiddenInput)
|
||||||
|
super(BasePolymorphicModelFormSet, self).add_fields(form, index)
|
||||||
|
|
||||||
|
def get_form_class(self, model):
|
||||||
|
if not self.child_forms:
|
||||||
|
raise ImproperlyConfigured("No 'child_forms' defined in {0}".format(self.__class__.__name__))
|
||||||
|
return self.child_forms[model]
|
||||||
|
|
||||||
|
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():
|
||||||
|
if django.VERSION >= (1, 9):
|
||||||
|
kwargs = self.get_form_kwargs(None) # New Django 1.9 method
|
||||||
|
else:
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
|
form = form_class(
|
||||||
|
auto_id=self.auto_id,
|
||||||
|
prefix=self.add_prefix('__prefix__'),
|
||||||
|
empty_permitted=True,
|
||||||
|
**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.")
|
||||||
|
|
||||||
|
|
||||||
|
class BasePolymorphicInlineFormSet(BaseInlineFormSet, BasePolymorphicModelFormSet):
|
||||||
|
"""
|
||||||
|
Polymorphic formset variation for inline formsets
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _construct_form(self, i, **kwargs):
|
||||||
|
return super(BasePolymorphicInlineFormSet, self)._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,10 @@
|
||||||
|
"""
|
||||||
|
Internal utils
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_media(dest, media):
|
||||||
|
"""
|
||||||
|
Optimized version of django.forms.Media.__add__() that doesn't create new objects.
|
||||||
|
"""
|
||||||
|
dest.add_css(media._css)
|
||||||
|
dest.add_js(media._js)
|
||||||
Loading…
Reference in New Issue