Merge branch 'feature/inlines'

fix_request_path_info
Diederik van der Boor 2016-08-10 11:48:02 +02:00
commit 352b56e104
23 changed files with 1599 additions and 6 deletions

View File

@ -2,4 +2,5 @@ include README.rst
include LICENSE
include DOCS.rst
include CHANGES.rst
recursive-include polymorphic/static *.js *.css
recursive-include polymorphic/templates *

View File

@ -59,10 +59,12 @@ use the ``base_form`` and ``base_fieldsets`` instead. The ``PolymorphicChildMode
automatically detect the additional fields that the child model has, display those in a separate fieldset.
Polymorphic Inlines
-------------------
Using polymorphic models in standard inlines
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To add a polymorphic child model as an Inline for another model, add a field to the inline's readonly_fields list formed by the lowercased name of the polymorphic parent model with the string "_ptr" appended to it. Otherwise, trying to save that model in the admin will raise an AttributeError with the message "can't set attribute".
To add a polymorphic child model as an Inline for another model, add a field to the inline's ``readonly_fields`` list
formed by the lowercased name of the polymorphic parent model with the string ``_ptr`` appended to it.
Otherwise, trying to save that model in the admin will raise an AttributeError with the message "can't set attribute".
.. _admin-example:

12
docs/formsets.rst 100644
View File

@ -0,0 +1,12 @@
Formsets
========
Polymorphic models can be used in formsets.
Use the :func:`polymorphic.formsets.polymorphic_inlineformset_factory` function to generate the formset.
As extra parameter, the factory needs to know how to display the child models.
Provide a list of :class:`polymorphic.formsets.PolymorphicFormSetChild` objects for this
.. code-block:: python
from polymorphic.formsets import polymorphic_child_forms_factory

View File

@ -55,6 +55,7 @@ Getting started
quickstart
admin
formsets
performance
Advanced topics

View File

@ -73,6 +73,7 @@ INSTALLED_APPS = (
'polymorphic', # needed if you want to use the polymorphic admin
'pexp', # this Django app is for testing and experimentation; not needed otherwise
'orders',
)
if django.VERSION >= (1, 7):

View File

View File

@ -0,0 +1,43 @@
from django.contrib import admin
from polymorphic.admin import PolymorphicInlineSupportMixin, StackedPolymorphicInline
from .models import Order, Payment, CreditCardPayment, BankPayment, SepaPayment
class CreditCardPaymentInline(StackedPolymorphicInline.Child):
model = CreditCardPayment
class BankPaymentInline(StackedPolymorphicInline.Child):
model = BankPayment
class SepaPaymentInline(StackedPolymorphicInline.Child):
model = SepaPayment
class PaymentInline(StackedPolymorphicInline):
"""
An inline for a polymorphic model.
The actual form appearance of each row is determined by
the child inline that corresponds with the actual model type.
"""
model = Payment
child_inlines = (
CreditCardPaymentInline,
BankPaymentInline,
SepaPaymentInline,
)
@admin.register(Order)
class OrderAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin):
"""
Admin for orders.
The inline is polymorphic.
To make sure the inlines are properly handled,
the ``PolymorphicInlineSupportMixin`` is needed to
"""
inlines = (PaymentInline,)

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Order',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=200, verbose_name='Title')),
],
options={
'ordering': ('title',),
'verbose_name': 'Organisation',
'verbose_name_plural': 'Organisations',
},
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('currency', models.CharField(default=b'USD', max_length=3)),
('amount', models.DecimalField(max_digits=10, decimal_places=2)),
],
options={
'verbose_name': 'Payment',
'verbose_name_plural': 'Payments',
},
),
migrations.CreateModel(
name='BankPayment',
fields=[
('payment_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='orders.Payment')),
('bank_name', models.CharField(max_length=100)),
('swift', models.CharField(max_length=20)),
],
options={
'verbose_name': 'Bank Payment',
'verbose_name_plural': 'Bank Payments',
},
bases=('orders.payment',),
),
migrations.CreateModel(
name='CreditCardPayment',
fields=[
('payment_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='orders.Payment')),
('card_type', models.CharField(max_length=10)),
('expiry_month', models.PositiveSmallIntegerField(choices=[(1, 'jan'), (2, 'feb'), (3, 'mar'), (4, 'apr'), (5, 'may'), (6, 'jun'), (7, 'jul'), (8, 'aug'), (9, 'sep'), (10, 'oct'), (11, 'nov'), (12, 'dec')])),
('expiry_year', models.PositiveIntegerField()),
],
options={
'verbose_name': 'Credit Card Payment',
'verbose_name_plural': 'Credit Card Payments',
},
bases=('orders.payment',),
),
migrations.CreateModel(
name='SepaPayment',
fields=[
('payment_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='orders.Payment')),
('iban', models.CharField(max_length=34)),
('bic', models.CharField(max_length=11)),
],
options={
'verbose_name': 'Bank Payment',
'verbose_name_plural': 'Bank Payments',
},
bases=('orders.payment',),
),
migrations.AddField(
model_name='payment',
name='order',
field=models.ForeignKey(to='orders.Order'),
),
migrations.AddField(
model_name='payment',
name='polymorphic_ctype',
field=models.ForeignKey(related_name='polymorphic_orders.payment_set+', editable=False, to='contenttypes.ContentType', null=True),
),
]

View File

@ -0,0 +1,78 @@
from django.db import models
from django.utils.dates import MONTHS_3
from django.utils.six import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from polymorphic.models import PolymorphicModel
@python_2_unicode_compatible
class Order(models.Model):
"""
An example order that has polymorphic relations
"""
title = models.CharField(_("Title"), max_length=200)
class Meta:
verbose_name = _("Organisation")
verbose_name_plural = _("Organisations")
ordering = ('title',)
def __str__(self):
return self.title
@python_2_unicode_compatible
class Payment(PolymorphicModel):
"""
A generic payment model.
"""
order = models.ForeignKey(Order)
currency = models.CharField(default='USD', max_length=3)
amount = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
verbose_name = _("Payment")
verbose_name_plural = _("Payments")
def __str__(self):
return "{0} {1}".format(self.currency, self.amount)
class CreditCardPayment(Payment):
"""
Credit card
"""
MONTH_CHOICES = [(i, n) for i, n in sorted(MONTHS_3.items())]
card_type = models.CharField(max_length=10)
expiry_month = models.PositiveSmallIntegerField(choices=MONTH_CHOICES)
expiry_year = models.PositiveIntegerField()
class Meta:
verbose_name = _("Credit Card Payment")
verbose_name_plural = _("Credit Card Payments")
class BankPayment(Payment):
"""
Payment by bank
"""
bank_name = models.CharField(max_length=100)
swift = models.CharField(max_length=20)
class Meta:
verbose_name = _("Bank Payment")
verbose_name_plural = _("Bank Payments")
class SepaPayment(Payment):
"""
Payment by SEPA (EU)
"""
iban = models.CharField(max_length=34)
bic = models.CharField(max_length=11)
class Meta:
verbose_name = _("SEPA Payment")
verbose_name_plural = _("SEPA Payments")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
BaseGenericPolymorphicInlineFormSet,
GenericPolymorphicFormSetChild,
generic_polymorphic_inlineformset_factory,
)
__all__ = (
'BasePolymorphicModelFormSet',
'BasePolymorphicInlineFormSet',
'PolymorphicFormSetChild',
'polymorphic_inlineformset_factory',
'polymorphic_child_forms_factory',
'BaseGenericPolymorphicInlineFormSet',
'GenericPolymorphicFormSetChild',
'generic_polymorphic_inlineformset_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 GenericPolymorphicFormSetChild(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(GenericPolymorphicFormSetChild, 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(GenericPolymorphicFormSetChild, self).get_form(**kwargs)
class BaseGenericPolymorphicInlineFormSet(BaseGenericInlineFormSet, BasePolymorphicModelFormSet):
"""
Polymorphic formset variation for inline generic formsets
"""
def generic_polymorphic_inlineformset_factory(model, formset_children, form=ModelForm,
formset=BaseGenericPolymorphicInlineFormSet,
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,313 @@
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 or ()
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.
This is mostly used internally, and rarely needs to be used by external projects.
When using the factory methods (:func:`polymorphic_inlineformset_factory`),
this feature is called already for you.
"""
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):
"""
Return the proper form class for the given 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)

View File

@ -0,0 +1,27 @@
.polymorphic-add-choice {
position: relative;
}
.polymorphic-add-choice a:focus {
text-decoration: none;
}
.polymorphic-type-menu {
position: absolute;
top: 2.2em;
left: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
padding: 2px;
background-color: #fff;
}
.polymorphic-type-menu ul {
padding: 2px;
margin: 0;
}
.polymorphic-type-menu li {
list-style: none inside none;
padding: 4px 8px;
}

View File

@ -0,0 +1,334 @@
/*global DateTimeShortcuts, SelectFilter*/
// This is a slightly adapted version of Django's inlines.js
// Forked for polymorphic by Diederik van der Boor
/**
* Django admin inlines
*
* Based on jQuery Formset 1.1
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
* Copyright (c) 2009, Stanislaus Madueke
* All rights reserved.
*
* Spiced up with Code from Zain Memon's GSoC project 2009
* and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip.
*
* Licensed under the New BSD License
* See: http://www.opensource.org/licenses/bsd-license.php
*/
(function($) {
'use strict';
$.fn.polymorphicFormset = function(opts) {
var options = $.extend({}, $.fn.polymorphicFormset.defaults, opts);
var $this = $(this);
var $parent = $this.parent();
var updateElementIndex = function(el, prefix, ndx) {
var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
var replacement = prefix + "-" + ndx;
if ($(el).prop("for")) {
$(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
el.name = el.name.replace(id_regex, replacement);
}
};
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off");
var nextIndex = parseInt(totalForms.val(), 10);
var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off");
// only show the add button if we are allowed to add more items,
// note that max_num = None translates to a blank string.
var showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0;
$this.each(function(i) {
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
});
if ($this.length && showAddButton) {
var addContainer;
var menuButton;
var addButtons;
// For Polymorphic inlines, the add button opens a menu.
var menu = '<div class="polymorphic-type-menu" style="display: none;"><ul>';
for (var i = 0; i < options.childTypes.length; i++) {
var obj = options.childTypes[i];
menu += '<li><a href="#" data-type="' + obj.type + '">' + obj.name + '</a></li>';
}
menu += '</ul></div>';
if ($this.prop("tagName") === "TR") {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = this.eq(-1).children().length;
$parent.append('<tr class="' + options.addCssClass + ' polymorphic-add-choice"><td colspan="' + numCols + '"><a href="#">' + options.addText + "</a>" + menu + "</tr>");
addContainer = $parent.find("tr:last > td");
menuButton = addContainer.children('a');
addButtons = addContainer.find("li a");
} else {
// Otherwise, insert it immediately after the last form:
$this.filter(":last").after('<div class="' + options.addCssClass + ' polymorphic-add-choice"><a href="#">' + options.addText + "</a>" + menu + "</div>");
addContainer = $this.filter(":last").next();
menuButton = addContainer.children('a');
addButtons = addContainer.find("li a");
}
menuButton.click(function(event) {
event.preventDefault();
event.stopPropagation(); // for menu hide
var $menu = $(event.target).next('.polymorphic-type-menu');
if(! $menu.is(':visible')) {
var hideMenu = function() {
$menu.slideUp(50);
$(document).unbind('click', hideMenu);
};
$(document).click(hideMenu);
}
$menu.slideToggle(50);
});
addButtons.click(function(event) {
event.preventDefault();
var polymorphicType = $(event.target).attr('data-type'); // Select polymorphic type.
var template = $("#" + polymorphicType + "-empty");
var row = template.clone(true);
row.removeClass(options.emptyCssClass)
.addClass(options.formCssClass)
.attr("id", options.prefix + "-" + nextIndex);
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
} else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
}
row.find("*").each(function() {
updateElementIndex(this, options.prefix, totalForms.val());
});
// Insert the new form when it has been fully edited
row.insertBefore($(template));
// Update number of total forms
$(totalForms).val(parseInt(totalForms.val(), 10) + 1);
nextIndex += 1;
// Hide add button in case we've hit the max, except we want to add infinitely
if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
addButtons.parent().hide();
}
// The delete button of each row triggers a bunch of other things
row.find("a." + options.deleteCssClass).click(function(e1) {
e1.preventDefault();
// Remove the parent form containing this button:
row.remove();
nextIndex -= 1;
// If a post-delete callback was provided, call it with the deleted form:
if (options.removed) {
options.removed(row);
}
$(document).trigger('formset:removed', [row, options.prefix]);
// Update the TOTAL_FORMS form count.
var forms = $("." + options.formCssClass);
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
// Show add button again once we drop below max
if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) {
addButtons.parent().show();
}
// Also, update names and ids for all remaining form controls
// so they remain in sequence:
var i, formCount;
var updateElementCallback = function() {
updateElementIndex(this, options.prefix, i);
};
for (i = 0, formCount = forms.length; i < formCount; i++) {
updateElementIndex($(forms).get(i), options.prefix, i);
$(forms.get(i)).find("*").each(updateElementCallback);
}
});
// If a post-add callback was supplied, call it with the added form:
if (options.added) {
options.added(row);
}
$(document).trigger('formset:added', [row, options.prefix]);
});
}
return this;
};
/* Setup plugin defaults */
$.fn.polymorphicFormset.defaults = {
prefix: "form", // The form prefix for your django formset
addText: "add another", // Text for the add link
childTypes: null, // defined by the client.
deleteText: "remove", // Text for the delete link
addCssClass: "add-row", // CSS class applied to the add link
deleteCssClass: "delete-row", // CSS class applied to the delete link
emptyCssClass: "empty-row", // CSS class applied to the empty row
formCssClass: "dynamic-form", // CSS class applied to each form in a formset
added: null, // Function called each time a new form is added
removed: null, // Function called each time a form is deleted
addButton: null // Existing add button to use
};
// Tabular inlines ---------------------------------------------------------
$.fn.tabularPolymorphicFormset = function(options) {
var $rows = $(this);
var alternatingRows = function(row) {
$($rows.selector).not(".add-row").removeClass("row1 row2")
.filter(":even").addClass("row1").end()
.filter(":odd").addClass("row2");
};
var reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
var updateSelectFilter = function() {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof SelectFilter !== 'undefined') {
$('.selectfilter').each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], false);
});
$('.selectfilterstacked').each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], true);
});
}
};
var initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
var field = $(this),
input = field.find('input, select, textarea'),
dependency_list = input.data('dependency_list') || [],
dependencies = [];
$.each(dependency_list, function(i, field_name) {
dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id'));
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
};
$rows.polymorphicFormset({
prefix: options.prefix,
addText: options.addText,
childTypes: options.childTypes,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: alternatingRows,
added: function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
alternatingRows(row);
},
addButton: options.addButton
});
return $rows;
};
// Stacked inlines ---------------------------------------------------------
$.fn.stackedPolymorphicFormset = function(options) {
var $rows = $(this);
var updateInlineLabel = function(row) {
$($rows.selector).find(".inline_label").each(function(i) {
var count = i + 1;
$(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
});
};
var reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force, yuck.
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
var updateSelectFilter = function() {
// If any SelectFilter widgets were added, instantiate a new instance.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], false);
});
$(".selectfilterstacked").each(function(index, value) {
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length - 1], true);
});
}
};
var initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
var field = $(this),
input = field.find('input, select, textarea'),
dependency_list = input.data('dependency_list') || [],
dependencies = [];
$.each(dependency_list, function(i, field_name) {
dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id'));
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
};
$rows.polymorphicFormset({
prefix: options.prefix,
addText: options.addText,
childTypes: options.childTypes,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: updateInlineLabel,
added: function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
updateInlineLabel(row);
},
addButton: options.addButton
});
return $rows;
};
$(document).ready(function() {
$(".js-inline-polymorphic-admin-formset").each(function() {
var data = $(this).data(),
inlineOptions = data.inlineFormset;
switch(data.inlineType) {
case "stacked":
$(inlineOptions.name + "-group .inline-related").stackedPolymorphicFormset(inlineOptions.options);
break;
case "tabular":
$(inlineOptions.name + "-group .tabular.inline-related tbody tr").tabularPolymorphicFormset(inlineOptions.options);
break;
}
});
});
})(django.jQuery);

View File

@ -0,0 +1,36 @@
{% load i18n admin_urls static %}
<div class="js-inline-polymorphic-admin-formset inline-group"
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }}">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}
<div class="inline-related inline-{{ inline_admin_form.model_admin.opts.model_name }}{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if not inline_admin_form.original.pk %} empty-form {% endif %}{% if forloop.last %} last-related{% endif %}"
id="{% if inline_admin_form.original.pk %}{{ inline_admin_formset.formset.prefix }}-{{ forloop.counter0 }}{% else %}{{ inline_admin_form.model_admin.opts.model_name }}-empty{% endif %}">
<h3><b>{{ inline_admin_form.model_admin.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
{% if inline_admin_form.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
</h3>
{% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }}
{{ inline_admin_form.polymorphic_ctype_field.field }}
</div>
{% endfor %}
</fieldset>
</div>