Many-to-many for co-author in CreateView for books

I want to build CreateView for books. But in this CreateView I have “if elif else” functions. And also I have Many-to-many for co-author.
When I use this approach (a code below) it works, but when the book is created, it has status for obj.coauthor. But I did not use a co-author for this book. Also If I created the book with publisher it also have status as if I used a co-author.
views.py

class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    form_class = BookForm
    template_name = "books/book_create.html"

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(BookCreateView, self).form_valid(form)

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

            if obj.publisher and obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.publisher:
                obj.state = obj.BookProcess.Co_author_approved
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            else:
                obj.state = obj.BookProcess.Published
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)

But when I use this code without, obj.save() form.save_m2m() firstly I have an error <Book: example>" needs to have a value for field “id” before this many-to-many relationship can be used (the code below).
views.py (without)

class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    form_class = BookForm
    template_name = "books/book_create.html"

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(BookCreateView, self).form_valid(form)

  def post(self, request,  * args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            obj = form.save(commit=False)
            obj.author = self.request.user
# I removed these two lines
            if obj.publisher and obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.publisher:
                obj.state = obj.BookProcess.Co_author_approved
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            else:
                obj.state = obj.BookProcess.Published
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)

models.py

class Book(models.Model):
    slug = models.SlugField(unique=True)
    title = models.CharField(max_length=255, db_index=True)
    author = models.ForeignKey(
        "users.CustomUser", on_delete=models.SET_NULL, null=True
    )
    coauthor = models.ManyToManyField(
        "users.CustomUser",
        blank=True,
        related_name="coauthor",
   abstract = models.TextField(blank=True, null=True)
    content = models.TextField(blank=True, null=True)
   publisher = models.ForeignKey(
        "Publisher", on_delete=models.SET_NULL, null=True, blank=True
    )

What does the form look like?

You reference a state field many places in your code, but I don’t see it in the model you posted.

Which version of the view are we trying to diagnose here? And what are the problems with it? (It gets confusing trying to follow a mixture of two descriptions with two versions of the same view. It would be helpful to focus on one version with one specific set of symptoms.)

Ken, hello!

class BookForm(forms.ModelForm):
     class Meta:
        model = Book
        exclude = [
            "slug",
            "author",
        ]

I apologize. I forgot to attach it. This is my full models for Book. And thank you again. I build a workflow as you advised me before.

class Book(models.Model):
    class BookProcess(models.TextChoices):
        Draft = "Draft", _("Draft")
        Co_author_approved = "Co-author Approved", _("Co-author Approved")
        Co_author_rejected = "Co-author Rejected", _("Co-author Rejected")
        Editor_approved = "Editor Approved", _("Editor Approved")
        Editor_rejected = "Editor Rejected", _("Editor Rejected")
        Published = "Published", _("Published")
    slug = models.SlugField(unique=True)
    title = models.CharField(max_length=255, db_index=True)
    author = models.ForeignKey(
        "users.CustomUser", on_delete=models.SET_NULL, null=True
    )
    coauthor = models.ManyToManyField(
        "users.CustomUser",
        blank=True,
        related_name="coauthor",
   abstract = models.TextField(blank=True, null=True)
    content = models.TextField(blank=True, null=True)
   publisher = models.ForeignKey(
        "Publisher", on_delete=models.SET_NULL, null=True, blank=True
    )
   state = FSMField(choices=BookProcess.choices,
                     default=BookProcess.Draft)

I apologize again. I think if I attach two options,it will be useful. I think my second one is more correct. But I get the error - <Book: example>" needs to have a value for field “id” before this many-to-many relationship can be used. Although I used form.save_m2m().

class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    form_class = BookForm
    template_name = "books/book_create.html"

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(BookCreateView, self).form_valid(form)

  def post(self, request,  * args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            obj = form.save(commit=False)
            obj.author = self.request.user
            if obj.publisher and obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.publisher:
                obj.state = obj.BookProcess.Co_author_approved
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            else:
                obj.state = obj.BookProcess.Published
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)

I’m not seeing anything wrong in what you’ve posted. The pattern certainly looks right to me.

Can you post the complete traceback for this? Or at least verify that the error is being thrown in this view - and at which line.

Ken, sure. This is traceback

Traceback (most recent call last):
  File "/Users/user/Desktop/column/.venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/user/Desktop/column/.venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/user/Desktop/column/.venv/lib/python3.8/site-packages/django/views/generic/base.py", line 69, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/user/Desktop/column/.venv/lib/python3.8/site-packages/django/contrib/auth/mixins.py", line 71, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/Users/user/Desktop/column/.venv/lib/python3.8/site-packages/django/views/generic/base.py", line 101, in dispatch
    return handler(request, *args, **kwargs)
  File "/Users/user/Desktop/column/books/views.py", line 195, in post
    elif obj.coauthor:
  File "/Users/user/Desktop/column/.venv/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 536, in __get__
    return self.related_manager_cls(instance)
  File "/Users/user/Desktop/column/.venv/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 851, in __init__
    raise ValueError('"%r" needs to have a value for field "%s" before '

Exception Type: ValueError at /articles/create
Exception Value: "<Book: example>" needs to have a value for field "id" before this many-to-many relationship can be used.

Ok, this is starting to clear things up.

Notice where the error is being thrown -

In order for there to even be an entry in the coauthor join table, then obj must have a primary key. You would need to do the obj.save() before starting this chain of if / elif conditions. (This is a different situation from the “typical” case where you can use a “tentative” pk to reference the object when other objects are being created. You’re not creating a new object at this point, you’re attempting to access an existing object - which doesn’t exist yet.)

If you need to change the state, you can still do that and save it in the individual conditions.

Ken, thank you. The book is saved, but it has state Draft. I think a book is taken default state from model. Maybe I was wrong.
Can you give me advice how to build with if / elif conditions? You wrote me, but I am not certain how to do it by automatically after create a book.

my views after your advice

class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    form_class = BookForm
    template_name = "books/book_create.html"

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(BookCreateView, self).form_valid(form)

  def post(self, request,  * args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            obj = form.save(commit=False)
            obj.author = self.request.user
            obj.save()
            if obj.publisher and obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.coauthor:
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.publisher:
                obj.state = obj.BookProcess.Co_author_approved
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            else:
                obj.state = obj.BookProcess.Published
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)

Your condition as written:

is always going to return True for the ManyToMany relationship. That’s not a valid test to determine whether any related objects are assigned.

Remember that in an M2M or reverse foreign key relationship, the object being referenced is a manager. When you’re looking to test for the existence of any related entities, you can use the .exists() method on that manager. (e.g. obj.coauthor.exists())

Ken, thank you for the explanation. This is useful information for me.
Unfortunately, I can’t build right a valid test to determine whether any related objects are assigned. It seems to me obj.coauthor.exists() does not work. I build in different approach, but it didn’t work. I think this is my best approach (code below). But it also didn’t work.
Here is the result after creating the books:
if obj.publisher and obj.coauthor.exists(): - return state Draft and book is not saved coauthor.
elif obj.coauthor.exists(): - return state Published and book is saved coauthor
elif obj.publisher: - return state Draft
else - return state Published

class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    form_class = BookForm
    template_name = "books/book_create.html"

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(BookCreateView, self).form_valid(form)

  def post(self, request,  * args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            obj = form.save(commit=False)
            obj.author = self.request.user
            obj.save()
            if obj.publisher and obj.coauthor.exists():
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.coauthor.exists():
                obj.state = obj.BookProcess.Draft
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            elif obj.publisher:
                obj.state = obj.BookProcess.Co_author_approved
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)
            else:
                obj.state = obj.BookProcess.Published
                obj.save()
                form.save_m2m()
                return redirect("book_detail", obj.slug)

It does work. Just keep in mind that when you’re first creating obj, there are no related coauthor instances - they don’t exist until after the save_m2m call is made.

Help me understand what you’re trying to achieve.

What do you want state to be under what conditions?

1 Like

I want to achieve that after the workbook is created it has different values based on selected values or entered values. For example, if I created a book, it has state Published. If I created a book with the selected coauthor, it has state Draft. If I created a book with the selected publisher, it has the status Co_author_approved.
I need it for display book only, which has state Published. And also for workflow

Then you would probably want to set that state after all the data has been initially saved from the form(s).

Yes, but I don’t know how to build it.

???

You know how to save the data from the forms. You know how to write your conditions. You know how to set the state. You’re going all this now.

The only issue that I can see at this point is that you’re doing it in the wrong order.

Save your data first, then run your checks to see how the state should be set. And then save that object again with its new state.

Ken, thanks! I understand what you wrote to me. Now it works according to your advice.
I’m sorry I didn’t understand it right away.

working version of the code

class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    form_class = BookForm
    template_name = "books/book_create.html"

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(BookCreateView, self).form_valid(form)

  def post(self, request,  * args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            obj = form.save(commit=False)
            obj.author = self.request.user
            obj.save()
            form.save_m2m()   #new
            if obj.publisher and obj.coauthor.exists():
                obj.state = obj.BookProcess.Draft
                obj.save()
                return redirect("book_detail", obj.slug)
            elif obj.coauthor.exists():
                obj.state = obj.BookProcess.Draft
                obj.save()
                return redirect("book_detail", obj.slug)
            elif obj.publisher:
                obj.state = obj.BookProcess.Co_author_approved
                obj.save()
                return redirect("book_detail", obj.slug)
            else:
                obj.state = obj.BookProcess.Published
                obj.save()
                return redirect("book_detail", obj.slug)
1 Like