add: валидатор форма, парсера и моделей (4) избежания дублей при вызове save() модели в обход админки и парсера
This commit is contained in:
@@ -239,12 +239,15 @@
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.utils.text import slugify
|
from django.core.exceptions import ValidationError
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
from filer.fields.file import FilerFileField
|
from filer.fields.file import FilerFileField
|
||||||
from frontend.utils import make_slug
|
from frontend.utils import make_slug, validate_and_raise_for_duplicates
|
||||||
from lpon_site.settings import KEY_SYNONYM
|
from lpon_site.settings import KEY_SYNONYM
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -765,10 +768,16 @@ class TbLabel(models.Model):
|
|||||||
2. Если статья не привязана - создаём новую автоматически
|
2. Если статья не привязана - создаём новую автоматически
|
||||||
3. Генерируем технический заголовок и slug для статьи
|
3. Генерируем технический заголовок и slug для статьи
|
||||||
"""
|
"""
|
||||||
|
# ===== ВАЛИДАЦИЯ НА ДУБЛИКАТЫ =====
|
||||||
|
# Проверяем ДО работы с синонимами и метаданными!
|
||||||
|
# Страховка: защита от прямого вызова save() минуя админку или (в будущем) парсер
|
||||||
|
validate_and_raise_for_duplicates(instance=self, main_field_name='s_label',
|
||||||
|
metadata_field_name='j_label_metadata')
|
||||||
|
|
||||||
# Определяем новый ли это лейбл или обновление существующего
|
# Определяем новый ли это лейбл или обновление существующего
|
||||||
is_new = self.pk is None
|
is_new = self.pk is None
|
||||||
|
|
||||||
# Получаем старое значение s_label из БД (для случая, если это редактирование, а не созданиее нового лейбла)
|
# Получаем старое значение s_label из БД (для случая, если это редактирование, а не создание нового лейбла)
|
||||||
old_s_label = None
|
old_s_label = None
|
||||||
if not is_new:
|
if not is_new:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -361,3 +361,83 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
|
|||||||
# В будущем сюда можно добавить логирование неожиданных типов
|
# В будущем сюда можно добавить логирование неожиданных типов
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def validate_and_raise_for_duplicates(
|
||||||
|
instance,
|
||||||
|
main_field_name: str,
|
||||||
|
metadata_field_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Валидирует экземпляр модели на дубликаты и выбрасывает ValidationError если найдены.
|
||||||
|
|
||||||
|
Используется в переопределённых методах save() моделей для проверки дубликатов
|
||||||
|
перед сохранением. Получает все необходимые данные из экземпляра модели.
|
||||||
|
|
||||||
|
УНИВЕРСАЛЬНЫЙ ХЕЛПЕР — работает для любых моделей (TbLabel, TbArtist, TbMusicStyle и т.д.)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: Экземпляр модели (self из save методе). Обязателен!
|
||||||
|
main_field_name: Имя основного поля модели ('s_label', 's_artist', 's_style_name'). Обязателен!
|
||||||
|
metadata_field_name: Имя поля метаданных ('j_label_metadata', 'j_artist_metadata'). Обязателен!
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: Если указанные поля не существуют в модели
|
||||||
|
ValidationError: Если найдены совпадения (дубликаты)
|
||||||
|
|
||||||
|
Пример использования в TbLabel.save():
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Валидируем ДО работы с данными!
|
||||||
|
validate_and_raise_for_duplicates(self, 's_label', 'j_label_metadata')
|
||||||
|
# ... остальная логика save()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
Пример использования в TbArtist.save():
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
validate_and_raise_for_duplicates(self, 's_artist', 'j_artist_metadata')
|
||||||
|
# ... остальная логика save()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
"""
|
||||||
|
# Получаем класс модели из экземпляра
|
||||||
|
model_class = instance.__class__
|
||||||
|
|
||||||
|
# Проверяем, что указанные поля существуют в модели
|
||||||
|
for field_name in [main_field_name, metadata_field_name]:
|
||||||
|
if not hasattr(instance, field_name):
|
||||||
|
raise AttributeError(
|
||||||
|
f"{model_class.__name__} instance has no attribute '{field_name}'. "
|
||||||
|
f"Check that main_field_name and metadata_field_name are correct."
|
||||||
|
)
|
||||||
|
|
||||||
|
main_field_value = getattr(instance, main_field_name)
|
||||||
|
|
||||||
|
# Вызываем основной валидатор дубликатов
|
||||||
|
duplicates_result = validate_for_duplicates(
|
||||||
|
model_class=model_class,
|
||||||
|
instance_pk=instance.pk, # None для новых записей
|
||||||
|
main_field_value=main_field_value, # ЗНАЧЕНИЕ основного поля модели
|
||||||
|
metadata_dict=getattr(instance, metadata_field_name), # ЗНАЧЕНИЕ поля метаданных модели
|
||||||
|
main_field_name=main_field_name, # ИМЯ основного поля модели
|
||||||
|
metadata_field_name=metadata_field_name, # ИМЯ поля метаданных модели
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обрабатываем результаты валидации через match-case
|
||||||
|
match duplicates_result.get(VALIDATE_KEY__MATCH_TYPE):
|
||||||
|
case ValidateMatchType.IS_DUPLICATE:
|
||||||
|
# Точный дубликат найден - это критическая ошибка!
|
||||||
|
model_name = model_class.__name__
|
||||||
|
dup_pks = [dup.pk for dup in duplicates_result[VALIDATE_KEY__VALUE]]
|
||||||
|
raise ValidationError(
|
||||||
|
f"{model_name}.save(): КРИТИЧЕСКАЯ ОШИБКА! Дубликат '{main_field_value}' уже существует. "
|
||||||
|
f"PK дубликатов: {dup_pks}. Сохранение отменено!"
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
# Неизвестный тип совпадения или дубликатов нет
|
||||||
|
# Это нормальная ситуация - логируем только если что-то странное
|
||||||
|
if VALIDATE_KEY__MATCH_TYPE in duplicates_result:
|
||||||
|
model_name = model_class.__name__
|
||||||
|
logger.warning(
|
||||||
|
f"{model_name}.save(): Неизвестный тип совпадения: "
|
||||||
|
f"{duplicates_result.get(VALIDATE_KEY__MATCH_TYPE)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user