DEV Community 👩‍💻👨‍💻

Cover image for Django Nested Inline. Save Data.
Maxim Danilov
Maxim Danilov

Posted on

Django Nested Inline. Save Data.

This article is part 2 in a 3-part series on creating nested inlines in the Django admin panel.

  1. 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.

  2. Django nested inline. Save data.
    The second article shows integration of Nested-inlines in the ModelAdmin to make saving data possible.

  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 whether change_view or add_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 the has_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 comes Content-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)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.