Admin post_save signal

I’m having trouble with a post_save signal when using Admin. For background, I have a User extension model that uses a many-to-many relationship for user follows. But, I don’t want users to follow themselves. So, I created the following receiver:

  @receiver(post_save, sender = UserInfo)
  def check_self_follow(sender, instance, created, **kwargs):
      logger.debug('Just saved %s', str(instance))
      logger.debug('instance id is %s', instance.id)
      logger.debug('users_followed %s', instance.users_followed.all())
      for follow in instance.users_followed.all():
          logger.debug('follow id is %s', follow.id)
          if follow.id == instance.id:
              logger.debug("Removing %s from follow links for %s", str(follow), str(instance))
              instance.users_followed.remove(follow)

I also have 2 helper functions defined in the UserInfo model class:

    def follow_user(self, user: "UserInfo"):
        """Create a link for the specified user to follow the user input."""
        logger.debug("creating link for user %s following %s", self.username, user.username)
        self.users_followed.add(user)
        self.save()
        logger.debug("Follow link creation successful")

    def get_users_followed(self):
        """Return a list of UserInfo objects for all users followed by this user"""
        users_followed = [list for list in self.users_followed.all()]
        logger.debug("getting list of users followed for user %s: %s", self.username, users_followed)
        return users_followed

This works fine when I’m working in the shell, I see something like this:

>>> betty.follow_user(Betty)
creating link for user betty following Betty
Just saved Betty
instance id is 1
users_followed <QuerySet [UserInfo("betty","betty"), UserInfo("joe","joe")]>
follow id is 1
Removing betty from follow links for Betty
follow id is 3
Follow link creation successful

This is working correctly, as shown in the shell:

>>> betty.get_users_followed()
getting list of users followed for user betty: [UserInfo("joe","Joe")]
[UserInfo("joe","Joe")]

But when I do a similar thing in the Django Admin console, I see the following:

Just saved Betty
instance id is 1
users_followed <QuerySet [UserInfo("joe","joe")]>
follow id is 3

And the shell shows:

>>> betty.get_users_followed()
getting list of users followed for user betty: [UserInfo("betty","betty"), UserInfo("joe","Joe")]
[UserInfo("betty","betty"), UserInfo("joe","Joe")]

It’s as if admin is calling check_self_follow in pre_save instead of post_save. This is my first whack at Django, and I’m mystified. Is there some difference with signals in admin?

Your test is comparing the pk of these elements and not the name. Showing the tests relative to the name field doesn’t really prove anything or illustrate anything relative to the situation here.

I think we’d need to see a more specific, detailed, and complete example showing everything that was being done to try and answer your questions here. At a minumum, you should change your __str__ methods to include the pk of the objects being printed.

Thanks for the reply. I modified the str function for my UserInfo model as you suggested and straightened out the logging in my signal function to (hopefully) make it easier to follow. The “follow_user” and “get_users_followed” functions have not changed. Here is the updated signal function:

@receiver(post_save, sender = UserInfo)
def check_self_follow(sender, instance, created, **kwargs):
    logger.debug('Just saved %s', instance)
    logger.debug('users_followed %s', instance.users_followed.all())
    for follow in instance.users_followed.all():
        logger.debug('testing followed user %s', follow.username)
        if follow.id == instance.id:
            logger.debug("Removing %s from follow links for %s", follow, instance)
            instance.users_followed.remove(follow)

When I use the follow_user function in the shell to make a user follow themselves, this works as expected. The check_self_follow function detects the condition and removes the follow link, as shown below:

>>> betty
<UserInfo: (user:"betty"," username:betty", pk:1)>
>>> betty.get_users_followed()
getting list of users followed for user betty: [<UserInfo: (user:"joe"," username:joe", pk:3)>]
[<UserInfo: (user:"joe"," username:joe", pk:3)>]
>>> betty.follow_user(betty)
creating link for user betty following betty
Just saved (user:"betty"," username:betty", pk:1)
users_followed <QuerySet [<UserInfo: (user:"betty"," username:betty", pk:1)>, <UserInfo: (user:"joe"," username:joe", pk:3)>]>
testing followed user betty
Removing (user:"betty"," username:betty", pk:1) from follow links for (user:"betty"," username:betty", pk:1)
testing followed user joe
Follow link creation successful
>>> betty.get_users_followed()
getting list of users followed for user betty: [<UserInfo: (user:"joe"," username:joe", pk:3)>]
[<UserInfo: (user:"joe"," username:joe", pk:3)>]

However, when I use the Admin console to make Betty follow herself, I see the following in the server log:

Just saved (user:"betty"," username:betty", pk:1)
users_followed <QuerySet [<UserInfo: (user:"joe"," username:joe", pk:3)>]>
testing followed user joe

And I see the following in the shell:

>>> betty.get_users_followed()
getting list of users followed for user betty: [<UserInfo: (user:"betty"," username:betty", pk:1)>, <UserInfo: (user:"joe"," username:joe", pk:3)>]
[<UserInfo: (user:"betty"," username:betty", pk:1)>, <UserInfo: (user:"joe"," username:joe", pk:3)>]

Notice that the server log does not show betty as a followed user after the save, even though that was exactly the change that I entered into the console. Since betty is not in the list of followed users when check_self_follow is called, obviously she cannot be removed. However, a subsequent query shows that betty is, indeed, added to the list. It’s as if check_self_follow is being called pre-save instead of post-save.

I hope this clarifies what I believe I’m seeing. Please let me know if you need any more information.

What you have is an issue with the sequence of events.

Keep in mind that if you have two models related by a many-to-many field, adding or removing an entry to that relationship does not require a change to either of the two related models. The only change is to the join table that defines the relationship.

If UserInfo is your custom user model (or a profile model) and it has a field defining a many-to-many relationship, the add to that field will not trigger the signal for the base model. Your follow_user method will do the add and then save the base instance - causing the trigger to be fired after the relation has been added. However, the admin works in reverse. It’s going to save the base model first - causing the signal to be sent, and then save the relationship entry. When the relationship entry is saved, there won’t be a signal sent for UserInfo.

What you really want to do here is change your many-to-many field to use a through table, and define a constraint in that table to prevent the two related foreign keys from being the same.

@adamchainz has done an excellent blog post on this - see Using Django Check Constraints to Prevent Self-Following - Adam Johnson

Thanks! That looks like exactly the solution!