This article expands on the ideas of my previous articles:
Django admin dynamic Inline positioning and How to solve the singleton problem in Django ModelAdmin.
This is also the first part of a three-part series on how to get Nested Inlines in the Django admin panel.
Django Nested Inline. Rendering.
This article is about rendering a Nested Inline. At the end we should see a Nested Inline in our ModelAdmin. But this Nested Inline can not save own data yet.Django nested inline. Save data.
The second article shows integration of nested inlines in the ModelAdmin to make saving data possible.Django nested inline. Javascript.
The final article in the series explains how to override the inlines.js in Django package to make it possible to add/remove Nested Inlines on fly in the ModelAdmin.
Although you will hopefully not need to implement nested-inlines at present, you can learn a thing or two about Django.
If you are want a quick code example click here.
Introspection of existed Nested Inline packages
The direct way to get Nested Inlines in Django Admin would be to install the package, since surely someone has already solved this problem, right?
After a quick search, you come across Django-Nested-Inline and think you're done.
Unfortunately, this solution has some problems and does not work properly.
For example, it doesn't allow synchronous editing of objects at the same time. I mean - if several people are working with your Admin Panel at the same time, there will be unexplainable errors all the time. The probability of such errors grows exponentially with each manager and each nested Inline.
So we can try to create our own Nested Inline. It's not complicated.
Basic Nested Inline rendering
First create a shop model, a product model and an image model. The product model must relate to the shop model, and the image model must relate to the product model.
Something like this:
Now we create some basic ModelAdmins with Inlines, like this:
Let's make some changes to ProductModelAdmin and ShopModelAdmin. We need to set the request and response as attributes for both ModelAdmin.
Experienced Django developers may notice that using a ModelAdmin instance as a container is not threadsave, and they would be correct if it were the default admin panel.
Here I refer them back to my previous article to explain how this works.
This is one of the cornerstones of how to create nested Inlines.
Additionally we should override the get_inline_instances
method of ShopModelAdmin
to set the modeladmin
inline attribute to the current admin instance, this will be useful later.
I have made this function as generator, it calculated only when the result used for iteration.
We can now take the idea of dynamic inline positioning and develop it further. If it's possible to use inline in ModelAdmin
as a field, it should be possible to do the same in inline itself, so let's copy the ProductModelAdmin
code from that article.
But not everything works the same as in the ModelAdmin
.
First, we can not get the context from the self.response
attribute because the inline does not have that attribute.
But the Inline
has the attribute modeladmin
because we defined it in get_inline_instances
. inline.modeladmin
is the ModelAdmin
instance. That has a request
and a response
attribute. So we can get the context from there.
context = getattr(self.modeladmin.response, 'context_data', None) or {}
Secondly, Context does not contain the Information about Nested inline. With the code above we want to display a nested inline in the field of the current inline. We can call the view method for ProductModelAdmin and extract a nested inline from response. There is inline that must be nested.
admin_response = ProductModelAdmin(self.model, self.modeladmin.admin_site).add_view(self.modeladmin.request)
inline = admin_response.context_data['inline_admin_formsets'][0]
Although it should be noted that this is not the final version: in reality we need to call the appropriate ModelAdmin
view as requested, rather than just add_view
.
Thirdly, we need to modify the args we pass to the render
method so, that they contain an updated context that includes our extracted nested inline, and request.
return get_template(inline.opts.template).render(context | {'inline_admin_formset': inline}, self.modeladmin.request)
In this construction we don't change context variable directly. The update operator ('|') creates a copy of the dictionary and updates it with the new values.
The final method should look like this:
If we start our project and try add a new shop model we see our nested inline.
As you can see, our nested inline is displayed. Yay! But you might also notice that there are some problems rendering the inlines. This is because Djangos inlines.js for inlines is a complete mess and needs to be refactored. My issue about it in djangoproject was rejected as "ivalid".
Fortunately, I will show, how to do this in last article of this series.
At the moment, this display error can be temporarily fixed by changing one line of code in the django\contrib\admin\static\admin\js\inlines.js file.
// row 335:
$(selector).stackedFormset(selector, inlineOptions.options);
// change to:
$(this).find("[id^=" + inlineOptions.name.substring(1) + "-]").stackedFormset(selector, inlineOptions.options);
Not it works better than before.
Now it's not perfect, and the rendering problem still exists.
Next Article
This article was only about getting the nested inline before the first render, without validation and saving data of nested Inline. In the next article I will show how to save the data properly.
This article war Written by Martin Achenrainer. I thank Martin, for translating and assisting with this English article. This idea was first presented in September 2021 at PyCon RU
Some words from Martin:
I would like to thank Maxim Danilov for his extensive help on this series of articles, he was the inspiration and led me to this clever solution. I would also like to thank him and his company wP Soft for giving me the opportunity to develop and improve my programming skills in learning by doing.
Top comments (2)
Hello,
This code works great!
there is just one bug, when I put the "extra" attribute in the Inlines to 0,
the addInlineclickHandlers don't function anymore. In this case, clicking on "Add Another" doesn't do anything.
I'm going to try and fix this in the inlines.js myself and post a fix here if I can find one.
If you guys have a quick solution to this I'd be glad to hear from you.
Thank you. Can you add an issue about this bug? I try to improve it.