Save many-to-many relationship after use the add() method on the field to add a record

I have models Book and I want to save new record this book after every update information about this book (duplicate information). Wherein, I want that main book don’t save in database, only duplicate information in database. All fields are stored normally except “many-to-many”
I have tried all methods that I know, but It doesn’t work. In the code below I left my attempts maybe it will be useful

models.py

class Book(models.Model):
    slug = models.SlugField(max_length=255)
    title = models.CharField(max_length=255)
    author = models.ForeignKey(
        "Author",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="author_book",
    )
    abstract = models.TextField()
    coauthor_book = models.ManyToManyField("Author", blank=True, related_name="coauthor_book")
    subject = ManyToManyField("Subject")

class Author(models.Model):
    slug = models.SlugField(max_length=255)
    name = models.CharField(max_length=255)

class Subject(models.Model):
    slug = models.SlugField(max_length=255)
    name = models.CharField(max_length=255)

forms.py

class BookForm(forms.ModelForm):
      class Meta:
        model = Book
        fields = "__all__"

views.py

class BookUpdateView(UpdateView):
    model = Book
    form_class = BookForm
    template_name = "book/book_update.html"

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        form = self.get_form()
        if form.is_valid():
            obj = form.save(commit=False)
            obj.editor = self.request.user

            coauthors = obj.coauthor_book
            subjects = obj.subject
            draft = Book.objects.create(
                title=obj.title,
                author=obj.author,
                abstract=obj.abstract,
                slug=obj.slug,
            )
            draft.save()

# My triying. But It doesn't work
            for coauthor in coauthors.all():
                draft.coauthor_book.add(coauthor)
            for subject in subjects.all():
                draft.subject.add(subject)
            return redirect("book", obj.slug)
        return self.render_to_response({"form": form})

Can you provide more specifics when you say “it doesn’t work”? Does it generate an error? Or does it just not create the new associations?

You have verified that the new instance of Book is being created?

Ken, hello!

It does not create many-to-many relationship (association). The book is created, but it does not have this relationship(association).

I’d verify that this code is doing what you’re expecting it to do by adding a couple of print statements to the flow.

I would want to print each instance of coauthor and subject within their related loops, along with printing coauthors.count() and subjects.count() before the loops - all in the interest of validating that you’re getting the right data.

Hi Wersony,

I am newbie to Django, and can relate to frustration of trying to find a solution. Two things I see that may need changing is your using a HTML function “POST” as a class name “post” not recommended. 2nd is you need to use the .add() method on the associative tables to make a new record. not the save() method. You are going to have a book_coauthors and a book_subjects associative tables. Use a viewer on your SQL database to examine the tables created and the column names. This helped me better understand how the many-to-many relationship works in Django.

i had to add both a save() and an add() to my own view to get both tables updated in own my app.

Hope this is helpful.

FC

1 Like

Sorry, that is not correct. He’s not creating a class name “post”. That function is the appropriately named function within a ClassBasedView.

This is defining a class named BookUpdateView with a method named post.

He is doing that. See the section of code that I quoted in my previous reply.

What makes his pattern look odd is that he’s creating a new copy of the base object here, not modifying an existing copy. But assuming the data being referenced is correct, this all appears “right”.

1 Like

Ken, you’re right. It’s empty. I get this <QuerySet []> book.Subject.None and <QuerySet []> book.Author.None. But I don’t know why it is.

From here, then I’d do more digging into the objects being used.

First, I’d probably examine the data in the tables themselves, then add some print statements earlier on to verify you’re working with the objects you think you’re working with and that the data you’re expecting to find is present in the table.

This line concerns me - I’m not convinced that obj is going to be fully populated in this case.

Check the primary key for obj at this point.

Ken, thank you for advice. I then went to try to fix it, I’ll write about my results here, if you don’t mind

1 Like

Ken, I dug into my objects which I used. It’s right objects. But it works If I only use (code is below):

class BookUpdateView(UpdateView):
    model = Book
    form_class = BookForm
    template_name = "book/book_update.html"

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        form = self.get_form()
        if form.is_valid():
            obj = form.save()  #New
            obj.editor = self.request.user

            coauthors = obj.coauthor_book
            subjects = obj.subject
            draft = Book.objects.create(
                title=obj.title,
                author=obj.author,
                abstract=obj.abstract,
                slug=obj.slug,
            )
            draft.save()
            for coauthor in coauthors.all():
                draft.coauthor_book.add(coauthor)
            for subject in subjects.all():
                draft.subject.add(subject)
            return redirect("book", obj.slug)
        return self.render_to_response({"form": form})

This creates a new object and update my currently book. Although, I don’t want to update my currently book.
Ken, could you please give me some advice why this is so?

My best guess is that the commit=False doesn’t populate the pk field of the skeleton object being built - so you’re not getting the id of the Book being submitted at that point. As a result, the obj.coauthor_book reference is going to return null.

Since you’re creating a new instance of the model and not updating the existing model, you’ve got a couple different choices.

You could call self.get_object() to retrieve the object being edited rather than saving the form - but this just “feels wrong”. My gut reaction to this process is a “square peg / round hole” situation.

You’re creating a new instance of a model rather than updating an existing instance - so I’d approach this a different way.

Rather than inheriting from UpdateView, I’d inherit from CreateView - but still have the url parameter of the primary key for the existing object. When building the view, use that existing object as the default data for the form.
Then, when the form is submitted, you have all the original data in the fields, and still have that url parameter available to retrieve the m2m references to populate your new instance accordingly.

Another option would be to not use a model form at all. Use a FormView

1 Like

Ken, thank you!
I was finally able to do it. It took me a long time. But your advice was useful, without it, I could not have done it.

I especially liked your comparison :slight_smile:

I can’t build it by Class-based view. I used function-based view. However, it seems to be working.

def BookUpdateView(request, slug):
    article = Book.objects.get(slug=slug)
    if request.method == "POST":
        form = BookForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)
            obj.slug = book.slug
            obj.save()
            messages.success(request, _("Thank you!"))
            form.save_m2m()
            return redirect("book", obj.slug)
    else:
        form = BookForm(instance=book)
    return render(request,
                  "book/book_update.html",
                  {'form': form,
                   "book": book}
                  )