Many to Many Field on Model Save or Signal

I am struggling with a set of many to many fields not saving as expected when trying to save from either the model save method or from a signal. I have a form view where changes to the many to many fields allowed_communities and allowed_departments save just fine. I am trying add logic where a change to a person’s position updates allowed_communities and allowed_departments since some positions should have access to single vs all vs regional communities / departments. In the below snippet the print statements right before the save show updated values as expected, the problem is the values are not saving to the database and persisting.

I have tried this logic in the model save, a presave signal, a post save signal, and with/without using the access_change_flag as a trigger. I have also tried pulling in the form that saves fine through the view and using that but all yield the same result. Any suggestions on how to resolve or a better way to handle the desired behavior?

class Person(models.Model):

    user = models.OneToOneField(User, on_delete=models.CASCADE)
    position = models.ForeignKey(Position, null=True, on_delete=models.SET_NULL)
    primary_community = models.ForeignKey(Community, null=True, on_delete=models.CASCADE, related_name="primary_community")
    allowed_communities = models.ManyToManyField(Community, blank=True, related_name="allowed_community")
    allowed_departments = models.ManyToManyField(Department, blank=True)
    access_change_flag = models.BooleanField(default=False)


@receiver (post_save, sender=Person)
def person_change_access(sender, instance, created, **kwargs):
    if created or not instance.access_change_flag:
        return None

    try:
        access_level = instance.position.location_access_level
    except:
        return None

    instance.access_change_flag = False
    if access_level == 'R':
        if instance.primary_community.community_name == 'Arrow Senior Living Home Office':
            region = instance.region
        else:
            region = instance.primary_community.region
        if region is not None:
            communities = Community.objects.filter(region=region)
        else:
            communities = [instance.primary_community]
    elif access_level == 'A':
        communities = Community.objects.filter(active=True)
    else:
        communities = [instance.primary_community]
    
    instance.allowed_communities.set(communities)
    instance.allowed_communities.add(instance.primary_community)

    dept_access = instance.position.department_only_access
    if dept_access:
        depts = [instance.position.department]
    else:
        depts = Department.objects.filter(active=True)

    instance.allowed_departments.set(depts)

    print(instance.allowed_communities.all())
    print(instance.allowed_departments.all())

    instance.save()

Side note: Keep in mind that saving membership in a many-to-many relationship does not result in saving either of the two models joined by that relationship. An M2M relationship is expressed as an entry in the join table with foreign keys to both related models.

Unfortunately, I’m having problems trying to understand what the issue is here that you’re trying do describe.

From what I can see, you have a model Person with the listed M2M fields allowed_communities and allowed_departments.

The issue that I think you’re trying to resolve is that when the position field of Person is changed, you want to automatically change the complete set of related Community and Department models. Is that correct?

If so, how are you determining that position has changed? (Or are you just reassigning the set of related models any time the Person is being saved?)

Correct, my goal is when the position field of Person changes that I will run some logic to update the set of allowed_communities and allowed_departments. I have a separate pre-save signal that is testing if position has changed when a Person is saved and setting the access_change_flag on Person to True. Both the pre-save and post-save signals are triggering as expected, the values print as expected, but they do not save.

My understanding from the docs is using set or add, as in instance.allowed_departments.set(values), should include handling the save. Somewhere along the way this is not working.

What are you looking at that is giving you an indication that those values aren’t being saved? Are you examining the database directly after the view has completed, or are you looking for these to be reflected by your view in which this operation is being performed? (If the latter, it would be helpful to see that view.)

(I’m not aware of any situation where a set or add doesn’t immediately save the data under normal conditions.)

I am verifying that saves are not being completed in the form view that works and in the Django admin view for the model. I can only upload a single image so I am going to put the view walkthrough over a couple of responses.

Don’t bother with the images - it’s not going to provide me with information that will be useful.

Simply confirming that you have looked at this outside the context of the view being run is good enough for now. (Or, if you really feel that the data will be more illustrative, then specifically providing the data - including the relevent keys and other reference data would be far more useful.)

When making a position change I see the print statements I included to troubleshoot triggering and displaying the correct values:

Pre-Save Signal
Signal Change Community
Signal Change Department
<QuerySet [<Community: Arrow Senior Living Home Office>, <Community: Boulevard St Charles>, <Community: Boulevard Wentzville>, <Community: Castlewood>, <Community: Cedar Trails>, <Community: Fremont>, <Community: Gentry Park>, <Community: Kingsland Walk>]>
<QuerySet [<Department: Culinary>]>

The above shows changing to a regional group of communities and a single department.

However when inspecting in the form view or admin nothing is changed.

Manual updates through the form view work and show in the form view as well as admin.
image

What is the view or process that is resulting in the post_save signal being fired? Is there any chance that that view is overwritting the changes made in the receiver?

The basic process is very boilerplate form submission triggers the presave signal and then the post save signal, there are no additional updates / saves that would overwrite. The form view works for changes directly to these fields, I would assume if an overwrite was present within the view it would also overwrite the form changes. The presave signal doesn’t trigger an additional save, it just updates the access_change_flag on the instance. Presave and post save signal together look like this

@receiver (pre_save, sender=Person)
def person_position_change_handler(sender, instance, **kwargs):
    if instance.access_change_flag:
        return None
    print('Pre-Save Signal')

    try:
        previous = Person.objects.get(id=instance.id)
    except Person.DoesNotExist:
        return None

    if previous.position is None:
        return None

    if previous.position != instance.position:
        new_access_level = instance.position.location_access_level
        previous_access_level = previous.position.location_access_level
        if new_access_level != previous_access_level:
            instance.access_change_flag = True
            print('Signal Change Community')

        new_dept_access = instance.position.department_only_access
        previous_dept_access = previous.position.department_only_access

        if new_dept_access != previous_dept_access:
            instance.access_change_flag = True
            print('Signal Change Department')
            

@receiver (post_save, sender=Person)
def person_change_access(sender, instance, created, **kwargs):
    if created or not instance.access_change_flag:
        return None

    try:
        access_level = instance.position.location_access_level
    except:
        return None

    instance.access_change_flag = False
    if access_level == 'R':
        if instance.primary_community.community_name == 'Arrow Senior Living Home Office':
            region = instance.region
        else:
            region = instance.primary_community.region
        if region is not None:
            communities = Community.objects.filter(region=region)
        else:
            communities = [instance.primary_community]
    elif access_level == 'A':
        communities = Community.objects.filter(active=True)
    else:
        communities = [instance.primary_community]
    
    instance.allowed_communities.set(communities)
    instance.allowed_communities.add(instance.primary_community)

    dept_access = instance.position.department_only_access
    if dept_access:
        depts = [instance.position.department]
    else:
        depts = Department.objects.filter(active=True)

    instance.allowed_departments.set(depts)

    print(instance.allowed_communities.all())
    print(instance.allowed_departments.all())

    instance.save()

But the only way I can get a complete picture of everything that is going on in this process is if you post the view where the Person object is being modified, along with the save methods of the form and view if they exist.

Note - I just realized something - you have in your signal handler:

You may not want to do this. This actually creates the possibility of a recursive call into your pre_save and post_save handlers, since saving this instance of person can trigger this same signals.

I’m guessing that one of the issues related to this is this line. (Although, this should be trapped by your first condition - this may turn out to be safe. But it’s worth trying to follow through the logic of what’s going to happen when you call your signal handlers at that point.)

To address your comment here:

That depends upon the specifics of that form and what it’s updating. If it’s only updating the many-to-many relationship, it won’t trigger the post_save signal, since the Person object isn’t being changed.

If you are modifying Person, then it becomes a sequence-of-events issue. (When is it calling the post_save function vs when your view is updating the sets.)

One final thought for debugging this - you could turn on logging for the SQL requests to see every update that is being issued. If there’s still some confusion about what is happening when, those logs might help clear up the sequence in which things are being called.

Thank you for the SQL logging suggestion! Fairly new to Django and would never have thought of that. I can see in the logging that at the point where the print statements were returning the values I expected they are matched with insert statements with the values expected. However then additional insert statements are being triggered that overwrite the changes. I will need to dig in to scrub why but being able to see the transactions makes this so much easier than fumbling around in the dark.

I had a similar problem, depending on a field value I’d run a function that changes a m2m on the same object.(using post save signal) The function worked correctly when change to the field was triggered from the api. But when the field value was changed from the admin the form would overwrite the value. My understanding is that this happens because the form saves the object then updates it’s m2m fields. The function would be triggered (post save) and then the form would overwrite the value by whatever was there before. I got the clue from here #30022 (Doc how to combine post_save signal with on_commit to alter a m2m relation when saving a model instance) – Django