add: валидатор форм для избежания дублей (2) оформлено в хелпер
This commit is contained in:
@@ -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"<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"или используйте синонимы из найденной записи."
|
||||
)
|
||||
)
|
||||
# Другие типы дубликатов обработаны будут позже
|
||||
# Используем универсальный хелпер для проверки дубликатов
|
||||
# Модель берется автоматически из 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
|
||||
|
||||
|
||||
@@ -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"<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
|
||||
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 # Совпадение по синониму
|
||||
|
||||
|
||||
Reference in New Issue
Block a user