djangodjango-modelsdjango-signalsdjango-related-manager

Django update m2m objects on save


I have a m-2-m rel and i want to update the rel B after any rel A is added via the admin page.

Here the details:

I have 2 models, Match (rel A) and Player (rel B). I want to update Player.matches_played eveytime a new Match with that player in Match.team1 or Match.team2 is added.

How can i achieve this???

the models

class Match(models.Model):
    data=models.DateField(auto_now_add=False,auto_now=False)
    team1=models.ManyToManyField('players.Player',related_name='team_1')
    team2=models.ManyToManyField('players.Player',related_name='team_2')
    score_team1=models.IntegerField(validators=[MinValueValidator(0)])
    score_team2=models.IntegerField(validators=[MinValueValidator(0)])

class Player(models.Model):
    matches_played= models.IntegerField(validators=[MinValueValidator(0)], default=0)

I tried signals but in post_save signal {instance.team1} or instance.team1.all() return an empty QuerySet which i believe is correct as the M2M rel is saved later. Then i tried m2m_changed but that signal is not fired with a save via the admin page. What am i missing?


@receiver(post_save, sender=Match, dispatch_uid="update_player_game_count")
def update_player_games(sender, instance, created, **kwargs):
    print(f'created {instance.pk}')
    print(f'created {instance.team1}')
    print(f'created {instance.team2}')
    print(f'created {instance.score_team1}')
    print(instance.team1.all())
@receiver(m2m_changed, sender=Match, dispatch_uid="update_player_game_count")
def update_player_game(sender, instance, created, action, **kwargs):
    print(action)
    if action == 'post_add':
        print(action)

Thank you very much for you help

an alternative which i thought about is to retrieve the data via a property in Player for instance like this, but i think, from a performance point of view, is better to update rows every time a match is added then count matches everytime a player is requested

Example

@property
def matches_played(self):
    return Match.objects.filter(team1__nome=self.nome).count()+Match.objects.filter(team2__nome=self.nome).count()

Solution

  • The sender of the m2m_change signal [Django-doc] is not Match, but Match.team1.through, and/or Match.team2.through, so:

    @receiver(
        m2m_changed,
        sender=Match.team1.through,
        dispatch_uid='update_player_game_count1',
    )
    @receiver(
        m2m_changed,
        sender=Match.team2.through,
        dispatch_uid='update_player_game_count2',
    )
    def update_player_game(sender, instance, created, action, **kwargs):
        print(action)
        if action == 'post_add':
            print(action)

    That being said, it make not much sense to store the number of matched played as a field in the model. We can determine it when needed, with:

    from django.db.models import Count
    
    Player.objects.annotate(
        total_matches=Count('team1', distinct=True) + Count('team2', distinct=True)
    )

    Furthermore I don't think using two ManyToManyFields is per se the best modeling here. You might want to use a single ManyToManyField with a through=… model [Django-doc] that determines if the player played for the first or second team.