Python – Django signals conditionally update a table against another table

Django signals conditionally update a table against another table… here is a solution to the problem.

Django signals conditionally update a table against another table

I’m working on my first Django project, which is a sports betting game.

Here is my model:

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 created an instance of the game that contained a null value in the target field, and then the user placed a bet.
When the game is over, I update the game target field. Now I need to assign credits to 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 like I should create an POST_SAVE signal to update Bet.score for each user based on updates to Game.home_goals and Game.away_goals? But I don’t know how to do it

Solution

I recommend staying away from signals. In general, you should use signals when:

  • Multiple pieces of code are interested in the same event;
  • You need to interact with third-party code that you don’t have direct access to.

In your case, only the Bet model is interested in Game save/change events. You can access the Game class directly.

I say this because signals tend to “hide” your application’s code/business logic, making maintenance more difficult (because you can’t immediately tell that you’re executing some code).

To me, this looks like the work of a regular View where you can add game scores and “turn it off”. There may be an extra field (which can be BooleanField or DateTimeField) to indicate that the Game has ended.

Take a look at the following example:

Form .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 occurs at multiple points (i.e., the model is updated by different parts of the application), in your case, the best option is to override the save() method, like this:

Model .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 to the signal, but I would be more explicit. As I mentioned, I don’t think this is the best solution to your problem. This can slow down your application because this for loop is executed every time you save a Game instance.

However, if you want to know more about the signal, I wrote a blog post about it: How to Create Django Signals .

Related Problems and Solutions