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.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.
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.
Form prefixes
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:
parentPrefix_parentIndex_childPrefix-childIndex
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.
Data validation
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.formset.is_valid()
. 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_formsets
method is called. This method returns the corresponding form sets and their instances depending on whetherchange_view
oradd_view
was requested. - After
get_inline_formsets
returns 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 image_inline
method.
save method
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 thehas_changed
method. 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 changed_data
property.
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 FileField
, the 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.
Form encoding
There are two types of enctype attribute values for our purposes:
- "application/x-www-form-urlencoded"
- "multipart/form-data"
This attribute determines how the browser sends the HTML form data.
The
application/x-www-form-urlencoded
enctype creates a simple long string of name and value pairs. These pairs are separated by&
, and the name and value are separated by=
.The
multipart/form-data
enctype define a 'delimeter' in the Conent-Type header. Each pair of values is separated by this boundary formatted as:--<boundary_value>
. Then comesContent-Disposition: form-data; name="<field_name>"
. After that, the value of the field is set. After that comes another boundary.
Final--
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.
The 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 multipart/form-data
enctype.
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.
For more information on the encoding of HTML forms I would recommend this article about it and this discussion on Stackoverflow.
Limitations
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.
Next Article
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.
Top comments (0)