Set ManyToMany value that is NOT in ModelForm

I have this model:

class EmailToAssociation (models.Model):
    email = models.EmailField(_("Email address"), unique=True)
    association = models.ManyToManyField(showorderofplay.Association)

And this ModelForm:

class ManageAscPeopleCreateForm(forms.ModelForm):
    class Meta:
        model = EmailToAssociation
        fields = ['email']

Which only allows the user to enter an email as the association id comes from the server so no point in passing it to the client to then just send it back. I’m trying to write the association in form_valid but as association was not in the form, it is not having it. Where would I intercept the save of the model data such that I could inject association alongside email?

class ManageAscPeopleCreate(CreateView):
    model = EmailToAssociation
    form_class = ManageAscPeopleCreateForm

    def get_success_url(self):
        return reverse("showorderofplay:manage-asc-people", args=[self.kwargs["association_id"]])

    def form_valid(self, form):

        super_resp = super().form_valid(form)
        self.model.association.add(self.kwargs["association_id"])
        return super_resp

I’m getting this error for the attempt to add association_id

AttributeError at /1/add_person_to_asc/
'ManyToManyDescriptor' object has no attribute 'add'

The issue is that you’re trying to assign the value to the EmailToAssociation model (self.model), instead of the instance of the model that was created in the view (self.object).

What is tripping me up each time is understanding what is available with each object. I tried using

print (dir(self.object))

Which produces :

'__bool__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', 
'__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', 
'__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', 
'__str__', '__subclasshook__']

So I still don’t know what I then call to access the association value that will be written to the database?

ccbv CreateView again doesn’t seem to clear to me how I set a model field value.

Where exactly in your view do you have that line?

You set a model field value the same regardless of where you set it:
instance.field_name = value

However, keep in mind that a ManyToManyField is not a field in the model. It’s a reference to a relationship manager for data stored in a “through” table that establishes the connection between the two models. Adding a value to a many-to-many relationship does not affect either of the two related models. It’s only the through table that is being changed.

I put it in form_valid as I was trying to see what was available to me

class ManageAscPeopleCreate(CreateView):
    model = EmailToAssociation
    form_class = ManageAscPeopleCreateForm

    def get_success_url(self):
        return reverse("showorderofplay:manage-asc-people", args=[self.kwargs["association_id"]])

    def form_valid(self, form):
        print (dir(self))
        print (self.assocation)
        self.object.model.association.add(self.kwargs["association_id"])
        return super().form_valid(form)

self.association doesn’t work either

AttributeError: 'ManageAscPeopleCreate' object has no attribute 'assocation'

which makes me think I need to add it somewhere because it is not defined in the form as I was trying to avoid sending it to the client and back again?

I used .add() in another class method to add a many to many id to a user so would that not work here as well?

The self.object isn’t created until the form is saved - which happens by default in the super method of form_valid.

You need to call super() before trying to use self.object, in exactly the same manner as at add value to users manytomany field from form results - #5 by nmilner1234

The reason I didn’t do it this way round was because I was thinking that after the point of calling super() the record would now already have been created in the EmailToAssociation model and therefore it now
is too late to set the assocition value. When exactly does the object get saved in the table?

See the last paragraph of my comment at: Set ManyToMany value that is NOT in ModelForm - #4 by KenWhitesell

When the .save() method is called on the form.

My understanding is that

super_resp = super().form_valid(form)

will save the form into the table (so at the moment the record just has email set, not assoction)

However, the object is now also available in super_resp so doing this:

self.object.association.add(self.kwargs["association_id"])

Now adds the association id to the object so is that also automatically updating the database record or is it the return of super_resp that then triggers .save() to then commit to the database?

No.

Quoting myself directly:

However, keep in mind that a ManyToManyField is not a field in the model. It’s a reference to a relationship manager for data stored in a “through” table that establishes the connection between the two models.
Adding a value to a many-to-many relationship does not affect either of the two related models. It’s only the through table that is being changed.

See the docs at Many-to-many relationships | Django documentation | Django

Sorry, it was the through table bit I didn’t appreciate.

I changed the field to a foreign key and this is now my form_valid() which works but I wondered if this is the best/correct way to do this:

    def form_valid(self, form):
        the_form = form.save(commit=False)
        the_form.association = Association.objects.get(pk=self.kwargs['association_id'])

        # If the user has an account, also add access to the association
        try:
            user_data = CustomUser.objects.get(email=the_form.email)
            user_data.asc_access.add(self.kwargs["association_id"])
            user_data.save()
        except:
            pass

        return super(ManageAscPeopleCreate, self).form_valid(form)

You’re overthinking this. You’re worried about things you don’t need to worry about.

To avoid confusion, you should not name the variable the_form, because the form.save() method returns the object being saved, not the form. The the_form variable will actually be the instance of EmailToAssociation.

One more time.

This line:

does not mean you need to do this:

because user_data is not modified by the add.

Actually, I’m starting to get lost with the models being used here. To summarize this for future messages on this thread:

  • This form is creating an instance of EmailToAssociation (?)
  • How does this relate to the Association object being referenced here?
  • Where does CustomUser come into play?

It might be helpful at this point if you post these models. I’m not sure I’m seeing the bigger picture here.

I’ve moved away from the manytomany relationship as it turned out to be the wrong relationship I needed.

the model is now:

class EmailToAssociation (models.Model):
    email = models.EmailField(_("Email address"))
    association = models.ForeignKey(
        Association, on_delete=models.CASCADE)

The view as discussed above is:

class ManageAscPeopleCreate(CreateView):
    model = EmailToAssociation
    form_class = ManageAscPeopleCreateForm

    def get_success_url(self):
        return reverse("showorderofplay:manage-asc-people", args=[self.kwargs["category_id"], self.kwargs["association_id"]])

    def form_valid(self, form):
        the_form = form.save(commit=False)
        the_form.association = Association.objects.get(
            pk=self.kwargs['association_id'])

        # If the user has an account, also add access to the association
        try:
            user_data = CustomUser.objects.get(email=the_form.email)
            user_data.asc_access.add(self.kwargs["association_id"])
            user_data.save()
        except:
            pass

        return super(ManageAscPeopleCreate, self).form_valid(form)

and the form is:

class ManageAscPeopleCreateForm(forms.ModelForm):
    class Meta:
        model = EmailToAssociation
        fields = ['email']

The form only looks at the email field hence the view has to add in association

The try: code is to update a user record not related to the current logged in user.

First, which many-to-many relationship has been removed? It appears you’re still relying upon one for the relationship between Association and CustomUser. If that is the relationship that has been changed, then some of what I’ve written below would not be right.

Except you are not updating the user record.

The CustomUser object, whichever one it is, is not being changed during this process. There is no need to save it.

There’s no need for you to do a query for the Association. You can do this:
(Renaming the_form to email_association for clarity.)

email_association = form.save(commit=False)
email_association.association_id = self.kwargs['association_id']`
email_association.save()

You could also avoid the extra query on CustomUser if you added the row directly to the join table established for that many-to-many relationship.