diff --git a/polymorphic/admin/__init__.py b/polymorphic/admin/__init__.py index 60f9f8a..e067b62 100644 --- a/polymorphic/admin/__init__.py +++ b/polymorphic/admin/__init__.py @@ -14,22 +14,22 @@ from .filters import PolymorphicChildModelFilter # Inlines from .inlines import ( - PolymorphicParentInlineModelAdmin, - PolymorphicChildInlineModelAdmin, + PolymorphicInlineModelAdmin, # base class + StackedPolymorphicInline, # stacked inline ) # Helpers for the inlines from .helpers import ( - InlinePolymorphicAdminForm, - InlinePolymorphicAdminFormSet, + 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 ( - PolymorphicParentGenericInlineModelAdmin, - PolymorphicChildGenericInlineModelAdmin, + GenericPolymorphicInlineModelAdmin, # base class + GenericStackedPolymorphicInline, # stacked inline ) __all__ = ( @@ -37,11 +37,10 @@ __all__ = ( 'PolymorphicChildModelAdmin', 'PolymorphicModelChoiceForm', 'PolymorphicChildModelFilter', - 'InlinePolymorphicAdminForm', - 'InlinePolymorphicAdminFormSet', + 'PolymorphicInlineAdminForm', + 'PolymorphicInlineAdminFormSet', 'PolymorphicInlineSupportMixin', - 'PolymorphicParentInlineModelAdmin', - 'PolymorphicChildInlineModelAdmin', - 'PolymorphicParentGenericInlineModelAdmin', - 'PolymorphicChildGenericInlineModelAdmin', + 'PolymorphicInlineModelAdmin', + 'GenericPolymorphicInlineModelAdmin', + 'GenericStackedPolymorphicInline', ) diff --git a/polymorphic/admin/generic.py b/polymorphic/admin/generic.py index 479a17e..747be67 100644 --- a/polymorphic/admin/generic.py +++ b/polymorphic/admin/generic.py @@ -2,15 +2,15 @@ from django.contrib.contenttypes.admin import GenericInlineModelAdmin from django.contrib.contenttypes.models import ContentType from django.utils.functional import cached_property -from polymorphic.formsets import polymorphic_child_forms_factory, BasePolymorphicGenericInlineFormSet, PolymorphicGenericFormSetChild -from .inlines import PolymorphicParentInlineModelAdmin, PolymorphicChildInlineModelAdmin +from polymorphic.formsets import polymorphic_child_forms_factory, BaseGenericPolymorphicInlineFormSet, GenericPolymorphicFormSetChild +from .inlines import PolymorphicInlineModelAdmin -class PolymorphicParentGenericInlineModelAdmin(PolymorphicParentInlineModelAdmin, GenericInlineModelAdmin): +class GenericPolymorphicInlineModelAdmin(PolymorphicInlineModelAdmin, GenericInlineModelAdmin): """ - Variation for inlines based on generic foreign keys. + Base class for variation of inlines based on generic foreign keys. """ - formset = BasePolymorphicGenericInlineFormSet + formset = BaseGenericPolymorphicInlineFormSet def get_formset(self, request, obj=None, **kwargs): """ @@ -26,30 +26,36 @@ class PolymorphicParentGenericInlineModelAdmin(PolymorphicParentInlineModelAdmin ) return FormSet - -class PolymorphicChildGenericInlineModelAdmin(PolymorphicChildInlineModelAdmin): - """ - Variation for generic inlines. - """ - # Make sure that the GFK fields are excluded from the child forms - formset_child = PolymorphicGenericFormSetChild - ct_field = "content_type" - ct_fk_field = "object_id" - - @cached_property - def content_type(self): + class Child(PolymorphicInlineModelAdmin.Child): """ - Expose the ContentType that the child relates to. - This can be used for the ``polymorphic_ctype`` field. + Variation for generic inlines. """ - return ContentType.objects.get_for_model(self.model) + # Make sure that the GFK fields are excluded from the child forms + formset_child = GenericPolymorphicFormSetChild + ct_field = "content_type" + ct_fk_field = "object_id" - def get_formset_child(self, request, obj=None, **kwargs): - # Similar to GenericInlineModelAdmin.get_formset(), - # make sure the GFK is automatically excluded from the form - defaults = { - "ct_field": self.ct_field, - "fk_field": self.ct_fk_field, - } - defaults.update(kwargs) - return super(PolymorphicChildGenericInlineModelAdmin, self).get_formset_child(request, obj=obj, **defaults) + @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' diff --git a/polymorphic/admin/helpers.py b/polymorphic/admin/helpers.py index 89e9da4..32698c9 100644 --- a/polymorphic/admin/helpers.py +++ b/polymorphic/admin/helpers.py @@ -3,19 +3,19 @@ Rendering utils for admin forms; This makes sure that admin fieldsets/layout settings are exported to the template. """ -from django.contrib.admin.helpers import InlineAdminFormSet, InlineAdminForm +import django +from django.contrib.admin.helpers import InlineAdminFormSet, InlineAdminForm, AdminField from polymorphic.formsets import BasePolymorphicModelFormSet -class InlinePolymorphicAdminForm(InlineAdminForm): +class PolymorphicInlineAdminForm(InlineAdminForm): """ Expose the admin configuration for a form """ - pass -class InlinePolymorphicAdminFormSet(InlineAdminFormSet): +class PolymorphicInlineAdminFormSet(InlineAdminFormSet): """ Internally used class to expose the formset in the template. """ @@ -23,7 +23,7 @@ class InlinePolymorphicAdminFormSet(InlineAdminFormSet): def __init__(self, *args, **kwargs): self.request = kwargs.pop('request', None) # Assigned later via PolymorphicInlineSupportMixin later. self.obj = kwargs.pop('obj', None) - super(InlinePolymorphicAdminFormSet, self).__init__(*args, **kwargs) + super(PolymorphicInlineAdminFormSet, self).__init__(*args, **kwargs) def __iter__(self): """ @@ -35,7 +35,7 @@ class InlinePolymorphicAdminFormSet(InlineAdminFormSet): child_inline = self.opts.get_child_inline_instance(model) view_on_site_url = self.opts.get_view_on_site_url(original) - yield InlinePolymorphicAdminForm( + yield PolymorphicInlineAdminForm( formset=self.formset, form=form, fieldsets=self.get_child_fieldsets(child_inline), @@ -50,7 +50,7 @@ class InlinePolymorphicAdminFormSet(InlineAdminFormSet): for form in self.formset.extra_forms + self.formset.empty_forms: model = form._meta.model child_inline = self.opts.get_child_inline_instance(model) - yield InlinePolymorphicAdminForm( + yield PolymorphicInlineAdminForm( formset=self.formset, form=form, fieldsets=self.get_child_fieldsets(child_inline), @@ -81,7 +81,7 @@ class PolymorphicInlineSupportMixin(object): depending on the polymorphic type of the form instance. This is achieved by overwriting :func:`get_inline_formsets` to return - an :class:`InlinePolymorphicAdminFormSet` instead of a standard Django + an :class:`PolymorphicInlineAdminFormSet` instead of a standard Django :class:`~django.contrib.admin.helpers.InlineAdminFormSet` for the polymorphic formsets. """ @@ -98,7 +98,7 @@ class PolymorphicInlineSupportMixin(object): 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__ = InlinePolymorphicAdminFormSet + admin_formset.__class__ = PolymorphicInlineAdminFormSet admin_formset.request = request admin_formset.obj = obj return inline_admin_formsets diff --git a/polymorphic/admin/inlines.py b/polymorphic/admin/inlines.py index 7817cf2..1a72c23 100644 --- a/polymorphic/admin/inlines.py +++ b/polymorphic/admin/inlines.py @@ -13,7 +13,7 @@ from polymorphic.formsets import polymorphic_child_forms_factory, BasePolymorphi from polymorphic.formsets.utils import add_media -class PolymorphicParentInlineModelAdmin(InlineModelAdmin): +class PolymorphicInlineModelAdmin(InlineModelAdmin): """ A polymorphic inline, where each formset row can be a different form. @@ -30,11 +30,11 @@ class PolymorphicParentInlineModelAdmin(InlineModelAdmin): extra = 0 #: Inlines for all model sub types that can be displayed in this inline. - #: Each row is a :class:`PolymorphicChildInlineModelAdmin` + #: Each row is a :class:`PolymorphicInlineModelAdmin.Child` child_inlines = () def __init__(self, parent_model, admin_site): - super(PolymorphicParentInlineModelAdmin, self).__init__(parent_model, admin_site) + super(PolymorphicInlineModelAdmin, self).__init__(parent_model, admin_site) # While the inline is created per request, the 'request' object is not known here. # Hence, creating all child inlines unconditionally, without checking permissions. @@ -47,7 +47,7 @@ class PolymorphicParentInlineModelAdmin(InlineModelAdmin): def get_child_inline_instances(self): """ - :rtype List[PolymorphicChildInlineModelAdmin] + :rtype List[PolymorphicInlineModelAdmin.Child] """ instances = [] for ChildInlineType in self.child_inlines: @@ -58,7 +58,7 @@ class PolymorphicParentInlineModelAdmin(InlineModelAdmin): """ Find the child inline for a given model. - :rtype: PolymorphicChildInlineModelAdmin + :rtype: PolymorphicInlineModelAdmin.Child """ try: return self._child_inlines_lookup[model] @@ -74,7 +74,7 @@ class PolymorphicParentInlineModelAdmin(InlineModelAdmin): :rtype: type """ # Construct the FormSet class - FormSet = super(PolymorphicParentInlineModelAdmin, self).get_formset(request, obj=obj, **kwargs) + 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. @@ -115,7 +115,7 @@ class PolymorphicParentInlineModelAdmin(InlineModelAdmin): # The media of the inline focuses on the admin settings, # whether to expose the scripts for filter_horizontal etc.. # The admin helper exposes the inline + formset media. - base_media = super(PolymorphicParentInlineModelAdmin, self).media + base_media = super(PolymorphicInlineModelAdmin, self).media all_media = Media() add_media(all_media, base_media) @@ -127,87 +127,96 @@ class PolymorphicParentInlineModelAdmin(InlineModelAdmin): 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 PolymorphicChildInlineModelAdmin(InlineModelAdmin): - """ - The child inline; which allows configuring the admin options - for the child appearance. - - Note that not all options will be honored by the parent, notably the formset options: - * :attr:`extra` - * :attr:`min_num` - * :attr:`max_num` - - The model form options however, will all be read. - """ - formset_child = PolymorphicFormSetChild - extra = 0 # TODO: currently unused for the children. - - def __init__(self, parent_inline): - self.parent_inline = parent_inline - super(PolymorphicChildInlineModelAdmin, self).__init__(parent_inline.parent_model, parent_inline.admin_site) - - def get_formset(self, request, obj=None, **kwargs): - # The child inline is only used to construct the form, - # and allow to override the form field attributes. - # The formset is created by the parent inline. - raise RuntimeError("The child get_formset() is not used.") - - def get_fields(self, request, obj=None): - if self.fields: - return self.fields - - # Standard Django logic, use the form to determine the fields. - # The form needs to pass through all factory logic so all 'excludes' are set as well. - # Default Django does: form = self.get_formset(request, obj, fields=None).form - # Use 'fields=None' avoids recursion in the field autodetection. - form = self.get_formset_child(request, obj, fields=None).get_form() - return list(form.base_fields) + list(self.get_readonly_fields(request, obj)) - - def get_formset_child(self, request, obj=None, **kwargs): + class Child(InlineModelAdmin): """ - Return the formset child that the parent inline can use to represent us. + The child inline; which allows configuring the admin options + for the child appearance. - :rtype: PolymorphicFormSetChild + 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. """ - # 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)) + formset_child = PolymorphicFormSetChild + extra = 0 # TODO: currently unused for the children. - if self.exclude is None: - exclude = [] - else: - exclude = list(self.exclude) + def __init__(self, parent_inline): + self.parent_inline = parent_inline + super(PolymorphicInlineModelAdmin.Child, self).__init__(parent_inline.parent_model, parent_inline.admin_site) - exclude.extend(self.get_readonly_fields(request, obj)) + 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.") - 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) + def get_fields(self, request, obj=None): + if self.fields: + return self.fields - #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) + # 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)) - # 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) + 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' diff --git a/polymorphic/formsets/__init__.py b/polymorphic/formsets/__init__.py index 4fb4114..079c506 100644 --- a/polymorphic/formsets/__init__.py +++ b/polymorphic/formsets/__init__.py @@ -34,18 +34,18 @@ from .models import ( ) from .generic import ( # Can import generic here, as polymorphic already depends on the 'contenttypes' app. - BasePolymorphicGenericInlineFormSet, - PolymorphicGenericFormSetChild, - polymorphic_generic_inlineformset_factory, + BaseGenericPolymorphicInlineFormSet, + GenericPolymorphicFormSetChild, + generic_polymorphic_inlineformset_factory, ) __all__ = ( 'BasePolymorphicModelFormSet', 'BasePolymorphicInlineFormSet', - 'BasePolymorphicGenericInlineFormSet', 'PolymorphicFormSetChild', - 'PolymorphicGenericFormSetChild', 'polymorphic_inlineformset_factory', - 'polymorphic_generic_inlineformset_factory', 'polymorphic_child_forms_factory', + 'BaseGenericPolymorphicInlineFormSet', + 'GenericPolymorphicFormSetChild', + 'generic_polymorphic_inlineformset_factory', ) diff --git a/polymorphic/formsets/generic.py b/polymorphic/formsets/generic.py index 0872906..2109d82 100644 --- a/polymorphic/formsets/generic.py +++ b/polymorphic/formsets/generic.py @@ -7,14 +7,14 @@ from django.forms.models import ModelForm from .models import BasePolymorphicModelFormSet, polymorphic_child_forms_factory, PolymorphicFormSetChild -class PolymorphicGenericFormSetChild(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(PolymorphicGenericFormSetChild, self).__init__(*args, **kwargs) + super(GenericPolymorphicFormSetChild, self).__init__(*args, **kwargs) def get_form(self, ct_field="content_type", fk_field="object_id", **kwargs): """ @@ -42,17 +42,17 @@ class PolymorphicGenericFormSetChild(PolymorphicFormSetChild): exclude.extend([ct_field.name, fk_field.name]) kwargs['exclude'] = exclude - return super(PolymorphicGenericFormSetChild, self).get_form(**kwargs) + return super(GenericPolymorphicFormSetChild, self).get_form(**kwargs) -class BasePolymorphicGenericInlineFormSet(BaseGenericInlineFormSet, BasePolymorphicModelFormSet): +class BaseGenericPolymorphicInlineFormSet(BaseGenericInlineFormSet, BasePolymorphicModelFormSet): """ Polymorphic formset variation for inline generic formsets """ -def polymorphic_generic_inlineformset_factory(model, formset_children, form=ModelForm, - formset=BasePolymorphicGenericInlineFormSet, +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