Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested forms with django

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

like image 453
Shiva Krishna Bavandla Avatar asked Nov 26 '13 13:11

Shiva Krishna Bavandla


1 Answers

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
like image 133
juliocesar Avatar answered Nov 19 '22 00:11

juliocesar