Как проверить модели 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)
но я предпочитаю хранить логику в отдельном файле для лучшей организации. Есть ли другие подходы, которые я должен рассмотреть?
Модель отвечает за достоверность данных, поэтому добавление валидаторов к полям модели - это, пожалуй, лучший способ сделать это. Всевозможные «продукты», возникающие из моделей, такие как ModelForm
s, ModelSerializer
s, ModelAdmin
s, ModelResource
s и т. д., будут видеть валидаторы и действовать соответствующим образом.
Многомодельная валидация может (технически) происходить в нескольких местах:
- Модель(и)
- Форма/модель
- 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)