diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py
index 4c08710..783363a 100644
--- a/lpon_site/frontend/admin.py
+++ b/lpon_site/frontend/admin.py
@@ -430,14 +430,19 @@ class LabelAdminForm(forms.ModelForm):
Кастомная форма для админки лейблов (Label).
Добавляет виджеты CodeMirror для текстовых полей
"""
+
class Meta:
model = TbLabel
fields = ('s_label', 'k_label_to_article', 'j_label_metadata',)
def __init__(self, *args, **kwargs):
"""
- При инициализации формы подгружаем
+ При инициализации формы подгружаем CodeMirror.
+ Получаем request из kwargs, переданных из get_form_kwargs в AdminClass.
"""
+ # Извлекаем request из kwargs если он есть
+ self.request = kwargs.pop('request', None)
+
# Атрибуты для активации CodeMirror редактора
codemirror_attrs = {
'data-codemirror-editor': '1',
@@ -446,6 +451,7 @@ class LabelAdminForm(forms.ModelForm):
super().__init__(*args, **kwargs)
+
# Активируем CodeMirror и устанавливаем классы для реальных полей
self.fields['s_label'].widget = Textarea(attrs={
**codemirror_attrs,
@@ -458,19 +464,23 @@ class LabelAdminForm(forms.ModelForm):
'data-language': 'json',
})
+
def clean(self):
"""
- Валидируем форму: проверяем на совпадения (дубликаты) основного поля s_label
+ Валидируем форму: проверяем на совпадения (дубликаты) основного поля s_label.
+ Используем GET параметр ignore_validate для пропуска валидации при переотправке.
"""
cleaned_data = super().clean()
# Используем универсальный хелпер для проверки дубликатов
# Модель берется автоматически из self.Meta.model
+ # Передаем request для проверки GET параметра ignore_validate
validate_entity_for_admin_form(
self,
cleaned_data,
main_field_name='s_label',
- metadata_field_name='j_label_metadata'
+ metadata_field_name='j_label_metadata',
+ request=self.request,
)
return cleaned_data
@@ -489,6 +499,7 @@ class LabelAdmin(admin.ModelAdmin):
'codemirror/editor.js', # Основной CodeMirror
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
)
+
list_display = ('id', 's_label', 't_label_created')
list_display_links = ('id', 's_label',)
search_fields = ('s_label',)
@@ -515,6 +526,28 @@ class LabelAdmin(admin.ModelAdmin):
}),
)
+ def get_form(self, request, obj=None, **kwargs):
+ """
+ Переопределяем get_form чтобы передать request в форму.
+ Создаем оборачивающий класс который передаст request в __init__.
+ """
+ FormClass = super().get_form(request, obj, **kwargs)
+
+ # Сохраняем request в замыкании для доступа в классе
+ request_ref = request
+
+ class FormWithRequest(FormClass):
+ """Оборачивающий класс который передает request при инстанцировании"""
+ def __init__(form_instance, *args, **init_kwargs):
+ # Добавляем request в kwargs перед вызовом __init__ родителя
+ init_kwargs['request'] = request_ref
+ super().__init__(*args, **init_kwargs)
+
+ return FormWithRequest
+
+
+
+
# ================
# АДМИН-ПАНЕЛЬ ДЛЯ ПРОДАВЦА/SELLER
#
diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py
index 87ac51e..ea77572 100644
--- a/lpon_site/frontend/utils.py
+++ b/lpon_site/frontend/utils.py
@@ -242,26 +242,55 @@ def validate_for_duplicates(
duplicates_found = {VALIDATE_KEY__MODEL: model_class.__name__}
+ # ПОДГОТОВКА: Получаем базовый queryset (все записи кроме текущей, если редактируем)
+ # Это ленивый запрос - запрос выполнится, только когда мы применим фильтры
+ records_to_check = model_class.objects.all()
+ if instance_pk is not None:
+ # При редактировании исключаем текущую запись из поиска, чтобы не найти "самого себя"
+ records_to_check = records_to_check.exclude(pk=instance_pk)
+
# ПРОВЕРКА 1: EXACT MATCH (точное совпадение основного поля)
# Ищем: есть ли другая запись с точно таким же main_field_value?
filter_kwargs = {f"{main_field_name}__exact": normalized_main_value}
- exact_matches = model_class.objects.filter(**filter_kwargs)
- if instance_pk is not None:
- # При редактировании, чтобы не найти "самого себя" как дубликат, исключаем текущую запись из поиска
- exact_matches = exact_matches.exclude(pk=instance_pk)
+ exact_matches = records_to_check.filter(**filter_kwargs)
if exact_matches.exists():
duplicates_found.update({
VALIDATE_KEY__MATCH_TYPE: ValidateMatchType.IS_DUPLICATE,
VALIDATE_KEY__VALUE: exact_matches,
})
return duplicates_found
- # for other_record in exact_matches:
- # other_main_value = str(getattr(other_record, main_field_name, ""))
- # duplicates_found[VALIDATE_KEY__VALUE].append({
- # MATCH__KEY_PK: other_record.pk,
- # 's_label': other_main_value,
- # 'matched_value': normalized_main_value,
- # })
+
+ # ПРОВЕРКА 2: SYNONYM MATCH (совпадение с синонимами в метаданных других записей)
+ # Ищем: есть ли текущее значение main_field в синонимах других записей?
+ # Например, если у записи A синонимы=['Sony Music', 'SME Records'],
+ # а мы добавляем запись B с основным полем 'Sony Music', это совпадение!
+
+ # Используем RawSQL для работы с JSON функциями SQLite
+ # json_each(j_label_metadata, '$.SYNONYM') развертывает массив синонимов в отдельные строки
+ # Это необходимо т.к. Django ORM для SQLite не поддерживает __contains для JSON полей
+ from django.db.models.expressions import RawSQL
+
+ # Строим RawSQL запрос для поиска в JSON массиве синонимов
+ # json_each распарсивает массив и ищет совпадение со значением
+ synonym_matches = records_to_check.annotate(
+ has_synonym=RawSQL(
+ f"""
+ EXISTS (
+ SELECT 1 FROM json_each({metadata_field_name}, '$.{KEY_SYNONYM}')
+ WHERE json_each.value = %s
+ )
+ """,
+ (normalized_main_value,)
+ )
+ ).filter(has_synonym=True)
+
+ # Если найдены совпадения в синонимах - возвращаем все найденные записи
+ if synonym_matches.exists():
+ duplicates_found.update({
+ VALIDATE_KEY__MATCH_TYPE: ValidateMatchType.FIND_IN_SYNONYM,
+ VALIDATE_KEY__VALUE: synonym_matches,
+ })
+ return duplicates_found
# Когда все проверки прошли -- возвращаем пустой словарь
return duplicates_found
@@ -269,7 +298,8 @@ def validate_for_duplicates(
def validate_entity_for_admin_form(form_instance, cleaned_data,
main_field_name='s_label',
- metadata_field_name='j_label_metadata'):
+ metadata_field_name='j_label_metadata',
+ request=None):
"""
Универсальный валидатор для админских форм.
@@ -283,9 +313,10 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
cleaned_data: Очищенные данные формы
main_field_name: Имя основного поля ('s_label', 's_artist', 's_style_name')
metadata_field_name: Имя поля метаданных ('j_label_metadata', 'j_artist_metadata')
+ request: HTTP request объект (опционально, используется для проверки GET параметра ignore_validate)
Raises:
- ValidationError: Если найдены совпадения (дубликаты)
+ ValidationError: Если найдены совпадения (дубликаты) и GET параметр не установлен
Пример использования в LabelAdminForm:
def clean(self):
@@ -294,12 +325,18 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
self,
cleaned_data,
main_field_name='s_label',
- metadata_field_name='j_label_metadata'
+ metadata_field_name='j_label_metadata',
+ request=self.request if hasattr(self, 'request') else None,
)
return cleaned_data
"""
from django.utils.html import mark_safe
+ # ПЕРЕД ВАЛИДАЦИЕЙ: проверяем, нажата ли submit-кнопка с измененным value='ignore_validate'
+ # Если пользователь нажал нашу кнопку подтверждения, она меняет value админских кнопок на 'ignore_validate'
+ if request and any(request.POST.get(btn) == 'ignore_validate' for btn in ['_save', '_addanother', '_continue']):
+ return
+
# Получаем класс модели из метаинформации формы
model_class = form_instance.Meta.model
@@ -326,15 +363,15 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
match_type = result[VALIDATE_KEY__MATCH_TYPE]
duplicates_queryset = result[VALIDATE_KEY__VALUE]
+ # В dup_links формируем ссылки на найденные дубликаты для быстрого перехода в админке
+ dup_links = []
+
# Используем match-case для удобной обработки разных типов совпадений
# С Enum вместо магических чисел код становится самодокументируемым
# В будущем легко добавить новые типы: ValidateMatchType.PARTIAL_MATCH = 2 и т.д.
match match_type:
case ValidateMatchType.IS_DUPLICATE:
# ОБРАБОТКА ТОЧНЫХ ДУБЛИКАТОВ
- # Строим ссылки на найденные дубликаты для быстрого перехода в админке
- dup_links = []
-
for dup in duplicates_queryset:
# Относительная ссылка зависит от режима админки:
# При создании: /admin/app/model/add/ → ../456/change/
@@ -347,12 +384,73 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
# Объединяем все найденные дубликаты в один список
dup_list = ", ".join(dup_links)
- # Выбрасываем ValidationError с HTML ссылками на дубликаты
+ # Для случая IS_DUPLICATE отключена проверка force_ignore_validate, т.к. это критическая ситуация
+ # и проверяемом поле часто unique=True на уровне модели.
raise ValidationError(
mark_safe(
- f"ОШИБКА: Найдено совпадение! "
- f"Отредактируйте {dup_list} "
+ f"ОШИБКА: Найдено ПОЛНОЕ совпадение! "
+ f"Измените название или отредактируйте {dup_list}."
+ )
+ )
+
+ case ValidateMatchType.FIND_IN_SYNONYM:
+ # ОБРАБОТКА СОВПАДЕНИЙ В СИНОНИМАХ
+ for dup in duplicates_queryset:
+ rel_url = f"../{dup.pk}/change/" if form_instance.instance.pk is None else f"../../{dup.pk}/change/"
+ dup_value = getattr(dup, main_field_name, '?')
+ dup_links.append(f"#{dup.pk} '{dup_value}'")
+ dup_list = ", ".join(dup_links)
+
+ # Кнопка подтверждения создания несмотря на синонимы
+ # При клике меняет value всех submit-кнопок на 'ignore_validate' и отправляет форму
+ # Если пользователь потом меняет данные - вотчер вернет оригинальные значения
+ confirmation_button = '''
+
+
+
+ Форма будет переотправлена без проверки синонимов
+
+
+
+ '''
+
+ raise ValidationError(
+ mark_safe(
+ f"ВНИМАНИЕ: Найдено совпадение в синонимах! "
+ f"Проверьте {dup_list} "
f"или используйте синонимы из найденной записи."
+ f"{confirmation_button}"
)
)
@@ -532,3 +630,8 @@ def update_synonyms_in_metadata(
if KEY_SYNONYM in metadata_dict and isinstance(metadata_dict[KEY_SYNONYM], list):
metadata_dict[KEY_SYNONYM] = list(dict.fromkeys(metadata_dict[KEY_SYNONYM]))
+ # ===== СООБЩАЕМ DJANGO ЧТО ПОЛЕ ИЗМЕНИЛОСЬ =====
+ # Для JSONField нужно явно сообщить что мы изменили содержимое
+ # иначе Django может не сохранить изменения
+ setattr(instance, metadata_field_name, metadata_dict)
+
diff --git a/lpon_site/lpon_site/settings.py b/lpon_site/lpon_site/settings.py
index d839b36..5c26ff0 100644
--- a/lpon_site/lpon_site/settings.py
+++ b/lpon_site/lpon_site/settings.py
@@ -328,6 +328,6 @@ VALIDATE_KEY__VALUE = 'MATCH_VALUE'
class ValidateMatchType(IntEnum):
"""Типы совпадений при поиске дубликатов."""
IS_DUPLICATE = 1 # Точное совпадение основного поля (s_label, s_artist и т.д.)
- # PARTIAL_MATCH = 2 # Частичное совпадение
+ FIND_IN_SYNONYM = 2 # Частичное совпадение
# SYNONYM_MATCH = 3 # Совпадение по синониму