diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py index 2c01203..76c1060 100644 --- a/lpon_site/frontend/models.py +++ b/lpon_site/frontend/models.py @@ -239,12 +239,15 @@ from django.db import models 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.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 import datetime +import logging + +logger = logging.getLogger(__name__) # ============================================================================ @@ -765,10 +768,16 @@ class TbLabel(models.Model): 2. Если статья не привязана - создаём новую автоматически 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 - # Получаем старое значение s_label из БД (для случая, если это редактирование, а не созданиее нового лейбла) + # Получаем старое значение s_label из БД (для случая, если это редактирование, а не создание нового лейбла) old_s_label = None if not is_new: try: diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py index e82233b..810a7f6 100644 --- a/lpon_site/frontend/utils.py +++ b/lpon_site/frontend/utils.py @@ -361,3 +361,83 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, # В будущем сюда можно добавить логирование неожиданных типов 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)}" + ) +