Skip to content

Commit ed8db2d

Browse files
pirannabckohan
authored andcommitted
Auto discover subclasses in polymorphic model admin.
1 parent 94267f3 commit ed8db2d

File tree

4 files changed

+99
-10
lines changed

4 files changed

+99
-10
lines changed

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* Jacob Rief
3737
* James Murty
3838
* Jedediah Smith (proxy models support)
39+
* Jesús Leganés-Combarro (Auto-discover child models and inlines, #582)
3940
* John Furr
4041
* Jonas Haag
4142
* Jonas Obrist

src/polymorphic/admin/helpers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __iter__(self):
6363
for form in self.formset.extra_forms + self.formset.empty_forms:
6464
model = form._meta.model
6565
child_inline = self.opts.get_child_inline_instance(model)
66+
6667
yield PolymorphicInlineAdminForm(
6768
formset=self.formset,
6869
form=form,
@@ -141,3 +142,26 @@ def get_inline_formsets(self, request, formsets, inline_instances, obj=None, *ar
141142
admin_formset.request = request
142143
admin_formset.obj = obj
143144
return inline_admin_formsets
145+
146+
147+
def get_leaf_subclasses(cls, exclude=None):
148+
"Get leaf subclasses of `cls` class"
149+
150+
if exclude is None:
151+
exclude = ()
152+
153+
elif not isinstance(exclude, (list, tuple)):
154+
# Accept single instance in `exclude`
155+
exclude = (exclude,)
156+
157+
result = []
158+
159+
subclasses = cls.__subclasses__()
160+
161+
if subclasses:
162+
for subclass in subclasses:
163+
result.extend(get_leaf_subclasses(subclass, exclude))
164+
elif not (cls in exclude or (hasattr(cls, "_meta") and cls._meta.abstract)):
165+
result.append(cls)
166+
167+
return result

src/polymorphic/admin/inlines.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
)
2121
from polymorphic.formsets.utils import add_media
2222

23-
from .helpers import PolymorphicInlineSupportMixin
23+
from .helpers import PolymorphicInlineSupportMixin, get_leaf_subclasses
2424

2525

2626
class PolymorphicInlineModelAdmin(InlineModelAdmin):
@@ -53,7 +53,14 @@ class PolymorphicInlineModelAdmin(InlineModelAdmin):
5353

5454
#: Inlines for all model sub types that can be displayed in this inline.
5555
#: Each row is a :class:`PolymorphicInlineModelAdmin.Child`
56-
child_inlines = ()
56+
child_inlines = None
57+
58+
#: The models that should be excluded from the auto-discovered leaf
59+
#: model sub types that can be displayed in this inline. This can be
60+
#: a list of models or a single model. It's useful to exclude
61+
#: non-abstract base models (abstract models are always excluded)
62+
#: when they don't have defined any child models.
63+
exclude_children = None
5764

5865
def __init__(self, parent_model, admin_site):
5966
super().__init__(parent_model, admin_site)
@@ -77,12 +84,44 @@ def __init__(self, parent_model, admin_site):
7784
for child_inline in self.child_inline_instances:
7885
self._child_inlines_lookup[child_inline.model] = child_inline
7986

87+
def get_child_inlines(self):
88+
"""
89+
Return the derived inline classes which this admin should handle
90+
91+
This should return a list of tuples, exactly like
92+
:attr:`child_inlines` is.
93+
94+
The inline classes can be retrieved as
95+
``base_inline.__subclasses__()``, a setting in a config file, or
96+
a query of a plugin registration system at your option
97+
"""
98+
if self.child_inlines is not None:
99+
return self.child_inlines
100+
101+
child_inlines = get_leaf_subclasses(
102+
PolymorphicInlineModelAdmin.Child, self.exclude_children
103+
)
104+
child_inlines = tuple(
105+
inline
106+
for inline in child_inlines
107+
if (inline.model is not None and issubclass(inline.model, self.model))
108+
)
109+
110+
if child_inlines:
111+
return child_inlines
112+
113+
raise ImproperlyConfigured(
114+
f"No child inlines found for '{self.model.__name__}', please "
115+
"define the 'child_inlines' attribute or overwrite the "
116+
"'get_child_inlines()' method."
117+
)
118+
80119
def get_child_inline_instances(self):
81120
"""
82121
:rtype List[PolymorphicInlineModelAdmin.Child]
83122
"""
84123
instances = []
85-
for ChildInlineType in self.child_inlines:
124+
for ChildInlineType in self.get_child_inlines():
86125
instances.append(ChildInlineType(parent_inline=self))
87126
return instances
88127

src/polymorphic/admin/parentadmin.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from polymorphic.utils import get_base_polymorphic_model
2020

2121
from .forms import PolymorphicModelChoiceForm
22+
from .helpers import get_leaf_subclasses
2223

2324

2425
class RegistrationClosed(RuntimeError):
@@ -51,6 +52,13 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
5152
#: The child models that should be displayed
5253
child_models = None
5354

55+
#: The models that should be excluded from the auto-discovered child
56+
#: leaf models that should be displayed. This can be a list of
57+
#: models or a single model. It's useful to exclude non-abstract
58+
#: base models (abstract models are always excluded) when they don't
59+
#: have defined any child models.
60+
exclude_children = None
61+
5462
#: Whether the list should be polymorphic too, leave to ``False`` to optimize
5563
polymorphic_list = False
5664

@@ -109,24 +117,41 @@ def register_child(self, model, model_admin):
109117
def get_child_models(self):
110118
"""
111119
Return the derived model classes which this admin should handle.
112-
This should return a list of tuples, exactly like :attr:`child_models` is.
113120
114-
The model classes can be retrieved as ``base_model.__subclasses__()``,
115-
a setting in a config file, or a query of a plugin registration system at your option
121+
This should return a list of tuples, exactly like
122+
:attr:`child_models` is.
123+
124+
The model classes can be retrieved as
125+
``base_model.__subclasses__()``, a setting in a config file, or
126+
a query of a plugin registration system at your option
116127
"""
117-
if self.child_models is None:
118-
raise NotImplementedError("Implement get_child_models() or child_models")
128+
if self.child_models is not None:
129+
return self.child_models
119130

120-
return self.child_models
131+
child_models = get_leaf_subclasses(self.base_model, self.exclude_children)
132+
133+
if child_models:
134+
return child_models
135+
136+
raise ImproperlyConfigured(
137+
f"No child models found for '{self.base_model.__name__}', please "
138+
"define the 'child_models' attribute or overwrite the "
139+
"'get_child_models' method."
140+
)
121141

122142
def get_child_type_choices(self, request, action):
123143
"""
124144
Return a list of polymorphic types for which the user has the permission to perform the given action.
125145
"""
126146
self._lazy_setup()
147+
148+
child_models = self._child_models
149+
if not child_models:
150+
raise ImproperlyConfigured("No child models are available.")
151+
127152
choices = []
128153
content_types = ContentType.objects.get_for_models(
129-
*self.get_child_models(), for_concrete_models=False
154+
*child_models, for_concrete_models=False
130155
)
131156

132157
for model, ct in content_types.items():

0 commit comments

Comments
 (0)