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
Diederik van der Boor 2016-06-10 14:30:51 +02:00
parent 2a599b5f99
commit a07ce7260c
8 changed files with 877 additions and 3 deletions

View File

@ -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),
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 (
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',
)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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',
)

View File

@ -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

View File

@ -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

View File

@ -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)