Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-discover child models and inlines #582

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
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"

if exclude is None:
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))
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`
piranna marked this conversation as resolved.
Show resolved Hide resolved
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
"""
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))
)

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

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

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