add: валидатор форм для избежания дублей (2) оформлено в хелпер
This commit is contained in:
@@ -2,22 +2,15 @@
|
|||||||
# Регистрируем модели с удобным интерфейсом.
|
# Регистрируем модели с удобным интерфейсом.
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db import models
|
from django.forms import Textarea
|
||||||
from django.forms import TextInput, Textarea, URLField
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html, mark_safe
|
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 easy_thumbnails.files import get_thumbnailer
|
||||||
from .models import (
|
from .models import (
|
||||||
TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller,
|
TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller,
|
||||||
TbOffer, TbSource, TbOfferHistory, TbMusicStyle
|
TbOffer, TbSource, TbOfferHistory, TbMusicStyle
|
||||||
)
|
)
|
||||||
from .utils import validate_for_duplicates
|
from .utils import validate_entity_for_admin_form
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# АДМИНИСТРИРОВАНИЕ TbImage
|
# АДМИНИСТРИРОВАНИЕ TbImage
|
||||||
@@ -467,55 +460,19 @@ class LabelAdminForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
Валидируем форму: проверяем на дубликаты основного поля s_label
|
Валидируем форму: проверяем на совпадения (дубликаты) основного поля s_label
|
||||||
"""
|
"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
# Получаем значения из очищенных данных
|
# Используем универсальный хелпер для проверки дубликатов
|
||||||
s_label = cleaned_data.get('s_label')
|
# Модель берется автоматически из self.Meta.model
|
||||||
j_label_metadata = cleaned_data.get('j_label_metadata') or {}
|
validate_entity_for_admin_form(
|
||||||
|
self,
|
||||||
if s_label:
|
cleaned_data,
|
||||||
# Вызываем валидатор для проверки дубликатов.
|
|
||||||
# Возвращает словарь: {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',
|
main_field_name='s_label',
|
||||||
metadata_field_name='j_label_metadata',
|
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"<big><a href='{rel_url}'>#{dup.pk} '{dup.s_label}'</a></big>"
|
|
||||||
)
|
|
||||||
|
|
||||||
dup_list = ", ".join(dup_links)
|
|
||||||
raise ValidationError(
|
|
||||||
mark_safe(
|
|
||||||
f"ОШИБКА: Найден точный дубликат лейбла! "
|
|
||||||
f"Отредактируйте {dup_list} "
|
|
||||||
f"или используйте синонимы из найденной записи."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Другие типы дубликатов обработаны будут позже
|
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
# Админ для лейбла (Label)
|
# Админ для лейбла (Label)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from lpon_site.settings import (
|
from lpon_site.settings import (
|
||||||
SLUG_MAX_LENGTH,
|
SLUG_MAX_LENGTH,
|
||||||
VALIDATE_KEY__MATCH_TYPE, VALIDATE_KEY__MODEL, VALIDATE_KEY__VALUE,
|
VALIDATE_KEY__MATCH_TYPE, VALIDATE_KEY__MODEL, VALIDATE_KEY__VALUE,
|
||||||
VALIDATE_VAL__IS_DUPLICATE
|
ValidateMatchType
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -251,7 +251,7 @@ def validate_for_duplicates(
|
|||||||
exact_matches = exact_matches.exclude(pk=instance_pk)
|
exact_matches = exact_matches.exclude(pk=instance_pk)
|
||||||
if exact_matches.exists():
|
if exact_matches.exists():
|
||||||
duplicates_found.update({
|
duplicates_found.update({
|
||||||
VALIDATE_KEY__MATCH_TYPE: VALIDATE_VAL__IS_DUPLICATE,
|
VALIDATE_KEY__MATCH_TYPE: ValidateMatchType.IS_DUPLICATE,
|
||||||
VALIDATE_KEY__VALUE: exact_matches,
|
VALIDATE_KEY__VALUE: exact_matches,
|
||||||
})
|
})
|
||||||
return duplicates_found
|
return duplicates_found
|
||||||
@@ -265,3 +265,102 @@ def validate_for_duplicates(
|
|||||||
|
|
||||||
# Когда все проверки прошли -- возвращаем пустой словарь
|
# Когда все проверки прошли -- возвращаем пустой словарь
|
||||||
return duplicates_found
|
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"<big><a href='{rel_url}'>#{dup.pk} '{dup_value}'</a></big>")
|
||||||
|
|
||||||
|
# Объединяем все найденные дубликаты в один список
|
||||||
|
dup_list = ", ".join(dup_links)
|
||||||
|
|
||||||
|
# Выбрасываем ValidationError с HTML ссылками на дубликаты
|
||||||
|
raise ValidationError(
|
||||||
|
mark_safe(
|
||||||
|
f"ОШИБКА: Найдено совпадение! "
|
||||||
|
f"Отредактируйте {dup_list} "
|
||||||
|
f"или используйте синонимы из найденной записи."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
# Неизвестный или не обработанный тип совпадения
|
||||||
|
# В будущем сюда можно добавить логирование неожиданных типов
|
||||||
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import environ
|
import environ
|
||||||
import os
|
import os
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
@@ -318,8 +319,15 @@ SLUG_MAX_LENGTH = 60
|
|||||||
# Ключи для типовых параметров в мета-полях (для TbLabel, TbSeller, TbArtist, TbMusicStyle и т.д.)
|
# Ключи для типовых параметров в мета-полях (для TbLabel, TbSeller, TbArtist, TbMusicStyle и т.д.)
|
||||||
KEY_SYNONYM = 'SYNONYM'
|
KEY_SYNONYM = 'SYNONYM'
|
||||||
|
|
||||||
# ДЛЯ МАТЧИНГА (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
|
# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
|
||||||
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
|
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
|
||||||
VALIDATE_KEY__MODEL = 'MODEL'
|
VALIDATE_KEY__MODEL = 'MODEL'
|
||||||
VALIDATE_KEY__VALUE = 'MATCH_VALUE'
|
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 # Совпадение по синониму
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user