Как проверить модели Django с отношениями внешних ключей перед сохранением записей?

Я работаю над проектом Django, где мне нужно проверить модель перед сохранением, основываясь на значениях в связанных с ней моделях. Я столкнулся с этой проблемой при извлечении приложения из проекта, использующего старую версию Django (3.1), в отдельный проект Django 5.1, после чего на всех классах валидации, использующих данные связанных моделей, возникла ошибка «ValueError: „Model...“ instance needs to have a primary key value before this relationship can be used».

Для демонстрации и упрощения у меня есть модель Reservation, которая ссылается на несколько объектов Guest через внешний ключ. Чтобы бронирование было действительным и сохранялось, все гости, связанные с ним, должны быть не моложе 18 лет, и один и тот же гость не может быть указан в двух или более бронированиях ( проверка, объединяющая данные из родительской и дочерней модели, является моей основной проблемой).

Однако ни одна из этих записей (ни бронирование, ни гости) еще не была сохранена в базе данных. Мне нужно выполнить эту проверку эффективно и чисто, желательно таким образом, чтобы логика проверки была отделена от самих моделей.

Как я могу подойти к этому сценарию проверки? Каковы лучшие практики проверки несохраненных отношений внешних ключей в Django?

Вот упрощенная версия моей установки:

Файл: 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)"

Файл: 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.")

def validate_guest_bookings(reservation):
    """
    Validate that no guest in the reservation is already booked for the same period.
    """
    for guest in reservation.guests.all():
        overlapping_reservations = Reservation.objects.filter(
            Q(guests=guest) &
            (
                Q(check_in_date__lte=reservation.check_out_date) &
                Q(check_out_date__gte=reservation.check_in_date)
            )
        ).exclude(id=reservation.id)  # Exclude the current reservation if updating

        if overlapping_reservations.exists():
            raise ValidationError(
                f"Guest '{guest.name}' is already booked for a reservation in the period "
                f"from {reservation.check_in_date} to {reservation.check_out_date}."
            )

Вопрос:

Каким образом лучше всего структурировать этот вид валидации в админке Django? Я открыт для использования пользовательских методов модели, валидации формы или сигналов, но я предпочитаю хранить логику в отдельном файле для лучшей организации. Есть ли другие подходы, которые я должен рассмотреть?

Любые примеры или советы будут высоко оценены!

Вы можете добавить 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)"

Все ModelForm, полученные на основе этой модели, будут проверять, что age не меньше 18. Поскольку ModelAdmin использует ModelForm, они также подтверждают это.

В ModelAdmin вы можете переместить Guest в 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)

но я предпочитаю хранить логику в отдельном файле для лучшей организации. Есть ли другие подходы, которые я должен рассмотреть?

Модель отвечает за достоверность данных, поэтому добавление валидаторов к полям модели - это, пожалуй, лучший способ сделать это. Всевозможные «продукты», возникающие из моделей, такие как ModelForms, ModelSerializers, ModelAdmins, ModelResources и т. д., будут видеть валидаторы и действовать соответствующим образом.

Многомодельная валидация может (технически) происходить в нескольких местах:

  • Модель(и)
  • Форма/модель
  • View

Что более «django-nic» в таком случае, как ваш, я не могу сказать точно - это может быть выбор между моделью и формой. Модели обычно проверяют значения одного экземпляра, в то время как для форм более привычно проверять критерии, охватывающие несколько экземпляров и/или моделей.

Однако все сводится к условиям использования и предпочтениям. Если вам нужны разные многоуровневые проверки одной и той же модели (моделей) в разных сценариях, вы, вероятно, сделаете это в форме. Но если вам нужна одна и та же валидация постоянно, независимо от того, как/когда/и т.д. происходит обращение к модели, то имеет смысл поместить ее в модель IMO.

Я бы предпочел сделать это в модели, и вот грубая эталонная реализация:

class Reservation(models.Model):
    ...

    def validate_guest_ages(self):
        for guest in self.guests:
            if guest.age <= 18:
                raise ValidationError("A guest's age cannot be below 18")

    def clean(self):
        self.validate_guest_ages()

    def save(self, *args, **kwargs):
        self.clean()
        super().save(*args, **kwargs)
Вернуться на верх