Search code examples
pythondjangodjango-signals

Django signal conditionally update one table based on another


I'm working on my first django project that is a sports betting game.

Here are my models:

class Game(models.Model):
    home_team = models.CharField(max_length=200)
    away_team = models.CharField(max_length=200)
    home_goals = models.IntegerField(default=None)
    away_goals = models.IntegerField(default=None)


class Bet(models.Model):
    gameid = models.ForeignKey(Game, on_delete=models.CASCADE)
    userid = models.ForeignKey(User, on_delete=models.CASCADE)
    home_goals = models.IntegerField()
    away_goals = models.IntegerField()
    score = models.IntegerField(default=None, null=True)

First I create a game instance with null values in goals fields, then users make their bets. Once the game is over, I update game goals fields. Now I need to assign points for each user like this:

WHEN bet.home_goals = game.home_goals AND bet.away_goals = game.away_goals THEN 2
WHEN game.home_goals > game.away_goals AND bet.home_goals > bet.away_goals THEN 1 
WHEN game.home_goals < game.away_goals AND bet.home_goals < bet.away_goals THEN 1 
WHEN bet.home_goals = bet.away_goals AND game.home_goals = game.away_goals THEN 1 
ELSE 0

It seems that I should create a POST_SAVE singal to update Bet.score for each user based on update of Game.home_goals and Game.away_goals? But I have no idea how to do this


Solution

  • I would recommend staying away from Signals. Generally speaking, you should use Signals when:

    • Multiple pieces of code are interested in the same event;
    • You need to interact with a third-party code (where you have no direct access).

    In your case, only the Bet model is interested in the Game save/change event. You have direct access to the Game class.

    I'm saying that because signals tend to "hide" code/business logic of your application, making the maintenance harder (because it's not immediately obvious you have some code being executed).

    For me it looks like a job for a regular view, where you would add the score of the game and "close" it. Maybe an extra field (could be a BooleanField or DateTimeField) to indicate that the Game is over.

    See an example below:

    forms.py

    from .models import Game
    from django import forms
    from django.db import transaction
    
    class GameForm(forms.ModelForm):
        class Meta:
            model = Game
            fields = ('home_goals', 'away_goals')
    
        # do everything inside the same database transaction to make sure the data is consistent
        @transaction.atomic
        def save(self):            
            game = super().save(commit=True)
            for bet in game.bet_set.all():
                if bet.home_goals == game.home_goals and bet.away_goals == game.away_goals:
                    bet.score = 2
                elif <build_your_logic_here>:
                    bet.score = 1
                else:
                    bet.score = 0
                bet.save()
            return game
    

    views.py

    from django.shortcuts import redirect
    from .forms import GameForm
    
    def end_game(request, game_id):
        game = Game.objects.get(pk=game_id)
        if request.method == 'POST':
            form = GameForm(request.POST, instance=game)
            if form.is_valid():
                form.save()
                return redirect('/gameboard/')  # add here the relevant url where to send the user
        else:
            form = GameForm(instance=game)
    
        return render(request, 'game_form.html', {'form': form})
    

    If for some reason, the score change event happened from multiple points (that is, the model was updated by different parts of your application), in your case the best option would be overriding the save() method, like this:

    models.py

    class Game(models.Model):
        home_team = models.CharField(max_length=200)
        away_team = models.CharField(max_length=200)
        home_goals = models.IntegerField(default=None)
        away_goals = models.IntegerField(default=None)
    
        def save(self, *args, **kwargs):
            # call the save method
            super().save(*args, **kwargs)
    
            # execute your extra logic 
            for bet in self.bet_set.all():
                if bet.home_goals == self.home_goals and bet.away_goals == self.away_goals:
                    bet.score = 2
                # rest of the if/else logic
                bet.save()
    

    This would be a similar implementation than a Signal, but I would say more explicit. As I mentioned, I do not think this is the best implementation for your problem. This could potentially slow down your application, because this for loop would be executed every time you save a Game instance.

    But, if you want to learn more about Signals, I've wrote a blog post about it: How to Create Django Signals.