diff --git a/polymorphic/admin/helpers.py b/polymorphic/admin/helpers.py index 6ed6a109..aa991613 100644 --- a/polymorphic/admin/helpers.py +++ b/polymorphic/admin/helpers.py @@ -63,6 +63,7 @@ def __iter__(self): 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, @@ -141,3 +142,22 @@ def get_inline_formsets(self, request, formsets, inline_instances, obj=None, *ar admin_formset.request = request admin_formset.obj = obj return inline_admin_formsets + + +def get_leaf_subclasses(cls, exclude=None): + "Get leaf subclasses of `cls` class" + + result = [] + + subclasses = cls.__subclasses__() + + if subclasses: + for subclass in subclasses: + result.extend(get_leaf_subclasses(subclass, exclude)) + elif not ( + (hasattr(cls, '_meta') and cls._meta.abstract) or + (exclude and cls in exclude) + ): + result.append(cls) + + return result diff --git a/polymorphic/admin/inlines.py b/polymorphic/admin/inlines.py index 00fd29d2..6f76011c 100644 --- a/polymorphic/admin/inlines.py +++ b/polymorphic/admin/inlines.py @@ -20,7 +20,7 @@ ) from polymorphic.formsets.utils import add_media -from .helpers import PolymorphicInlineSupportMixin +from .helpers import PolymorphicInlineSupportMixin, get_leaf_subclasses class PolymorphicInlineModelAdmin(InlineModelAdmin): @@ -53,7 +53,8 @@ class PolymorphicInlineModelAdmin(InlineModelAdmin): #: Inlines for all model sub types that can be displayed in this inline. #: Each row is a :class:`PolymorphicInlineModelAdmin.Child` - child_inlines = () + child_inlines = None + exclude_childs = None def __init__(self, parent_model, admin_site): super().__init__(parent_model, admin_site) @@ -77,12 +78,47 @@ def __init__(self, parent_model, admin_site): for child_inline in self.child_inline_instances: self._child_inlines_lookup[child_inline.model] = child_inline + def get_child_inlines(self): + """ + Return the derived inline classes which this admin should handle + + This should return a list of tuples, exactly like + :attr:`child_inlines` is. + + The inline classes can be retrieved as + ``base_inline.__subclasses__()``, a setting in a config file, or + a query of a plugin registration system at your option + """ + if self.child_inlines is not None: + return self.child_inlines + + child_inlines = get_leaf_subclasses( + PolymorphicInlineModelAdmin.Child, self.exclude_childs + ) + child_inlines = tuple( + inline + for inline in child_inlines + if ( + inline.model is not None and + issubclass(inline.model, self.model) + ) + ) + + if child_inlines: + return child_inlines + + raise ImproperlyConfigured( + f"No child inlines found for '{self.model.__name__}', please " + "define the 'child_inlines' attribute or overwrite the " + "'get_child_inlines()' method." + ) + def get_child_inline_instances(self): """ :rtype List[PolymorphicInlineModelAdmin.Child] """ instances = [] - for ChildInlineType in self.child_inlines: + for ChildInlineType in self.get_child_inlines(): instances.append(ChildInlineType(parent_inline=self)) return instances diff --git a/polymorphic/admin/parentadmin.py b/polymorphic/admin/parentadmin.py index 98ec5093..9dd96a66 100644 --- a/polymorphic/admin/parentadmin.py +++ b/polymorphic/admin/parentadmin.py @@ -19,6 +19,7 @@ from polymorphic.utils import get_base_polymorphic_model from .forms import PolymorphicModelChoiceForm +from .helpers import get_leaf_subclasses class RegistrationClosed(RuntimeError): @@ -50,6 +51,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): #: The child models that should be displayed child_models = None + exclude_childs = None #: Whether the list should be polymorphic too, leave to ``False`` to optimize polymorphic_list = False @@ -109,24 +111,43 @@ def register_child(self, model, model_admin): def get_child_models(self): """ Return the derived model classes which this admin should handle. - This should return a list of tuples, exactly like :attr:`child_models` is. - The model classes can be retrieved as ``base_model.__subclasses__()``, - a setting in a config file, or a query of a plugin registration system at your option + This should return a list of tuples, exactly like + :attr:`child_models` is. + + The model classes can be retrieved as + ``base_model.__subclasses__()``, a setting in a config file, or + a query of a plugin registration system at your option """ - if self.child_models is None: - raise NotImplementedError("Implement get_child_models() or child_models") + if self.child_models is not None: + return self.child_models + + child_models = get_leaf_subclasses( + self.base_model, self.exclude_childs + ) - return self.child_models + if child_models: + return child_models + + raise ImproperlyConfigured( + f"No child models found for '{self.base_model.__name__}', please " + "define the 'child_models' attribute or overwrite the " + "'get_child_models' method." + ) def get_child_type_choices(self, request, action): """ Return a list of polymorphic types for which the user has the permission to perform the given action. """ self._lazy_setup() + + child_models = self._child_models + if not child_models: + raise ImproperlyConfigured("No child models are available.") + choices = [] content_types = ContentType.objects.get_for_models( - *self.get_child_models(), for_concrete_models=False + *child_models, for_concrete_models=False ) for model, ct in content_types.items():