Add and remove Django-Admin Inlines with JavaScript

The built-in Django Admin-Interface (django.contrib.admin) has a nifty little feature called Inlines or InlineModelAdmins. With Inlines you can edit multiple related objects right on the bottom of the page of the parent object. Looking at the official Django tutorial a Poll-object can have multiple Choice-objects which can then be edited inline with the Poll-object. I'm not going deeper into this now and assume that you are familiar with Inlines.

Current State

If you are using a lot of Inlines on one page it can quickly get messy because a setting of extra=3 means that with for example 3 different Inlines you get at least 9 empty Inline forms when loading the page. And if you want to add 5 objects to one of them you first have to add three, click save, wait till the page refreshes and start adding the last 2 and save again.

Adding a bit JavaScript

Trying to solve this I have written a bit of JavaScript. It's not a complete solution but it works for me (TM) so I'm going to share it here hoping that someone else may find it useful of might improve it further.

First let't handle removing/deleting of already saved Inlines: Every Inline that's present when you load the page has a tiny checkbox on the top-right corner labeled "delete", which will - if checked - delete the Inline once you click save. My naive approach to make this more usable is to just collapse the Inline once it's marked for deletion. The code thas has to be added to the page is just a few lines but uses jQuery:

$(function(){
    $('.inline-group .inline-related .delete input').each(function(i,e){
        $(e).bind('change', function(e){
            if(this.checked) {
                // marked for deletion
                $(this).parents('.inline-related').children('fieldset.module').addClass('collapsed collapse')
            }else{
                $(this).parents('.inline-related').children('fieldset.module').removeClass('collapsed')
            }
        })
    })
})

The code reuses CSS classes already present in the Admin CSS for collapsing fieldsets.

Now let's dynamically add more Inline objects with JavaScript: Adding new Inline forms involves a bit more steps, first we need a button which let's the user add a new form. Looking into the django/contrib/admin/templates/admin/edit_inline/stacked.html template you can see that this button is already included but commented out:

{# <ul class="tools"> #}
{#   <li><a class="add" href="">Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li> #}
{# </ul> #}

This leaves us with two good solutions: Either add a template to out InlineModelAdmin which overwrites the default inline template and has this Link included or write some JavaScript code to dynamically add this Button at runtime. For simplicity I used the template approach.

Additionally I've added a target to the link. The modified lines in my Inline template (which is essentially just a copy of the default stacked-inline template above) look like this:

<ul class="tools">
   <li><a class="add" href="#"
        onclick="return add_inline_form('{{ inline_admin_formset.formset.management_form.prefix }}')">
        Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li>
</ul>

Clicking this link will call a (yet to be defined) JavaScript function called add_inline_form which takes the prefix of the Inline formset as a parameter, this is needed to calculate some values.

The following JS code has to be added to the page:

function increment_form_ids(el, to, name) {
    var from = to-1
    $(':input', $(el)).each(function(i,e){
        var old_name = $(e).attr('name')
        var old_id = $(e).attr('id')
        $(e).attr('name', old_name.replace(from, to))
        $(e).attr('id', old_id.replace(from, to))
        $(e).val('')
    })
}


function add_inline_form(name) {
    var first = $('#id_'+name+'-0-id').parents('.inline-related')
    var last = $(first).parent().children('.last-related')
    var copy = $(last).clone(true)
    var count = $(first).parent().children('.inline-related').length
    $(last).removeClass('last-related');
    $(last).after(copy);
    $('input#id_'+name+'-TOTAL_FORMS').val(count+1)
    increment_form_ids($(first).parents('.inline-group').children('.last-related'), count, name);
    return false;
}

This code takes care of adding a new Inline form (by cloning the last one and resetting values) and incrementing id and name attributes on the new form and incrementing the counter of how many inline forms for the Inline exist (which is stored in a hidden form-field).

Conslusion

I am able to add Inline forms on the fly and collapse Inlines which are marked for deletion, which both are huge usability improvements for the enduser using the page to edit data. The code above is at most a prototype to show that this is possible and may break at any point. I've only tested it with pretty simple Inline models (containing CharFields). If you have ideas for improvements feel free to take the code and do whatever you want with it.

Sidenote: Also check out Simon's sort snippet to sort Inlines dynamically using drag-and-drop.


Kommentare