pythondjangovalidationunsaved-data

How can I enforce a minimum age constraint and manage related models in Django?


I am working on a Django project where I need to validate a model before saving it, based on values in its related models. I came up with this issue while extracting an app from an project using an old Django version (3.1) to a separate Django 5.1 project, then there error "ValueError: 'Model...' instance needs to have a primary key value before this relationship can be used" raised on all validation classes that used related model data.

For demonstration and simplification purposes, I have a Reservation model that references multiple Guest objects via a foreign key. For the reservation to be valid and be saved, all guests linked to it must be at least 18 years old.

However, none of these records (neither the reservation nor the guests) have been saved to the database yet. I need to perform this validation efficiently and cleanly, preferably in a way that keeps the validation logic separated from the models themselves.

How can I approach this validation scenario? What are the best practices for validating unsaved foreign key relationships in Django?

Here is a simplified version of my setup:

File: models.py

from django.db import models

class Reservation(models.Model):
    check_in_date = models.DateField()
    check_out_date = models.DateField()

    def __str__(self):
        return f"Reservation from {self.check_in_date} to {self.check_out_date}"

class Guest(models.Model):
    name = models.CharField(max_length=255)
    age = models.PositiveIntegerField()
    reservation = models.ForeignKey(
        Reservation,
        related_name="guests",
        on_delete=models.CASCADE
    )

    def __str__(self):
        return f"{self.name} ({self. Age} years old)"

File: validation.py

from django.core.exceptions import ValidationError

def validate_reservation_and_guests(reservation):
    """
    Validate that all guests in the reservation are at least 18 years old.
    """
    for guest in reservation.guests.all():
        if guest.age < 18:
            raise ValidationError("All guests must be at least 18 years old.")

Question:

What is the best way to structure this kind of validation in Django admin? I am open to using custom model methods, form validation, or signals, but I prefer to keep the logic in a separate file for better organization. Are there other approaches I should consider?

Any examples or advice would be greatly appreciated!


Solution

  • You can add a MinValueValidator [Django-doc]:

    from django.core.validators import MinValueValidator
    
    
    class Guest(models.Model):
        name = models.CharField(max_length=255)
        age = models.PositiveIntegerField(
            validators=[
                MinValueValidator(18, 'All guests must be at least 18 years old.')
            ]
        )
        reservation = models.ForeignKey(
            Reservation, related_name='guests', on_delete=models.CASCADE
        )
    
        def __str__(self):
            return f"{self.name} ({self. Age} years old)"

    All ModelForms derived from this model will thus check that the age is at least 18. Since a ModelAdmin uses a ModelForm, these thus also validate this.

    In the ModelAdmin, you can then move the Guests to an inline [Django-doc]:

    from django.contrib import admin
    from myapp.models import Guest, Reservation
    
    
    class GuestInline(admin.TabularInline):
        model = Guest
    
    
    class ReservationAdmin(admin.ModelAdmin):
        inlines = [
            GuestInline,
        ]
    
    
    admin.site.register(Reservation, ReservationAdmin)

    but I prefer to keep the logic in a separate file for better organization. Are there other approaches I should consider?

    A model is the main responsible to ensure the data is valid, so adding validators to the model fields is the probably the best way to do this. All sorts of "products" arising from models like ModelForms, ModelSerializers, ModelAdmins, ModelResources, etc. will then all see the validators and act accordingly.