Django many-to-many formset for books and authors: only books added to database

41 Views Asked by At

My aim is to have a form where users can enter a book and its authors into the database using an 'Add Author' button if there's more than one author. I use a many-to-many relationship so all of these authors can be added (or in most cases, just 1 author). Currently the book is getting added to the table but not the author. I think it has to do with the multiple forms aspect.

Originally I tried CreateView until I realised that doesn't work with more than one model at a time.

models.py:

class Book(models.Model):
    title = models.CharField(max_length=200)
    authors = models.ManyToManyField('Author', through='Authored')

    def __str__(self):
        return self.title

class Author(models.Model):
    name = models.CharField(max_length=200)
    books = models.ManyToManyField('Book', through='Authored')

    def __str__(self):
        return self.name

class Authored(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

views.py:

class BookCreate(View):
    template_name = 'bookmany/book_form.html'
    form_class = BookForm
    formset_class = AuthorFormSet

    def get(self, request, *args, **kwargs):
        form = self.form_class()
        formset = self.formset_class()
        return render(request, self.template_name, {'form': form, 'formset': formset})

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        formset = self.formset_class(request.POST)
        success_url = reverse_lazy('bookmany:book_list')

        if form.is_valid():
            book = form.save()

            if formset.is_valid():
                for form in formset:
                    if form.cleaned_data.get('name'):
                        author, created = Author.objects.get_or_create(
                            name=form.cleaned_data.get('name'))
                        Authored.objects.create(book=book, author=author)
                return redirect(success_url)
        return render(request, self.template_name, {'form': form, 'formset': formset})

book_form.html:

{% extends "base_menu.html" %}
{% block content %}
<p>
    <form action="" method="post">
        {% csrf_token %}
        {{ form.as_p }}

        <div id="author-forms">
            {{ formset.management_form }}
            {% for author_form in formset %}
                {{ author_form.as_p }}
            {% endfor %}
        </div>

        <div id="empty-form" style="display:none;">
            <div class="author-form">
                {{ formset.empty_form }}
            </div>
        </div>
        <button type="button" class="add-form">Add Author</button>
        <input type="submit" value="Submit">
        <input type="submit" value="Cancel" onclick="window.location.href=
                                    "{% url 'bookmany:book_list' %}";return false;">
    </form>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script type="text/javascript">
        $(function() {
            $('.add-form').click(function() {
                var formIdx = parseInt($('#id_authorformset-TOTAL_FORMS').val());
                var newForm = $('#empty-form').html().replace(/__prefix__/g, formIdx);
                $('#author-forms').append(newForm);
                $('#id_authorformset-TOTAL_FORMS').val(formIdx + 1);
            });
        });
    </script>
</p>
{% endblock %}
1

There are 1 best solutions below

0
ELKOSAKO On

You don't have to use models.ManyToManyField in both models. Use it only in Book model. Then create Book model object and Author model object and use them to create Authored model object. But to be honest, you don't have to use through in relationship in this case. You can use only models.ManyToManyField in one of that models. We use transitional table, when we want to add another parameter to this relationship.