Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f075aab
Raise proper exception if no child models are available
piranna Feb 16, 2024
d9644fa
Merge branch 'jazzband:master' into master
piranna Feb 21, 2024
d12eccf
Auto-discover child models by default
piranna Feb 29, 2024
308f1d4
Auto-discover child inlines by default
piranna Feb 29, 2024
4830b30
Merge remote-tracking branch 'jazzband/master'
piranna Mar 4, 2024
f8a9302
Fix string template
piranna Mar 5, 2024
91b0a47
Merge remote-tracking branch 'jazzband/master'
piranna Mar 27, 2024
87e4b95
Fix string template
piranna Mar 27, 2024
0de8744
Adjust DocStrings to pep8
piranna Mar 27, 2024
fa69bf7
Merge branch 'jazzband:master' into master
piranna Apr 4, 2024
8da75ce
Merge remote-tracking branch 'jazzband/master'
piranna Apr 24, 2024
ada2ef3
Merge remote-tracking branch 'jazzband/master'
piranna May 17, 2024
b4db7e0
Merge remote-tracking branch 'jazzband/master'
piranna May 18, 2024
c97c5c6
Use already initialized `_child_models`
piranna May 19, 2024
7a0f54c
Allow to exclude child subclasses
piranna May 19, 2024
4136bd6
Merge remote-tracking branch 'jazzband/master'
piranna May 19, 2024
d6b2a88
Merge remote-tracking branch 'jazzband/master'
piranna Jun 17, 2024
aaa7fa3
Don't consider `abstract` Models as leaf subclasses
piranna Jun 17, 2024
0f5f302
Replace `childs` for `children`
piranna Sep 2, 2024
b6cac5e
Accept single instance in `exclude`
piranna Sep 2, 2024
49be2ef
Added documentation
piranna Sep 2, 2024
39f1d79
Fix ruff formatting
piranna Sep 2, 2024
515885d
Remove deprecated warning on `ruff` linting command
piranna Sep 2, 2024
b56b3cd
Add `Jesús Leganés-Combarro` to `AUTHORS.rst` file
piranna Sep 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Contributors
* Jacob Rief
* James Murty
* Jedediah Smith (proxy models support)
* Jesús Leganés-Combarro (Auto-discover child models and inlines, #582)
* John Furr
* Jonas Haag
* Jonas Obrist
Expand Down
24 changes: 24 additions & 0 deletions polymorphic/admin/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -141,3 +142,26 @@ 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"

Comment on lines +148 to +149
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is missing proper documentation including parameter descriptions and return type. Following the codebase convention, add a proper docstring with parameter descriptions, return type (:rtype: list), and explanation of behavior (e.g., "Recursively finds all leaf subclasses of cls that are not abstract and not in the exclude list.").

Suggested change
"Get leaf subclasses of `cls` class"
"""
Recursively finds all leaf subclasses of `cls` that are not abstract and not in the exclude list.
:param type cls: The base class to find leaf subclasses for.
:param exclude: A class or iterable of classes to exclude from the results.
:type exclude: type or iterable of types, optional
:rtype: list
:return: A list of leaf subclasses of `cls` that are not abstract and not in the exclude list.
"""

Copilot uses AI. Check for mistakes.
if exclude is None:
Comment on lines +148 to +150
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring is missing proper punctuation and should follow the project's docstring style. It should end with a period and provide more detail about the function's behavior, parameters, and return value.

Suggested change
"Get leaf subclasses of `cls` class"
if exclude is None:
"""
Return a list of all leaf subclasses of the given class.
A leaf subclass is a subclass that does not have any further subclasses.
Args:
cls: The base class to search for leaf subclasses.
exclude: An optional class, list, or tuple of classes to exclude from the results.
Returns:
list: A list of leaf subclasses of `cls`, excluding any specified in `exclude`.
"""

Copilot uses AI. Check for mistakes.
exclude = ()

elif not isinstance(exclude, (list, tuple)):
# Accept single instance in `exclude`
exclude = (exclude,)

result = []

subclasses = cls.__subclasses__()

if subclasses:
for subclass in subclasses:
result.extend(get_leaf_subclasses(subclass, exclude))
Comment on lines +159 to +163
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __subclasses__() method only returns direct subclasses, not all descendants. This is correct for a recursive implementation, but be aware that this will only work for classes that have been imported/defined at the time this function is called. If child classes are defined in modules that haven't been imported yet, they won't be discovered. Consider documenting this limitation or ensuring all relevant modules are imported before auto-discovery.

Copilot uses AI. Check for mistakes.
elif not (cls in exclude or (hasattr(cls, "_meta") and cls._meta.abstract)):
result.append(cls)

return result
45 changes: 42 additions & 3 deletions polymorphic/admin/inlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -53,7 +53,14 @@ 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

#: The models that should be excluded from the auto-discovered leaf
#: model sub types that can be displayed in this inline. This can be
#: a list of models or a single model. It's useful to exclude
#: non-abstract base models (abstract models are always excluded)
#: when they don't have defined any child models.
exclude_children = None

def __init__(self, parent_model, admin_site):
super().__init__(parent_model, admin_site)
Expand All @@ -77,12 +84,44 @@ 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
Comment on lines +89 to +96
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring should be updated to mention the new auto-discovery behavior and the exclude_children parameter. Currently, it doesn't explain what happens when child_inlines is None or how the auto-discovery works.

Suggested change
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
Return the derived inline classes which this admin should handle.
If the :attr:`child_inlines` attribute is set, this method returns its value.
Otherwise, it auto-discovers all leaf subclasses of
:class:`PolymorphicInlineModelAdmin.Child` whose model is a subclass of
this inline's model. The auto-discovery process excludes any models specified
in the :attr:`exclude_children` attribute.
Returns:
tuple: A tuple of inline classes to be used as child inlines.
Raises:
ImproperlyConfigured: If no child inlines are found.

Copilot uses AI. Check for mistakes.
"""
if self.child_inlines is not None:
return self.child_inlines

child_inlines = get_leaf_subclasses(
PolymorphicInlineModelAdmin.Child, self.exclude_children
)
child_inlines = tuple(
inline
for inline in child_inlines
if (inline.model is not None and issubclass(inline.model, self.model))
)
Comment on lines +101 to +108
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auto-discovery searches for ALL leaf subclasses of PolymorphicInlineModelAdmin.Child across the entire application, then filters by model. This means if you have multiple polymorphic inline configurations, each will scan all Child subclasses application-wide. While the filtering by issubclass(inline.model, self.model) ensures only relevant inlines are used, this approach may be inefficient and could lead to confusion if developers define Child classes that they don't intend to be auto-discovered. Consider documenting this behavior clearly or providing guidance on organizing Child classes to avoid unintended discovery.

Copilot uses AI. Check for mistakes.

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."
)
Comment on lines +98 to +117
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new auto-discovery behavior for child inlines lacks test coverage. Consider adding tests that verify:

  1. Child inlines are correctly auto-discovered when child_inlines is None
  2. The exclude_children parameter works correctly
  3. An ImproperlyConfigured exception is raised when no child inlines are found
  4. Only inlines that are subclasses of the current inline's model are included

Copilot uses AI. Check for mistakes.

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

Expand Down
39 changes: 32 additions & 7 deletions polymorphic/admin/parentadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -51,6 +52,13 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
#: The child models that should be displayed
child_models = None

#: The models that should be excluded from the auto-discovered child
#: leaf models that should be displayed. This can be a list of
#: models or a single model. It's useful to exclude non-abstract
#: base models (abstract models are always excluded) when they don't
#: have defined any child models.
exclude_children = None

#: Whether the list should be polymorphic too, leave to ``False`` to optimize
polymorphic_list = False

Expand Down Expand Up @@ -109,24 +117,41 @@ 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
Comment on lines +121 to +126
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring should be updated to mention the new auto-discovery behavior and the exclude_children parameter. Currently, it doesn't explain what happens when child_models is None or how the auto-discovery works.

Suggested change
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 the :attr:`child_models` attribute is set, its value is returned.
If :attr:`child_models` is None (the default), this method will
automatically discover all concrete (non-abstract) leaf subclasses
of the base model using :func:`get_leaf_subclasses`.
The :attr:`exclude_children` attribute can be set to a model or a list
of models to exclude from the auto-discovered child models. Abstract
models are always excluded automatically.
Returns:
list: A list of child model classes (or tuples, as in :attr:`child_models`).
Raises:
ImproperlyConfigured: If no child models are found.

Copilot uses AI. Check for mistakes.
"""
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

return self.child_models
child_models = get_leaf_subclasses(self.base_model, self.exclude_children)

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."
)
Comment on lines +128 to +140
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new auto-discovery behavior for child models lacks test coverage. Consider adding tests that verify:

  1. Child models are correctly auto-discovered when child_models is None
  2. The exclude_children parameter works correctly
  3. An ImproperlyConfigured exception is raised when no child models are found
  4. Abstract models are properly excluded from auto-discovery

Copilot uses AI. Check for mistakes.

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():
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ requires = [
[tool.ruff]
line-length = 99

[tool.ruff.lint]
extend-ignore = [
"E501",
]
Expand All @@ -18,7 +19,7 @@ select = [
"W",
]

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"example/**" = [
"F401",
"F403",
Expand Down