Search code examples
djangoforeign-keysone-to-one

Django: Validate relationships among ForeignKeys


I have a model Run with two ForeignKeys, signup and report,

class Run(models.Model):
    signup = models.ForeignKey(Signup, on_delete=models.CASCADE, related_name="runs")
    report = models.ForeignKey(Report, on_delete=models.CASCADE, related_name="runs")
    kind = ... 

pointing to models, which in turn are related to another model, Training,

class Report(models.Model):
    training = models.OneToOneField(
        Training, on_delete=models.CASCADE, primary_key=True
    )
    cash_at_start = ....


class Signup(models.Model):
    training = models.ForeignKey(
        Training, on_delete=models.CASCADE, related_name="signups"
    )
    participant = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="signups"
    )

When creating a Run I would like to make sure, that it's signup and report are for the same Training, i.e. that report.training == signup.training.

What is this kind of validation called? And how would I achieve it?

Also, I am happy to learn other ways to implement this, if another database structure would be better.


Solution

  • Here are the docs that describe the validation process on models.

    Please note that this validation usually only happens with ModelForms (when calling form.is_valid()). Manually creating an object and using save() doesn't trigger this validation if you don't call model_instance.full_clean() method. It's aimed at the user, not the developer.

    According to the mentioned docs, my suggestion is to use the clean method:

    class Run(models.Model):
        signup = models.ForeignKey(Signup, on_delete=models.CASCADE, related_name="runs")
        report = models.ForeignKey(Report, on_delete=models.CASCADE, related_name="runs")
    
        def clean(self):
            if self.signup.training != self.report.training:
                # Note: adding an error code is best practice :)
                raise ValidationError('some message', code='some_error_code')
    

    EDIT #1: Not using ModelForm

    You don't have to use the ModelForm to have validation on the model. Let's say you obtained Signup and Report instances from somewhere. Then you can instantiate and validate your Run instance like this:

    run = Run(signup=signup, report=report)
    # Raises ValidationErrors
    run.full_clean()
    # If all is fine, call save
    run.save()