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