9

I have functionality where i need to implement nested django forms with the below models

class Publisher(models.Model):
    name = models.CharField(max_length=256)
    address1 = models.CharField(max_length=256)
    address2 = models.CharField(max_length=256)
    city = models.CharField(max_length=256)

class Author(models.Model):
    publisher = models.ForeignKey(Publisher) 
    name = models.CharField(max_length=256)
    address = models.CharField(max_length=256)

class Book(models.Model):
    author = models.ForeignKey(Author)
    name = models.CharField(max_length=256)
    price = models.FloatField()

forms.py

class PublisherForm(ModelForm):
    class Meta:
        model = Publisher

    def __init__(self, *args, **kwargs):

        super(PublisherForm, self).__init__(*args, **kwargs)
        self.fields['name'].widget.attrs = {'id':'inputIcon', 'class':'input-block', 'placeholder':'Publisher Name', 'autofocus':'autofocus'}
        self.fields['address'].widget.attrs = {'id':'inputIcon', 'class':'input-block', 'placeholder':'Publisher Address '}


class AuthorForm(ModelForm):
    class Meta:
        model = Author
        exclude = ('publisher',)    

    def __init__(self, *args, **kwargs):

        super(AuthorForm, self).__init__(*args, **kwargs)
        self.fields['name'].widget.attrs = {'id':'inputIcon', 'class':'input-block', 'placeholder':'Author Name'}
        self.fields['address'].widget.attrs = {'id':'inputIcon', 'class':'input-block', 'placeholder':'Author Address'}

class BookForm(ModelForm):
    class Meta:
        model = Book
        exclude = ('author',)    

    def __init__(self, *args, **kwargs):

        super(BookForm, self).__init__(*args, **kwargs)
        self.fields['name'].widget.attrs = {'id':'inputIcon', 'class':'input-block', 'placeholder':'Book Name'}
        self.fields['price'].widget.attrs = {'id':'inputIcon', 'class':'input-block', 'placeholder':'Book Price'}

So with the above models and forms, i need to create the forms dynamically on the same screen like in the UI screen below

enter image description here

So from the above screen, we can observe all the three model forms should displayon the same page.

1. The publisher may have many authors
2. Each author may have many books

Also you can observe from the design, we have two button for

1.Add Another Author -  Adding Multiple Authors
2.Add Book - Adding multiple books for Author

2. Add Book

When we click on Add Book, a new Book form should be created as in the screenshot

1. Add another Author

When we click on Add another author button a new Author record should be displayed and he can able to add multiple Books for this author same as above by clicking on Add Book

If we have only two models A and B, and if B has ForeignKey to A, then we could be able to achive this functionlaity by usign django formsets or inline_formsets or model_formsets , but here in the above we should be able to

  1. Add nested(multiple) Book forms for Author
  2. Add nested(multiple) Author forms for Publisher

So how to achieve the above functionality ?, i have searched a lot, but could n't able to figure out the above stuff

Shiva Krishna Bavandla
  • 25,548
  • 75
  • 193
  • 313

1 Answers1

10

This can be done by playing with inline formsets, in the view of create a publisher, returns the authors and books formsets (using differente prefix parameters for each forms), then use javascript to add new forms empty forms for books and authors.

Bellow is a basic sample I coded for you.

The trick is to use javascript to generate book formsets in templates with dynamic form prefixes related to the parent author (books_formset_0, books_formset_1, ...), then on sumbit the form, iterate for each author to find the related book_formset.

A complete django project to run and test this code can be downloaded here.

IMPORTANT: The following code hasn't been optimized and not use some standards tools like js templates, ajax, etc, but it works and shows how to solve the problem.

template.py:

<script type="text/javascript" src="{{ STATIC_URL }}js/jquery.js"></script>
<script type="text/javascript">
    $(function () {
        $('form').delegate('.btn_add_book', 'click', function () {
            var $this = $(this)
            var author_ptr = $this.attr('id').split('-')[1]
            var $total_author_books = $(':input[name=books_formset_' + author_ptr + '-TOTAL_FORMS]');
            var author_book_form_count = parseInt($total_author_books.val())
            $total_author_books.val(author_book_form_count + 1)

            var $new_book_form = $('<fieldset class="author_book_form">' +
                '<legend>Book</legend>' +
                '<p>' +
                '<label for="id_books_formset_' + author_ptr + '-' + author_book_form_count + '-name">Name:</label>' +
                '<input id="id_books_formset_' + author_ptr + '-' + author_book_form_count + '-name" maxlength="256" name="books_formset_' + author_ptr + '-' + author_book_form_count + '-name" type="text" />' +
                '<input id="id_books_formset_' + author_ptr + '-' + author_book_form_count + '-author" name="books_formset_' + author_ptr + '-' + author_book_form_count + '-author" type="hidden" />' +
                '<input id="id_books_formset_' + author_ptr + '-' + author_book_form_count + '-id" name="books_formset_' + author_ptr + '-' + author_book_form_count + '-id" type="hidden" />' +
                '</p>' +
                '</fieldset>'
            )

            $this.parents('.author_form').find('.author_books').prepend($new_book_form)
        })

        $('form').delegate('#btn_add_author', 'click', function () {
            var $total_authors = $(':input[name=authors_formset-TOTAL_FORMS]');
            author_form_count = parseInt($total_authors.val())
            $total_authors.val(author_form_count + 1)

            book_form = '<fieldset class="author_book_form">' +
                '<legend>Book</legend>' +
                '<p>' +
                '<label for="id_books_formset_' + author_form_count + '-0-name">Name:</label>' +
                '<input id="id_books_formset_' + author_form_count + '-0-name" maxlength="256" name="books_formset_' + author_form_count + '-0-name" type="text" />' +
                '<input id="id_books_formset_' + author_form_count + '-0-author" name="books_formset_' + author_form_count + '-0-author" type="hidden" />' +
                '<input id="id_books_formset_' + author_form_count + '-0-id" name="books_formset_' + author_form_count + '-0-id" type="hidden" />' +
                '</p>' +
                '</fieldset>';

            $new_author_form = $(
                '<fieldset class="author_form">' +
                '<legend>Author</legend>' +
                '<p>' +
                '<label for="id_authors_formset-' + author_form_count + '-name">Name:</label>' +
                '<input id="id_authors_formset-' + author_form_count + '-name" maxlength="256" name="authors_formset-' + author_form_count + '-name" type="text" />' +
                '<input id="id_authors_formset-' + author_form_count + '-publisher" name="authors_formset-' + author_form_count + '-publisher" type="hidden" />' +
                '<input id="id_authors_formset-' + author_form_count + '-id" name="authors_formset-' + author_form_count + '-id" type="hidden" />' +
                '</p>' +
                '<p><input type="button" value="Add Book" class="btn_add_book" id="author-' + author_form_count + '"/></p>' +
                '<div class="author_books">' +
                '<input id="id_books_formset_' + author_form_count + '-TOTAL_FORMS" name="books_formset_' + author_form_count + '-TOTAL_FORMS" type="hidden" value="1" />' +
                '<input id="id_books_formset_' + author_form_count + '-INITIAL_FORMS" name="books_formset_' + author_form_count + '-INITIAL_FORMS" type="hidden" value="0" />' +
                '<input id="id_books_formset_' + author_form_count + '-MAX_NUM_FORMS" name="books_formset_' + author_form_count + '-MAX_NUM_FORMS" type="hidden" value="1000" />' +
                book_form +
                '</div >' +
                '</fieldset >'
            )

            $('#authors').prepend($new_author_form)
        })
    })
</script>
<h1>Add Publisher</h1>
<form action="" method="post">
    {% csrf_token %}
    {{ form.as_p }}

    <p><input type="button" id="btn_add_author" value="Add another author"/></p>

    <div id="authors">
        {{ authors_formset.management_form }}
        {% for form in authors_formset %}
            <fieldset class="author_form">
                <legend>Author</legend>
                {{ form.as_p }}
                <p><input type="button" value="Add Book" class="btn_add_book" id="author-{{ forloop.counter0 }}"/></p>

                <div class="author_books">
                    {{ books_formset.management_form }}
                    {% for form in books_formset %}
                        <fieldset class="author_book_form">
                            <legend>Book</legend>
                            {{ form.as_p }}
                        </fieldset>
                    {% endfor %}
                </div>
            </fieldset>
        {% endfor %}
    </div>
    <p><input type="submit" value="Save"></p>
</form>

forms.py:

AuthorInlineFormSet = inlineformset_factory(Publisher, Author, extra=1, can_delete=False)
BookInlineFormSet = inlineformset_factory(Author, Book, extra=1, can_delete=False)

views.py:

class PublisherCreateView(CreateView):
    model = Publisher

    def form_valid(self, form):
        result = super(PublisherCreateView, self).form_valid(form)

        authors_formset = AuthorInlineFormSet(form.data, instance=self.object, prefix='authors_formset')
        if authors_formset.is_valid():
            authors = authors_formset.save()

        authors_count = 0
        for author in authors:
            books_formset = BookInlineFormSet(form.data, instance=author, prefix='books_formset_%s' % authors_count)
            if books_formset.is_valid():
                books_formset.save()
            authors_count += 1

        return result

    def get_context_data(self, **kwargs):
        context = super(PublisherCreateView, self).get_context_data(**kwargs)
        context['authors_formset'] = AuthorInlineFormSet(prefix='authors_formset')
        context['books_formset'] = BookInlineFormSet(prefix='books_formset_0')
        return context
Noelle L.
  • 100
  • 6
juliocesar
  • 5,706
  • 8
  • 44
  • 63
  • can u please provide me an example, so that it will be useful for other users too ? – Shiva Krishna Bavandla Nov 26 '13 at 17:52
  • Hey juliocesar, extremely thanks, and so sweet of you, i think you have saved my day, i will go through the above code and will comment for any doubts :) – Shiva Krishna Bavandla Nov 27 '13 at 05:50
  • Also can we do it with forms instead of models ? because had some design templates where we need to have some custom classes and ids for fields ? – Shiva Krishna Bavandla Nov 27 '13 at 07:03
  • So can we do the above functionality from normal `formsets` using `Forms` that i have customized above ? , because i need to add the custom classes to fields when rendering – Shiva Krishna Bavandla Nov 27 '13 at 07:29
  • Also for the book and author model fields from the above code,the field validation is not occuring right ? – Shiva Krishna Bavandla Nov 27 '13 at 10:08
  • I must tell that your problem is not trivial, so I simplified models and hard coded some features to make the example I provided, you need to adapt it to your suit, also as I recommended in answer is better to use other things to do it more efficient and generic, I figure that using a javascript template plugin to give you more flexibilities and make it more linked to models properties and relations, validations, existing forms, etc. And of course to use existing templates you need to change the js code, but take care to change in both html tags and javascript functions and jquery selectors – juliocesar Nov 27 '13 at 13:04
  • yeah actually this is a complex functionality !!!, may will need to use jquery methods to clone the htmla nd paste it(some kind of stuff like this..) because we have custom html design in place rather than simply doing `form.as_p`. but the problem was, in the above provided can i know why the validation for author and Books fields are not working/displaying on screen ? only Publisher field is validating and displaying an error message – Shiva Krishna Bavandla Nov 27 '13 at 13:22
  • Yes, jquery clone should be ok, I thought to use it first, but then got new problem: change ids, class, etc. The validation problem is caused by use the `form_valid` method on the view, use `post` instead – juliocesar Nov 27 '13 at 16:20
  • yeah exactly, the main challenge is to change/update the ids according to dynamic form names, and yeah i am not class based views, just using normal method with POST method – Shiva Krishna Bavandla Nov 27 '13 at 17:26
  • thanks And i had implemented the same without any jquery library :), so can we make the same for editing functionality ? so i have an `Edit Author Button`, so when a user clicks on this button all the Authors and Books should be in Edit mode(without publisher) ? Is it possible `juliocesar` ? – Shiva Krishna Bavandla Nov 28 '13 at 12:07
  • Yes it is, but just a bit complicated ...If this was help you to solve the problem, do not forgot accept the answer ;) – juliocesar Nov 28 '13 at 16:18
  • Second link is dead. – Ledorub May 13 '22 at 17:53
  • Hi @juliocesar it was very helpful! but do you have any blog that can describe this much better? – Akram BEN GHANEM Mar 01 '23 at 17:59