Setting default for Many-to-Many field

Hi all

I am trying to allow tasks to be allocated to users in my to do app. Users will have multiple tasks and the same task might have multiple users at the same time. Tasks do not have to be allocated to any users, so the form field can be blank.

I have a Task model with a many-to-many relationship with the User model, called “assigned”. (Separately, there is a foreign key to the User model to identify the user that created the instance. I don’t think that’s problematic, here):

class Task(models.Model):
    """
    Stores a single task, related to :model:`core.User` and model: `core.Space`.
    """
    ...
    creator = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="creator", null=False, blank=False
    )  # Required, links to core.User
    text = models.CharField(  # Required
        null=False,
        max_length=250,
        validators=[
            MaxLengthValidator(
                limit_value=249, message="Tasks can only have 250 characters"
            )
        ],
        blank=False,
        verbose_name="Task",
        )
    assigned = models.ManyToManyField(
            User, blank=True, related_name="assignee"
            # Default is the user creating the task
        )
    ...

It works, and if a one or more users is associated with a task in the form, then the junction table in the database is updated, as you would expect.

What I am looking to do is ensure that when a task is created the form suggests the current user be assigned the task, by default. This default could be changed in the browser - including not assigning the task to any users.

I’ve tried:

  • using the form init method to set a value for assigned. This didn’t work because the new instance of the Task hadn’t been saved, so it couldn’t add it to the junction table;
  • updating cleaned_data in the form_valid method in the view. Also didn’t work because, again, the new instance of the Task hadn’t been saved, so it couldn’t add it to the junction table;
  • overriding the save method on the Task model, which I can’t get to work, but also isn’t really what I’m trying to do. I’d like the default to be a prompt to the user through the view/form, rather than embedded in the logic of the database.
   def save(self, *args, **kwargs):
        print("Creator:", self.creator)
        super().save(*args, **kwargs)
        # If the value of 'assigned' is empty, then set it to 'creator' (ie. the current user)
        if not self.assigned.all():
            print ("assigned is empty")
#            print(type(self.assigned))
            self.assigned.add(self.creator)
        print("Assigned is now:", self.assigned)

Sorry to add to the litany of many-to-many related questions, but I can’t find a way through this one. I’ve read so many conflicting posts and bits of documentation that I am now even more confused and now completely stuck.

Thanks in advance

You can set the form field’s initial value to be request.user in the view The Forms API | Django documentation | Django

Thanks Phil

Here’s my attempt in my views.py:

    def form_valid(self, form):
        form.instance.creator = self.request.user
        form.save(commit=False)
        …
        print (form.cleaned_data["assigned"])
        form.save()
        if not form.cleaned_data["assigned"].exists():
            print("didn't provide an assignee")
            form.instance.assigned.set(assigned=self.request.user)
        return super().form_valid(form)

It gives me a TypeError:

create_forward_many_to_many_manager.<locals>.ManyRelatedManager.set() got an unexpected keyword argument 'assigned'
1 Like

I’ve made another couple of (unsuccessful) attempts:

In views.py, using the following, in an attempt to pick up @philgyford’s suggestion:

form_class = TaskForm(initial={"assigned": request.user})

I get:

NameError: name 'request' is not defined

In views.py, using the following:

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        if kwargs['instance'] is None:
            kwargs['instance'] = Task()
        kwargs['instance'].assigned.set(self.request.user)
        return kwargs

I am back to:

ValueError at /tasks/new/
"<Task: >" needs to have a value for field "id" before this many-to-many relationship can be used.

What is the context of that line? In a method of a class based view? Does that method have request as an argument? I assume not, hence the error. So try self.request.user

It would help if you posted the complete view and form here.

Apologies - you’re right. It’s a generic CreateView. It doesn’t have request as an argument.

So, the relevant bit of views.py:

class TaskCreateView(SuccessMessageMixin, CreateView):
    model = Task
    form_class = TaskForm
    form_class = TaskForm(initial={"assigned": request.user})
    template_name = "tasks/task_detail.html"
    success_message = "Task, %(text)s, created successfully."

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["page_title"] = "New Task"
        return context

    def form_valid(self, form):
        form.instance.creator = self.request.user
        form.save(commit=False)
        if form.cleaned_data["is_complete"] == True:
            form.instance.completed = timezone.now()
        else: 
            form.instance.completed = None
        print (form.cleaned_data["assigned"])
        form.save()
        #if not form.cleaned_data["assigned"].exists():
        #    print("didn't provide an assignee")
        #    form.instance.assigned.set(assigned=self.request.user)
        return super().form_valid(form)

... (I've omitted get_success_message and get_success_url methods)

And, forms.py:

class TaskForm(forms.ModelForm):

    is_complete = forms.BooleanField(
        required=False,
        widget=forms.CheckboxInput(
            attrs={"class": "checkbox"}
            )
        )

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop("user", None)
        super().__init__(*args, **kwargs)

        if self.instance.completed is None:
            self.fields["is_complete"].initial = False
        elif self.instance.completed is False:
            self.fields["is_complete"].initial = False
        else:
            self.fields["is_complete"].initial = True
        
        self.fields["spaces"].queryset=Space.objects.by_level()

        # doesn't work
        #if self.instance.assigned is None:
        #    self.fields["is_complete"].initial = self.user

        self.fields["assigned"].queryset=User.objects.exclude(is_superuser=True) # should this logic be in models.py?


    def clean_private(self):
        private = self.cleaned_data["private"]
        if private:
            if self.user != self.instance.creator:
                self.add_error("private", ValidationError(
                    "As you did not create this task, you cannot set it to be private."
                    )
                )
        return private

    class Meta:
        model = Task
        fields = ["is_complete", "text", "private", "deadline", "scheduled", "spaces", "assigned"]
        widgets = {
            "text": forms.TextInput(attrs={
                "class": "input input-primary"
                }
            ),
            "private": forms.CheckboxInput(attrs={
                "class": "checkbox",
                }
            ),
            "deadline": DateInput(attrs={
                'type': 'date'
                }
            ),
            "scheduled": DateInput(attrs={
                'type': 'date'
                }
            ),
        }

Many thanks in advance

The example of setting initial in the docs is:

f = ContactForm(initial={"subject": "Hi there!"})

This is setting f to an instance of the ContactForm.

You have:

Aside from setting form_class twice, form_class should be a class not an object/instance. Which is what you’re doing by setting it as TaskForm(...). So remove that line (and keep form_class = TaskForm).

So, how should se set initial on the form instance?

If we look at the methods in CreateView (see on CCBV) we can see there’s a get_initial() method. That sounds promising!

CreateView.get_initial() looks like this:

def get_initial(self):
    """Return the initial data to use for forms on this view."""
    return self.initial.copy()

self.initial is set to an empty dict by default.

So in TaskCreateView() you could set the form’s initial values by overriding the method like this:

def get_initial(self):
    """Return the initial data to use for forms on this view."""
    initial = self.initial.copy()
    initial["assigned"] = self.request.user
    return initial

Many thanks Phil, for the explanation and the working solution. I had dived into Classy CBV, because setting initial through the form_class didn’t make sense to me. That was also the origin of the get_form_kwargs method approach from my initial post - along with the confetti of unsuccessful tweaks in other files.

So much of my searching found suggestions that the model’s save method needed overwriting, so there was a saved instance of the object before adding the many to many relationship. I also kept getting a Value error, on the same point, (seemingly!)reinforcing that there was a model-level issue, and distracting me from the form layer!

Thanks again