This article is part 2 in a 3-part series on creating nested inlines in the Django admin panel.
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.
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.
On the journey to a working nested inline in this article I will touch on the issues of form prefixes, validating a custom field, saving a custom field and form encoding's problem.
If you are want a quick code example click here.
First, we need to handle the form prefixes so that our data is sent in the correct form.
Djangos inline has a
prefix associated with it, each new line gets its own
id, set as follows
prefix-*index*. I thought a reasonable solution for nested-inline would be to handle the prefix of the nested-forms as follows:
In the end it should resemple this stucture:
>ParentPrefix-0 >>ParentPrefix_0_ChildPrefix-0 >>ParentPrefix_0_ChildPrefix-1 >ParentPrefix-1 >>ParentPrefix_1_ChildPrefix-0 >>ParentPrefix_1_ChildPrefix-1
We update the inline-formset prefix with the prefix of the parent form. This can be done in the
image_inline method, but first we need to get the prefix of the parent form from somewhere. We can set the
form attribute in instance inside the
__init__ method of that form itself.
So we get the 'prefix' from the parent form to generate the 'prefix' of the child forms.
Django handles validation of the main form and its own inlines on its own, but validation of nested inlines is not yet supported.
Since validation happens in ModelForm, we need to look there to implement validation for our nested-inlines.
While I was trying different methods, I realized that it would be much easier to implement the nested-inline itself in the form as follows.
We see that inside the
is_valid method calls
Self.nested is an attribute that keeps the link to the nested inline.
- First is created an instance of *Product*ModelAdmin.
- After that, the
_create_formsetsmethod is called. This method returns the corresponding form sets and their instances depending on whether
get_inline_formsetsreturns the nested inlines of that form.
- At the end the prefix is handled and the finished inline is returned.
We set the
request attribute of
ShopModelAdmin in the
render_change_form method before. Now, that's not enough. If we want to have the
request attribute for both GET and POST requests, we need to set the attribute in
changeform_view, because this method is called earlier and is independent of the method of request.
Now that the nested inline is processed in the form, we can shorten the code of the original
Let's wrap the
save_form method of
ShopModelAdmin to also save nested-inlines.
A form calls its save method only when 2 conditions are met.
form.is_valid()must be True.
form.has_changed()must be True. We've already handled the is_valid method. Now we need to change the
has_changedmethod. It should check if our nested data has changed. We also need to specify, which fields have changed so that the form can save the changed data in the model.
Fortunately, we can do everything in one
Of course it should be a on-line generator and not the two for's
Since our nested-inline is defined as a read-only field, the
changed_data property will nominally ignore all changes in nested-inline, so we need to add the modified fields of nested- inline to the other modified fields.
Right now we have a working nested-inline for most Use-cases. But I discovered an exception. If our nested-inline contain any
request.POST processing breaks. It took me a long time to figure out, why it worked with other fields and not with the FileField. The answer lies in the attribute enctype="" of the HTML-form.
There are two types of enctype attribute values for our purposes:
This attribute determines how the browser sends the HTML form data.
application/x-www-form-urlencodedenctype creates a simple long string of name and value pairs. These pairs are separated by
&, and the name and value are separated by
multipart/form-dataenctype define a 'delimeter' in the Conent-Type header. Each pair of values is separated by this boundary formatted as:
--<boundary_value>. Then comes
Content-Disposition: form-data; name="<field_name>". After that, the value of the field is set. After that comes another boundary.
--closes the body of the query.
It is not possible to send files with the
application/x-www-form-urlencoded. This enctype is best used for most web forms with short alphanumeric values.
multipart/form-data, as already mentioned, is used when we want to send files of any type.
But why do we need two arts of enctypes for HTML forms?
The answer lies in the speed and size of the request. The request without data with
application/x-www-form-urlencoded enctype is about 6 times faster and incredibly smaller than the same request in
Why is this important for our nested-inline? If we have a
FileField in a nested-inline, by default the form does not recognize it, because we have nested-inline as a read-only field. This means that the nested-inline will not send the content of the FileField. The other fields will be handled normally.
To fix this, we need to wrap the
is_multiform method of our form so that it also calls the
is_multiform method of the nested inline.
Why not use
multipart/form-data enctype all the time? Yes, you can do that, and it will work. But the query size increases significantly with each field you add to the form. That's just unnecessary overhead.
This implementation of nested inlines is not without its flaws. This is just an example of how to approach creating nested inlines.
With a little more time and determination, it is possible to implement general inline support in the Django-inlines themselves, which would make creating nested-inlines pretty easy.
In this solution, we assume that an inline is already defined in ProductModelAdmin. But that may not be the case. We can add a check to see if ModelAdmin has an inline.
Multiple inlines with multiple nested.inlines are not implemented in this solution. The
nested method returns only the first inline from ProducModelAdmin. This is solvable by returning a list of all ModelAdmin inlines.
As you can see, this is not the final answer to the question of creating nested inlines, but this is the working solution that you can use in your projects.
Saving data is now possible in a nested inline, but you may notice that it still cannot be used in the django admin panel. This is a result of the mess the django developers created in inline.js.
We'll solve this problem in the next article.
Written by Martin Achenrainer
PS: 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 a working environment.