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
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
Book
forms for Author
Author
forms for PublisherSo how to achieve the above functionality ?, i have searched a lot, but could n't able to figure out the above stuff
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With