diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py
index eadc45d..4c08710 100644
--- a/lpon_site/frontend/admin.py
+++ b/lpon_site/frontend/admin.py
@@ -2,22 +2,15 @@
# Регистрируем модели с удобным интерфейсом.
from django import forms
-from django.db import models
-from django.forms import TextInput, Textarea, URLField
+from django.forms import Textarea
from django.contrib import admin
from django.utils.html import format_html, mark_safe
-from django.core.exceptions import ValidationError
-from django.urls import reverse
-from lpon_site.settings import (
- VALIDATE_KEY__MATCH_TYPE, VALIDATE_KEY__MODEL, VALIDATE_KEY__VALUE,
- VALIDATE_VAL__IS_DUPLICATE
-)
from easy_thumbnails.files import get_thumbnailer
from .models import (
TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller,
TbOffer, TbSource, TbOfferHistory, TbMusicStyle
)
-from .utils import validate_for_duplicates
+from .utils import validate_entity_for_admin_form
# ============================================================================
# АДМИНИСТРИРОВАНИЕ TbImage
@@ -467,54 +460,18 @@ class LabelAdminForm(forms.ModelForm):
def clean(self):
"""
- Валидируем форму: проверяем на дубликаты основного поля s_label
+ Валидируем форму: проверяем на совпадения (дубликаты) основного поля s_label
"""
cleaned_data = super().clean()
- # Получаем значения из очищенных данных
- s_label = cleaned_data.get('s_label')
- j_label_metadata = cleaned_data.get('j_label_metadata') or {}
-
- if s_label:
- # Вызываем валидатор для проверки дубликатов.
- # Возвращает словарь: {VALIDATE_KEY__MATCH_TYPE: type, VALIDATE_KEY__VALUE: queryset, ...} или пустой словарь, если дубликатов нет
- result = validate_for_duplicates(
- model_class=TbLabel,
- instance_pk=self.instance.pk, # pk экземпляра (None для новых)
- main_field_value=s_label,
- metadata_dict=j_label_metadata,
- main_field_name='s_label',
- metadata_field_name='j_label_metadata',
- )
-
- # Если найдены дубликаты, обрабатываем по типу совпадения
- if VALIDATE_KEY__MATCH_TYPE in result:
- match_type = result[VALIDATE_KEY__MATCH_TYPE]
- duplicates_queryset = result[VALIDATE_KEY__VALUE]
-
- if match_type == VALIDATE_VAL__IS_DUPLICATE:
- # Точное совпадение основного поля — критическая ошибка
- # Строим ссылки на дубликаты для быстрого перехода в админке
- dup_links = []
- for dup in duplicates_queryset:
- # Получаем относительный URL для редактирования дубликата
- # reverse вернет абсолютный путь, берем часть после /admin/
- admin_url = reverse('admin:frontend_tblabel_change', args=[dup.pk])
- # Делаем ссылку относительной (убираем начальный слэш)
- rel_url = admin_url.lstrip('/')
- dup_links.append(
- f"#{dup.pk} '{dup.s_label}'"
- )
-
- dup_list = ", ".join(dup_links)
- raise ValidationError(
- mark_safe(
- f"ОШИБКА: Найден точный дубликат лейбла! "
- f"Отредактируйте {dup_list} "
- f"или используйте синонимы из найденной записи."
- )
- )
- # Другие типы дубликатов обработаны будут позже
+ # Используем универсальный хелпер для проверки дубликатов
+ # Модель берется автоматически из self.Meta.model
+ validate_entity_for_admin_form(
+ self,
+ cleaned_data,
+ main_field_name='s_label',
+ metadata_field_name='j_label_metadata'
+ )
return cleaned_data
diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py
index ccf8fb5..6cdb014 100644
--- a/lpon_site/frontend/utils.py
+++ b/lpon_site/frontend/utils.py
@@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError
from lpon_site.settings import (
SLUG_MAX_LENGTH,
VALIDATE_KEY__MATCH_TYPE, VALIDATE_KEY__MODEL, VALIDATE_KEY__VALUE,
- VALIDATE_VAL__IS_DUPLICATE
+ ValidateMatchType
)
logger = logging.getLogger(__name__)
@@ -251,7 +251,7 @@ def validate_for_duplicates(
exact_matches = exact_matches.exclude(pk=instance_pk)
if exact_matches.exists():
duplicates_found.update({
- VALIDATE_KEY__MATCH_TYPE: VALIDATE_VAL__IS_DUPLICATE,
+ VALIDATE_KEY__MATCH_TYPE: ValidateMatchType.IS_DUPLICATE,
VALIDATE_KEY__VALUE: exact_matches,
})
return duplicates_found
@@ -265,3 +265,102 @@ def validate_for_duplicates(
# Когда все проверки прошли -- возвращаем пустой словарь
return duplicates_found
+
+
+def validate_entity_for_admin_form(form_instance, cleaned_data,
+ main_field_name='s_label',
+ metadata_field_name='j_label_metadata'):
+ """
+ Универсальный валидатор для админских форм.
+
+ Проверяет сущность на совпадения (дубликаты) с уже существующими записями.
+ Выбрасывает ValidationError с кликабельными ссылками на найденные дубликаты.
+
+ Используется во всех админских forms: LabelAdminForm, ArtistAdminForm, MusicStyleAdminForm и т.д.
+
+ Args:
+ form_instance: Экземпляр формы (self из clean методе)
+ cleaned_data: Очищенные данные формы
+ main_field_name: Имя основного поля ('s_label', 's_artist', 's_style_name')
+ metadata_field_name: Имя поля метаданных ('j_label_metadata', 'j_artist_metadata')
+
+ Raises:
+ ValidationError: Если найдены совпадения (дубликаты)
+
+ Пример использования в LabelAdminForm:
+ def clean(self):
+ cleaned_data = super().clean()
+ validate_entity_for_admin_form(
+ self,
+ cleaned_data,
+ main_field_name='s_label',
+ metadata_field_name='j_label_metadata'
+ )
+ return cleaned_data
+ """
+ from django.urls import reverse
+ from django.utils.html import mark_safe
+
+ # Получаем класс модели из метаинформации формы
+ model_class = form_instance.Meta.model
+
+ # Получаем значения из формы
+ main_field_value = cleaned_data.get(main_field_name)
+ metadata_dict = cleaned_data.get(metadata_field_name) or {}
+
+ # Если основное поле не заполнено, пропускаем валидацию
+ if not main_field_value:
+ return
+
+ # Вызываем основной валидатор дубликатов
+ result = validate_for_duplicates(
+ model_class=model_class,
+ instance_pk=form_instance.instance.pk,
+ main_field_value=main_field_value,
+ metadata_dict=metadata_dict,
+ main_field_name=main_field_name,
+ metadata_field_name=metadata_field_name,
+ )
+
+ # Обрабатываем результаты проверки в зависимости от типа найденного совпадения
+ if VALIDATE_KEY__MATCH_TYPE in result:
+ match_type = result[VALIDATE_KEY__MATCH_TYPE]
+ duplicates_queryset = result[VALIDATE_KEY__VALUE]
+
+ # Используем match-case для удобной обработки разных типов совпадений
+ # С Enum вместо магических чисел код становится самодокументируемым
+ # В будущем легко добавить новые типы: ValidateMatchType.PARTIAL_MATCH = 2 и т.д.
+ match match_type:
+ case ValidateMatchType.IS_DUPLICATE:
+ # ОБРАБОТКА ТОЧНЫХ ДУБЛИКАТОВ
+ # Строим ссылки на найденные дубликаты для быстрого перехода в админке
+ dup_links = []
+ for dup in duplicates_queryset:
+ # Получаем admin URL автоматически через Django meta
+ model_name = model_class._meta.model_name
+ app_label = model_class._meta.app_label
+ admin_url = reverse(f'admin:{app_label}_{model_name}_change', args=[dup.pk])
+ # Делаем ссылку относительной (убираем начальный слэш для универсальности)
+ rel_url = admin_url.lstrip('/')
+
+ # Получаем значение основного поля из дубликата
+ dup_value = getattr(dup, main_field_name, '?')
+ dup_links.append(f"#{dup.pk} '{dup_value}'")
+
+ # Объединяем все найденные дубликаты в один список
+ dup_list = ", ".join(dup_links)
+
+ # Выбрасываем ValidationError с HTML ссылками на дубликаты
+ raise ValidationError(
+ mark_safe(
+ f"ОШИБКА: Найдено совпадение! "
+ f"Отредактируйте {dup_list} "
+ f"или используйте синонимы из найденной записи."
+ )
+ )
+
+ case _:
+ # Неизвестный или не обработанный тип совпадения
+ # В будущем сюда можно добавить логирование неожиданных типов
+ pass
+
diff --git a/lpon_site/lpon_site/settings.py b/lpon_site/lpon_site/settings.py
index 1738f15..d839b36 100644
--- a/lpon_site/lpon_site/settings.py
+++ b/lpon_site/lpon_site/settings.py
@@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
from pathlib import Path
import environ
import os
+from enum import IntEnum
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -318,8 +319,15 @@ SLUG_MAX_LENGTH = 60
# Ключи для типовых параметров в мета-полях (для TbLabel, TbSeller, TbArtist, TbMusicStyle и т.д.)
KEY_SYNONYM = 'SYNONYM'
-# ДЛЯ МАТЧИНГА (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
+# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
VALIDATE_KEY__MODEL = 'MODEL'
VALIDATE_KEY__VALUE = 'MATCH_VALUE'
-VALIDATE_VAL__IS_DUPLICATE = 'exact' # Строгое совпадение (по имени, без учета регистра)
+
+# Типы совпадений как Enum (более информативно чем просто числа, работает в match-case)
+class ValidateMatchType(IntEnum):
+ """Типы совпадений при поиске дубликатов."""
+ IS_DUPLICATE = 1 # Точное совпадение основного поля (s_label, s_artist и т.д.)
+ # PARTIAL_MATCH = 2 # Частичное совпадение
+ # SYNONYM_MATCH = 3 # Совпадение по синониму
+