Merge branch 'feature/inlines'
commit
352b56e104
|
|
@ -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 *
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -55,6 +55,7 @@ Getting started
|
|||
|
||||
quickstart
|
||||
admin
|
||||
formsets
|
||||
performance
|
||||
|
||||
Advanced topics
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,)
|
||||
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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")
|
||||
|
|
@ -4,12 +4,43 @@ ModelAdmin code to display polymorphic models.
|
|||
The admin consists of a parent admin (which shows in the admin with a list),
|
||||
and a child admin (which is used internally to show the edit/delete dialog).
|
||||
"""
|
||||
# Admins for the regular models
|
||||
from .parentadmin import PolymorphicParentModelAdmin
|
||||
from .childadmin import PolymorphicChildModelAdmin
|
||||
|
||||
# Utils
|
||||
from .forms import PolymorphicModelChoiceForm
|
||||
from .filters import PolymorphicChildModelFilter
|
||||
|
||||
__all__ = (
|
||||
'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin',
|
||||
'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter'
|
||||
# Inlines
|
||||
from .inlines import (
|
||||
PolymorphicInlineModelAdmin, # base class
|
||||
StackedPolymorphicInline, # stacked inline
|
||||
)
|
||||
|
||||
# Helpers for the inlines
|
||||
from .helpers import (
|
||||
PolymorphicInlineAdminForm,
|
||||
PolymorphicInlineAdminFormSet,
|
||||
PolymorphicInlineSupportMixin, # mixin for the regular model admin!
|
||||
)
|
||||
|
||||
# Expose generic admin features too. There is no need to split those
|
||||
# as the admin already relies on contenttypes.
|
||||
from .generic import (
|
||||
GenericPolymorphicInlineModelAdmin, # base class
|
||||
GenericStackedPolymorphicInline, # stacked inline
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'PolymorphicParentModelAdmin',
|
||||
'PolymorphicChildModelAdmin',
|
||||
'PolymorphicModelChoiceForm',
|
||||
'PolymorphicChildModelFilter',
|
||||
'PolymorphicInlineAdminForm',
|
||||
'PolymorphicInlineAdminFormSet',
|
||||
'PolymorphicInlineSupportMixin',
|
||||
'PolymorphicInlineModelAdmin',
|
||||
'GenericPolymorphicInlineModelAdmin',
|
||||
'GenericStackedPolymorphicInline',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from django.core.urlresolvers import resolve
|
|||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .helpers import PolymorphicInlineSupportMixin
|
||||
|
||||
|
||||
class ParentAdminNotRegistered(RuntimeError):
|
||||
"The admin site for the model is not registered."
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ class PolymorphicChildModelFilter(admin.SimpleListFilter):
|
|||
"""
|
||||
An admin list filter for the PolymorphicParentModelAdmin which enables
|
||||
filtering by its child models.
|
||||
|
||||
This can be used in the parent admin:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
list_filter = (PolymorphicChildModelFilter,)
|
||||
"""
|
||||
title = _('Type')
|
||||
parameter_name = 'polymorphic_ctype'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
from django.contrib.contenttypes.admin import GenericInlineModelAdmin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from polymorphic.formsets import polymorphic_child_forms_factory, BaseGenericPolymorphicInlineFormSet, GenericPolymorphicFormSetChild
|
||||
from .inlines import PolymorphicInlineModelAdmin
|
||||
|
||||
|
||||
class GenericPolymorphicInlineModelAdmin(PolymorphicInlineModelAdmin, GenericInlineModelAdmin):
|
||||
"""
|
||||
Base class for variation of inlines based on generic foreign keys.
|
||||
"""
|
||||
formset = BaseGenericPolymorphicInlineFormSet
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
"""
|
||||
Construct the generic inline formset class.
|
||||
"""
|
||||
# Construct the FormSet class. This is almost the same as parent version,
|
||||
# except that a different super is called so generic_inlineformset_factory() is used.
|
||||
# NOTE that generic_inlineformset_factory() also makes sure the GFK fields are excluded in the form.
|
||||
FormSet = GenericInlineModelAdmin.get_formset(self, request, obj=obj, **kwargs)
|
||||
|
||||
FormSet.child_forms = polymorphic_child_forms_factory(
|
||||
formset_children=self.get_formset_children(request, obj=obj)
|
||||
)
|
||||
return FormSet
|
||||
|
||||
class Child(PolymorphicInlineModelAdmin.Child):
|
||||
"""
|
||||
Variation for generic inlines.
|
||||
"""
|
||||
# Make sure that the GFK fields are excluded from the child forms
|
||||
formset_child = GenericPolymorphicFormSetChild
|
||||
ct_field = "content_type"
|
||||
ct_fk_field = "object_id"
|
||||
|
||||
@cached_property
|
||||
def content_type(self):
|
||||
"""
|
||||
Expose the ContentType that the child relates to.
|
||||
This can be used for the ``polymorphic_ctype`` field.
|
||||
"""
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
|
||||
def get_formset_child(self, request, obj=None, **kwargs):
|
||||
# Similar to GenericInlineModelAdmin.get_formset(),
|
||||
# make sure the GFK is automatically excluded from the form
|
||||
defaults = {
|
||||
"ct_field": self.ct_field,
|
||||
"fk_field": self.ct_fk_field,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super(GenericPolymorphicInlineModelAdmin.Child, self).get_formset_child(request, obj=obj, **defaults)
|
||||
|
||||
|
||||
class GenericStackedPolymorphicInline(GenericPolymorphicInlineModelAdmin):
|
||||
"""
|
||||
The stacked layout for generic inlines.
|
||||
"""
|
||||
template = 'admin/polymorphic/edit_inline/stacked.html'
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
"""
|
||||
Rendering utils for admin forms;
|
||||
|
||||
This makes sure that admin fieldsets/layout settings are exported to the template.
|
||||
"""
|
||||
import json
|
||||
|
||||
import django
|
||||
from django.contrib.admin.helpers import InlineAdminFormSet, InlineAdminForm, AdminField
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from polymorphic.formsets import BasePolymorphicModelFormSet
|
||||
|
||||
|
||||
class PolymorphicInlineAdminForm(InlineAdminForm):
|
||||
"""
|
||||
Expose the admin configuration for a form
|
||||
"""
|
||||
|
||||
def polymorphic_ctype_field(self):
|
||||
return AdminField(self.form, 'polymorphic_ctype', False)
|
||||
|
||||
|
||||
class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
|
||||
"""
|
||||
Internally used class to expose the formset in the template.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request', None) # Assigned later via PolymorphicInlineSupportMixin later.
|
||||
self.obj = kwargs.pop('obj', None)
|
||||
super(PolymorphicInlineAdminFormSet, self).__init__(*args, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Output all forms using the proper subtype settings.
|
||||
"""
|
||||
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
|
||||
# Output the form
|
||||
model = original.get_real_concrete_instance_class()
|
||||
child_inline = self.opts.get_child_inline_instance(model)
|
||||
view_on_site_url = self.opts.get_view_on_site_url(original)
|
||||
|
||||
yield PolymorphicInlineAdminForm(
|
||||
formset=self.formset,
|
||||
form=form,
|
||||
fieldsets=self.get_child_fieldsets(child_inline),
|
||||
prepopulated_fields=self.get_child_prepopulated_fields(child_inline),
|
||||
original=original,
|
||||
readonly_fields=self.get_child_readonly_fields(child_inline),
|
||||
model_admin=child_inline,
|
||||
view_on_site_url=view_on_site_url
|
||||
)
|
||||
|
||||
# Extra rows, and empty prefixed forms.
|
||||
for form in self.formset.extra_forms + self.formset.empty_forms:
|
||||
model = form._meta.model
|
||||
child_inline = self.opts.get_child_inline_instance(model)
|
||||
yield PolymorphicInlineAdminForm(
|
||||
formset=self.formset,
|
||||
form=form,
|
||||
fieldsets=self.get_child_fieldsets(child_inline),
|
||||
prepopulated_fields=self.get_child_prepopulated_fields(child_inline),
|
||||
original=None,
|
||||
readonly_fields=self.get_child_readonly_fields(child_inline),
|
||||
model_admin=child_inline,
|
||||
)
|
||||
|
||||
def get_child_fieldsets(self, child_inline):
|
||||
return list(child_inline.get_fieldsets(self.request, self.obj) or ())
|
||||
|
||||
def get_child_readonly_fields(self, child_inline):
|
||||
return list(child_inline.get_readonly_fields(self.request, self.obj))
|
||||
|
||||
def get_child_prepopulated_fields(self, child_inline):
|
||||
fields = self.prepopulated_fields.copy()
|
||||
fields.update(child_inline.get_prepopulated_fields(self.request, self.obj))
|
||||
return fields
|
||||
|
||||
# The polymorphic template follows the same method like all other inlines do in Django 1.10.
|
||||
# This method is added for compatibility with older Django versions.
|
||||
def inline_formset_data(self):
|
||||
"""
|
||||
A JavaScript data structure for the JavaScript code
|
||||
"""
|
||||
verbose_name = self.opts.verbose_name
|
||||
return json.dumps({
|
||||
'name': '#%s' % self.formset.prefix,
|
||||
'options': {
|
||||
'prefix': self.formset.prefix,
|
||||
'addText': ugettext('Add another %(verbose_name)s') % {
|
||||
'verbose_name': capfirst(verbose_name),
|
||||
},
|
||||
'childTypes': [
|
||||
{
|
||||
'type': model._meta.model_name,
|
||||
'name': force_text(model._meta.verbose_name)
|
||||
} for model in self.formset.child_forms.keys()
|
||||
],
|
||||
'deleteText': ugettext('Remove'),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class PolymorphicInlineSupportMixin(object):
|
||||
"""
|
||||
A Mixin to add to the regular admin, so it can work with our polymorphic inlines.
|
||||
|
||||
This mixin needs to be included in the admin that hosts the ``inlines``.
|
||||
It makes sure the generated admin forms have different fieldsets/fields
|
||||
depending on the polymorphic type of the form instance.
|
||||
|
||||
This is achieved by overwriting :func:`get_inline_formsets` to return
|
||||
an :class:`PolymorphicInlineAdminFormSet` instead of a standard Django
|
||||
:class:`~django.contrib.admin.helpers.InlineAdminFormSet` for the polymorphic formsets.
|
||||
"""
|
||||
|
||||
def get_inline_formsets(self, request, formsets, inline_instances, obj=None):
|
||||
"""
|
||||
Overwritten version to produce the proper admin wrapping for the
|
||||
polymorphic inline formset. This fixes the media and form appearance
|
||||
of the inline polymorphic models.
|
||||
"""
|
||||
inline_admin_formsets = super(PolymorphicInlineSupportMixin, self).get_inline_formsets(
|
||||
request, formsets, inline_instances, obj=obj)
|
||||
|
||||
for admin_formset in inline_admin_formsets:
|
||||
if isinstance(admin_formset.formset, BasePolymorphicModelFormSet):
|
||||
# This is a polymorphic formset, which belongs to our inline.
|
||||
# Downcast the admin wrapper that generates the form fields.
|
||||
admin_formset.__class__ = PolymorphicInlineAdminFormSet
|
||||
admin_formset.request = request
|
||||
admin_formset.obj = obj
|
||||
return inline_admin_formsets
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
"""
|
||||
Django Admin support for polymorphic inlines.
|
||||
|
||||
Each row in the inline can correspond with a different subclass.
|
||||
"""
|
||||
from functools import partial
|
||||
|
||||
from django.contrib.admin.options import InlineModelAdmin
|
||||
from django.contrib.admin.utils import flatten_fieldsets
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.forms import Media
|
||||
|
||||
from polymorphic.formsets import polymorphic_child_forms_factory, BasePolymorphicInlineFormSet, PolymorphicFormSetChild
|
||||
from polymorphic.formsets.utils import add_media
|
||||
from .helpers import PolymorphicInlineSupportMixin
|
||||
|
||||
|
||||
class PolymorphicInlineModelAdmin(InlineModelAdmin):
|
||||
"""
|
||||
A polymorphic inline, where each formset row can be a different form.
|
||||
|
||||
Note that:
|
||||
|
||||
* Permissions are only checked on the base model.
|
||||
* The child inlines can't override the base model fields, only this parent inline can do that.
|
||||
"""
|
||||
|
||||
formset = BasePolymorphicInlineFormSet
|
||||
|
||||
#: The extra media to add for the polymorphic inlines effect.
|
||||
#: This can be redefined for subclasses.
|
||||
polymorphic_media = Media(
|
||||
js=(
|
||||
'polymorphic/js/polymorphic_inlines.js',
|
||||
),
|
||||
css={
|
||||
'all': (
|
||||
'polymorphic/css/polymorphic_inlines.css',
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
#: The extra forms to show
|
||||
#: By default there are no 'extra' forms as the desired type is unknown.
|
||||
#: Instead, add each new item using JavaScript that first offers a type-selection.
|
||||
extra = 0
|
||||
|
||||
#: Inlines for all model sub types that can be displayed in this inline.
|
||||
#: Each row is a :class:`PolymorphicInlineModelAdmin.Child`
|
||||
child_inlines = ()
|
||||
|
||||
def __init__(self, parent_model, admin_site):
|
||||
super(PolymorphicInlineModelAdmin, self).__init__(parent_model, admin_site)
|
||||
|
||||
# Extra check to avoid confusion
|
||||
# While we could monkeypatch the admin here, better stay explicit.
|
||||
parent_admin = admin_site._registry.get(parent_model, None)
|
||||
if parent_admin is not None: # Can be None during check
|
||||
if not isinstance(parent_admin, PolymorphicInlineSupportMixin):
|
||||
raise ImproperlyConfigured(
|
||||
"To use polymorphic inlines, add the `PolymorphicInlineSupportMixin` mixin "
|
||||
"to the ModelAdmin that hosts the inline."
|
||||
)
|
||||
|
||||
# While the inline is created per request, the 'request' object is not known here.
|
||||
# Hence, creating all child inlines unconditionally, without checking permissions.
|
||||
self.child_inline_instances = self.get_child_inline_instances()
|
||||
|
||||
# Create a lookup table
|
||||
self._child_inlines_lookup = {}
|
||||
for child_inline in self.child_inline_instances:
|
||||
self._child_inlines_lookup[child_inline.model] = child_inline
|
||||
|
||||
def get_child_inline_instances(self):
|
||||
"""
|
||||
:rtype List[PolymorphicInlineModelAdmin.Child]
|
||||
"""
|
||||
instances = []
|
||||
for ChildInlineType in self.child_inlines:
|
||||
instances.append(ChildInlineType(parent_inline=self))
|
||||
return instances
|
||||
|
||||
def get_child_inline_instance(self, model):
|
||||
"""
|
||||
Find the child inline for a given model.
|
||||
|
||||
:rtype: PolymorphicInlineModelAdmin.Child
|
||||
"""
|
||||
try:
|
||||
return self._child_inlines_lookup[model]
|
||||
except KeyError:
|
||||
raise ValueError("Model '{0}' not found in child_inlines".format(model.__name__))
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
"""
|
||||
Construct the inline formset class.
|
||||
|
||||
This passes all class attributes to the formset.
|
||||
|
||||
:rtype: type
|
||||
"""
|
||||
# Construct the FormSet class
|
||||
FormSet = super(PolymorphicInlineModelAdmin, self).get_formset(request, obj=obj, **kwargs)
|
||||
|
||||
# Instead of completely redefining super().get_formset(), we use
|
||||
# the regular inlineformset_factory(), and amend that with our extra bits.
|
||||
# This code line is the essence of what polymorphic_inlineformset_factory() does.
|
||||
FormSet.child_forms = polymorphic_child_forms_factory(
|
||||
formset_children=self.get_formset_children(request, obj=obj)
|
||||
)
|
||||
return FormSet
|
||||
|
||||
def get_formset_children(self, request, obj=None):
|
||||
"""
|
||||
The formset 'children' provide the details for all child models that are part of this formset.
|
||||
It provides a stripped version of the modelform/formset factory methods.
|
||||
"""
|
||||
formset_children = []
|
||||
for child_inline in self.child_inline_instances:
|
||||
# TODO: the children can be limited here per request based on permissions.
|
||||
formset_children.append(child_inline.get_formset_child(request, obj=obj))
|
||||
return formset_children
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
"""
|
||||
Hook for specifying fieldsets.
|
||||
"""
|
||||
if self.fieldsets:
|
||||
return self.fieldsets
|
||||
else:
|
||||
return [] # Avoid exposing fields to the child
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
if self.fields:
|
||||
return self.fields
|
||||
else:
|
||||
return [] # Avoid exposing fields to the child
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
# The media of the inline focuses on the admin settings,
|
||||
# whether to expose the scripts for filter_horizontal etc..
|
||||
# The admin helper exposes the inline + formset media.
|
||||
base_media = super(PolymorphicInlineModelAdmin, self).media
|
||||
all_media = Media()
|
||||
add_media(all_media, base_media)
|
||||
|
||||
# Add all media of the child inline instances
|
||||
for child_instance in self.child_inline_instances:
|
||||
child_media = child_instance.media
|
||||
|
||||
# Avoid adding the same media object again and again
|
||||
if child_media._css != base_media._css and child_media._js != base_media._js:
|
||||
add_media(all_media, child_media)
|
||||
|
||||
add_media(all_media, self.polymorphic_media)
|
||||
|
||||
return all_media
|
||||
|
||||
class Child(InlineModelAdmin):
|
||||
"""
|
||||
The child inline; which allows configuring the admin options
|
||||
for the child appearance.
|
||||
|
||||
Note that not all options will be honored by the parent, notably the formset options:
|
||||
* :attr:`extra`
|
||||
* :attr:`min_num`
|
||||
* :attr:`max_num`
|
||||
|
||||
The model form options however, will all be read.
|
||||
"""
|
||||
formset_child = PolymorphicFormSetChild
|
||||
extra = 0 # TODO: currently unused for the children.
|
||||
|
||||
def __init__(self, parent_inline):
|
||||
self.parent_inline = parent_inline
|
||||
super(PolymorphicInlineModelAdmin.Child, self).__init__(parent_inline.parent_model, parent_inline.admin_site)
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
# The child inline is only used to construct the form,
|
||||
# and allow to override the form field attributes.
|
||||
# The formset is created by the parent inline.
|
||||
raise RuntimeError("The child get_formset() is not used.")
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
if self.fields:
|
||||
return self.fields
|
||||
|
||||
# Standard Django logic, use the form to determine the fields.
|
||||
# The form needs to pass through all factory logic so all 'excludes' are set as well.
|
||||
# Default Django does: form = self.get_formset(request, obj, fields=None).form
|
||||
# Use 'fields=None' avoids recursion in the field autodetection.
|
||||
form = self.get_formset_child(request, obj, fields=None).get_form()
|
||||
return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
|
||||
|
||||
def get_formset_child(self, request, obj=None, **kwargs):
|
||||
"""
|
||||
Return the formset child that the parent inline can use to represent us.
|
||||
|
||||
:rtype: PolymorphicFormSetChild
|
||||
"""
|
||||
# Similar to the normal get_formset(), the caller may pass fields to override the defaults settings
|
||||
# in the inline. In Django's GenericInlineModelAdmin.get_formset() this is also used in the same way,
|
||||
# to make sure the 'exclude' also contains the GFK fields.
|
||||
#
|
||||
# Hence this code is almost identical to InlineModelAdmin.get_formset()
|
||||
# and GenericInlineModelAdmin.get_formset()
|
||||
#
|
||||
# Transfer the local inline attributes to the formset child,
|
||||
# this allows overriding settings.
|
||||
if 'fields' in kwargs:
|
||||
fields = kwargs.pop('fields')
|
||||
else:
|
||||
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
|
||||
|
||||
if self.exclude is None:
|
||||
exclude = []
|
||||
else:
|
||||
exclude = list(self.exclude)
|
||||
|
||||
exclude.extend(self.get_readonly_fields(request, obj))
|
||||
|
||||
if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
|
||||
# Take the custom ModelForm's Meta.exclude into account only if the
|
||||
# InlineModelAdmin doesn't define its own.
|
||||
exclude.extend(self.form._meta.exclude)
|
||||
|
||||
#can_delete = self.can_delete and self.has_delete_permission(request, obj)
|
||||
defaults = {
|
||||
"form": self.form,
|
||||
"fields": fields,
|
||||
"exclude": exclude or None,
|
||||
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
# This goes through the same logic that get_formset() calls
|
||||
# by passing the inline class attributes to modelform_factory()
|
||||
FormSetChildClass = self.formset_child
|
||||
return FormSetChildClass(self.model, **defaults)
|
||||
|
||||
|
||||
class StackedPolymorphicInline(PolymorphicInlineModelAdmin):
|
||||
"""
|
||||
Stacked inline for django-polymorphic models.
|
||||
Since tabular doesn't make much sense with changed fields, just offer this one.
|
||||
"""
|
||||
template = 'admin/polymorphic/edit_inline/stacked.html'
|
||||
|
|
@ -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',
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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> <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>
|
||||
Loading…
Reference in New Issue