mod: валидатор форм, парсера и моделей (09) уделение дублей в синонимах других записей при редактировании через админку
This commit is contained in:
@@ -332,11 +332,6 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
|
|||||||
"""
|
"""
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
|
|
||||||
# ПЕРЕД ВАЛИДАЦИЕЙ: проверяем GET параметр ignore_validate
|
|
||||||
# Если пользователь нажал красную кнопку, addGetParam добавит GET параметр к URL
|
|
||||||
if request and request.GET.get('ignore_validate') == '1':
|
|
||||||
return
|
|
||||||
|
|
||||||
# Получаем класс модели из метаинформации формы
|
# Получаем класс модели из метаинформации формы
|
||||||
model_class = form_instance.Meta.model
|
model_class = form_instance.Meta.model
|
||||||
|
|
||||||
@@ -348,6 +343,9 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
|
|||||||
if not main_field_value:
|
if not main_field_value:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Нормализуем основное значение для сравнения (как в validate_for_duplicates)
|
||||||
|
normalized_main_value = normalize_string(main_field_value)
|
||||||
|
|
||||||
# Вызываем основной валидатор дубликатов
|
# Вызываем основной валидатор дубликатов
|
||||||
result = validate_for_duplicates(
|
result = validate_for_duplicates(
|
||||||
model_class=model_class,
|
model_class=model_class,
|
||||||
@@ -384,8 +382,8 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
|
|||||||
# Объединяем все найденные дубликаты в один список
|
# Объединяем все найденные дубликаты в один список
|
||||||
dup_list = ", ".join(dup_links)
|
dup_list = ", ".join(dup_links)
|
||||||
|
|
||||||
# Для случая IS_DUPLICATE отключена проверка force_ignore_validate, т.к. это критическая ситуация
|
# Для случая IS_DUPLICATE всегда выбрасываем ошибку, т.к. это критическая ситуация
|
||||||
# и проверяемом поле часто unique=True на уровне модели.
|
# и поле часто имеет unique=True на уровне модели.
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
mark_safe(
|
mark_safe(
|
||||||
f"ОШИБКА: Найдено ПОЛНОЕ совпадение! "
|
f"ОШИБКА: Найдено ПОЛНОЕ совпадение! "
|
||||||
@@ -395,37 +393,63 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
|
|||||||
|
|
||||||
case ValidateMatchType.FIND_IN_SYNONYM:
|
case ValidateMatchType.FIND_IN_SYNONYM:
|
||||||
# ОБРАБОТКА СОВПАДЕНИЙ В СИНОНИМАХ
|
# ОБРАБОТКА СОВПАДЕНИЙ В СИНОНИМАХ
|
||||||
for dup in duplicates_queryset:
|
# Проверяем: это запрос с подтверждением (ignore_validate=1) или первоначальная проверка?
|
||||||
rel_url = f"../{dup.pk}/change/" if form_instance.instance.pk is None else f"../../{dup.pk}/change/"
|
if request and request.GET.get('ignore_validate') == '1':
|
||||||
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)
|
for duplicate_record in duplicates_queryset:
|
||||||
|
# Получаем текущие метаданные записи
|
||||||
|
dup_metadata = getattr(duplicate_record, metadata_field_name) or {}
|
||||||
|
|
||||||
# Кнопка подтверждения создания несмотря на синонимы
|
# Если в метаданных есть синонимы, удаляем из них текущее значение
|
||||||
# При клике вызывает функцию markSubmitButtonsToIgnoreValidation()
|
if KEY_SYNONYM in dup_metadata and isinstance(dup_metadata[KEY_SYNONYM], list):
|
||||||
# которая добавляет класс force-ignore-validation ко всем submit-кнопкам.
|
# Удаляем нормализованное значение из списка синонимов
|
||||||
# Вотчер видит этот класс и добавляет onclick обработчик к кнопкам
|
dup_metadata[KEY_SYNONYM] = [
|
||||||
# для добавления GET параметра ignore_validate=1 перед отправкой формы.
|
syn for syn in dup_metadata[KEY_SYNONYM]
|
||||||
# Весь JS код находится в form-field-watcher.js для чистоты и переиспользования.
|
if normalize_string(syn) != normalized_main_value
|
||||||
confirmation_button = '''
|
]
|
||||||
<div class="confirmation-button-container">
|
# Сохраняем обновленные метаданные
|
||||||
<button type="button" onclick="markSubmitButtonsToIgnoreValidation();">
|
setattr(duplicate_record, metadata_field_name, dup_metadata)
|
||||||
|
# Сохраняем запись (обновляем только поле с метаданными)
|
||||||
|
duplicate_record.save(update_fields=[metadata_field_name])
|
||||||
|
# Выходим без ошибки в админку, т.к. пользователь "проверил и уверен!"
|
||||||
|
# Синонимы из удалены, запись сохранится нормально
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
# РЕЖИМ: ПЕРВОНАЧАЛЬНАЯ ПРОВЕРКА
|
||||||
|
# Показываем пользователю красную кнопку подтверждения с информацией о совпадениях
|
||||||
|
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"<big><a href='{rel_url}'>#{dup.pk} '{dup_value}'</a></big>")
|
||||||
|
dup_list = ", ".join(dup_links)
|
||||||
|
|
||||||
|
# Кнопка подтверждения создания несмотря на синонимы
|
||||||
|
# При клике вызывает функцию markSubmitButtonsToIgnoreValidation()
|
||||||
|
# которая добавляет класс force-ignore-validation ко всем submit-кнопкам.
|
||||||
|
# Вотчер видит этот класс и добавляет onclick обработчик к кнопкам
|
||||||
|
# для добавления GET параметра ignore_validate=1 перед отправкой формы.
|
||||||
|
# Весь JS код находится в form-field-watcher.js для чистоты и переиспользования.
|
||||||
|
confirmation_button = '''
|
||||||
|
<div class="confirmation-button-container">
|
||||||
|
<button type="button" onclick="markSubmitButtonsToIgnoreValidation();">
|
||||||
<big>Я проверил и уверен!</big><br/>
|
<big>Я проверил и уверен!</big><br/>
|
||||||
Сохранить, несмотря на синонимы.<br/>
|
Сохранить, несмотря на синонимы.<br/>
|
||||||
<i>Точно совпадения в синонимах других записей будут удалены.</i>
|
<i>Точные совпадения в синонимах других записей будут удалены.</i>
|
||||||
</button>
|
</button>
|
||||||
<em>Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить.</em>
|
<em>Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить.</em>
|
||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
mark_safe(
|
mark_safe(
|
||||||
f"ВНИМАНИЕ: Найдено совпадение в синонимах! "
|
f"ВНИМАНИЕ: Найдено совпадение в синонимах! "
|
||||||
f"Проверьте {dup_list} "
|
f"Проверьте {dup_list} "
|
||||||
f"или используйте синонимы из найденной записи."
|
f"или используйте синонимы из найденной записи."
|
||||||
f"{confirmation_button}"
|
f"{confirmation_button}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
# Неизвестный или не обработанный тип совпадения
|
# Неизвестный или не обработанный тип совпадения
|
||||||
|
|||||||
@@ -18,120 +18,120 @@
|
|||||||
|
|
||||||
// Функция для добавления GET параметра к form action кнопки
|
// Функция для добавления GET параметра к form action кнопки
|
||||||
function addGetParam(button, key, value) {
|
function addGetParam(button, key, value) {
|
||||||
const form = button.form; // Находим форму, в которой лежит кнопка
|
const form = button.form; // Находим форму, в которой лежит кнопка
|
||||||
const baseAction = form.getAttribute('action'); // Получаем текущий action
|
const baseAction = form.getAttribute('action'); // Получаем текущий action
|
||||||
|
|
||||||
// Удаляем старый параметр, если он есть
|
// Удаляем старый параметр, если он есть
|
||||||
let cleanAction = baseAction.split('?')[0];
|
let cleanAction = baseAction.split('?')[0];
|
||||||
|
|
||||||
// Проверяем, есть ли уже в action другие GET-параметры
|
// Проверяем, есть ли уже в action другие GET-параметры
|
||||||
const separator = cleanAction === baseAction ? '?' : '&';
|
const separator = cleanAction === baseAction ? '?' : '&';
|
||||||
|
|
||||||
// Динамически прописываем измененный URL в formAction кнопки
|
// Динамически прописываем измененный URL в formAction кнопки
|
||||||
button.formAction = baseAction + separator + key + '=' + value;
|
button.formAction = baseAction + separator + key + '=' + value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для добавления класса force-ignore-validation ко всем submit-кнопкам формы
|
// Функция для добавления класса force-ignore-validation ко всем submit-кнопкам формы
|
||||||
// Используется при клике на кнопку "Я проверил и уверен!"
|
// Используется при клике на кнопку "Я проверил и уверен!"
|
||||||
function markSubmitButtonsToIgnoreValidation() {
|
function markSubmitButtonsToIgnoreValidation() {
|
||||||
// Находим все submit-кнопки на странице и добавляем им класс
|
// Находим все submit-кнопки на странице и добавляем им класс
|
||||||
// form-field-watcher.js потом отследит добавление класса через MutationObserver
|
// form-field-watcher.js потом отследит добавление класса через MutationObserver
|
||||||
// и добавит соответствующие onclick обработчики
|
// и добавит соответствующие onclick обработчики
|
||||||
document.querySelectorAll('input[type=submit]').forEach(function(btn) {
|
document.querySelectorAll('input[type=submit]').forEach(function (btn) {
|
||||||
btn.classList.add('force-ignore-validation');
|
btn.classList.add('force-ignore-validation');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Находим все submit-кнопки администратора
|
// Находим все submit-кнопки администратора
|
||||||
let submitButtons = document.querySelectorAll('input[type=submit]');
|
let submitButtons = document.querySelectorAll('input[type=submit]');
|
||||||
|
|
||||||
// Если нет submit-кнопок, выходим (не админская форма)
|
// Если нет submit-кнопок, выходим (не админская форма)
|
||||||
if (submitButtons.length === 0) {
|
if (submitButtons.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим форму
|
||||||
|
let form = document.querySelector('form');
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем оригинальный action формы
|
||||||
|
let originalAction = form.getAttribute('action');
|
||||||
|
|
||||||
|
// Функция для добавления onclick обработчика
|
||||||
|
function addOnclickHandler(btn) {
|
||||||
|
// Проверяем есть ли уже onclick
|
||||||
|
if (!btn.getAttribute('onclick')) {
|
||||||
|
btn.setAttribute('onclick', 'if (this.classList.contains("force-ignore-validation")) { ' +
|
||||||
|
'const form = this.form; ' +
|
||||||
|
'const baseAction = form.getAttribute("action") || ""; ' +
|
||||||
|
'let cleanAction = baseAction.split("?")[0]; ' +
|
||||||
|
'const separator = cleanAction === baseAction ? "?" : "&"; ' +
|
||||||
|
'form.setAttribute("action", baseAction + separator + "ignore_validate=1"); ' +
|
||||||
|
'}');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Находим форму
|
// Функция для удаления onclick обработчика
|
||||||
let form = document.querySelector('form');
|
function removeOnclickHandler(btn) {
|
||||||
if (!form) {
|
btn.removeAttribute('onclick');
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем оригинальный action формы
|
// Отслеживаем изменения всех типов полей в форме
|
||||||
let originalAction = form.getAttribute('action');
|
let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]');
|
||||||
|
|
||||||
// Функция для добавления onclick обработчика
|
// Функция которая срабатывает при любом изменении
|
||||||
function addOnclickHandler(btn) {
|
function handleChange() {
|
||||||
// Проверяем есть ли уже onclick
|
// При изменении любого поля:
|
||||||
if (!btn.getAttribute('onclick')) {
|
// 1. Удаляем класс force-ignore-validation (кнопки вернут нормальный цвет)
|
||||||
btn.setAttribute('onclick', 'if (this.classList.contains("force-ignore-validation")) { ' +
|
// 2. Удаляем onclick обработчик
|
||||||
'const form = this.form; ' +
|
// 3. Восстанавливаем оригинальный action формы
|
||||||
'const baseAction = form.getAttribute("action") || ""; ' +
|
submitButtons.forEach(function (btn) {
|
||||||
'let cleanAction = baseAction.split("?")[0]; ' +
|
if (btn.classList.contains('force-ignore-validation')) {
|
||||||
'const separator = cleanAction === baseAction ? "?" : "&"; ' +
|
btn.classList.remove('force-ignore-validation');
|
||||||
'form.setAttribute("action", baseAction + separator + "ignore_validate=1"); ' +
|
removeOnclickHandler(btn);
|
||||||
'}');
|
}
|
||||||
|
});
|
||||||
|
form.setAttribute('action', originalAction);
|
||||||
|
|
||||||
|
// Скрываем сообщения об ошибках валидации
|
||||||
|
let errorNotes = document.querySelectorAll('.errornote, .errorlist');
|
||||||
|
errorNotes.forEach(function (errorElement) {
|
||||||
|
errorElement.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
formInputs.forEach(function (input) {
|
||||||
|
// Слушаем оба события: 'change' для обычных input/select
|
||||||
|
// и 'input' для CodeMirror и других редакторов
|
||||||
|
input.addEventListener('change', handleChange);
|
||||||
|
input.addEventListener('input', handleChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Мониторим изменение класса force-ignore-validation на кнопках
|
||||||
|
// Используем MutationObserver для отслеживания добавления/удаления класса
|
||||||
|
const observer = new MutationObserver(function (mutations) {
|
||||||
|
mutations.forEach(function (mutation) {
|
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||||
|
let btn = mutation.target;
|
||||||
|
if (btn.tagName === 'INPUT' && btn.type === 'submit') {
|
||||||
|
// Если класс добавлен - навешиваем onclick
|
||||||
|
if (btn.classList.contains('force-ignore-validation')) {
|
||||||
|
addOnclickHandler(btn);
|
||||||
|
}
|
||||||
|
// Если класс удален - удаляем onclick
|
||||||
|
else if (btn.getAttribute('onclick')) {
|
||||||
|
removeOnclickHandler(btn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для удаления onclick обработчика
|
|
||||||
function removeOnclickHandler(btn) {
|
|
||||||
btn.removeAttribute('onclick');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отслеживаем изменения всех типов полей в форме
|
|
||||||
let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]');
|
|
||||||
|
|
||||||
// Функция которая срабатывает при любом изменении
|
|
||||||
function handleChange() {
|
|
||||||
// При изменении любого поля:
|
|
||||||
// 1. Удаляем класс force-ignore-validation (кнопки вернут нормальный цвет)
|
|
||||||
// 2. Удаляем onclick обработчик
|
|
||||||
// 3. Восстанавливаем оригинальный action формы
|
|
||||||
submitButtons.forEach(function(btn) {
|
|
||||||
if (btn.classList.contains('force-ignore-validation')) {
|
|
||||||
btn.classList.remove('force-ignore-validation');
|
|
||||||
removeOnclickHandler(btn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
form.setAttribute('action', originalAction);
|
|
||||||
|
|
||||||
// Скрываем сообщения об ошибках валидации
|
|
||||||
let errorNotes = document.querySelectorAll('.errornote, .errorlist');
|
|
||||||
errorNotes.forEach(function(errorElement) {
|
|
||||||
errorElement.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formInputs.forEach(function(input) {
|
|
||||||
// Слушаем оба события: 'change' для обычных input/select
|
|
||||||
// и 'input' для CodeMirror и других редакторов
|
|
||||||
input.addEventListener('change', handleChange);
|
|
||||||
input.addEventListener('input', handleChange);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Мониторим изменение класса force-ignore-validation на кнопках
|
// Настраиваем observer для отслеживания изменения класса
|
||||||
// Используем MutationObserver для отслеживания добавления/удаления класса
|
submitButtons.forEach(function (btn) {
|
||||||
const observer = new MutationObserver(function(mutations) {
|
observer.observe(btn, {attributes: true, attributeFilter: ['class']});
|
||||||
mutations.forEach(function(mutation) {
|
});
|
||||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
|
||||||
let btn = mutation.target;
|
|
||||||
if (btn.tagName === 'INPUT' && btn.type === 'submit') {
|
|
||||||
// Если класс добавлен - навешиваем onclick
|
|
||||||
if (btn.classList.contains('force-ignore-validation')) {
|
|
||||||
addOnclickHandler(btn);
|
|
||||||
}
|
|
||||||
// Если класс удален - удаляем onclick
|
|
||||||
else if (btn.getAttribute('onclick')) {
|
|
||||||
removeOnclickHandler(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Настраиваем observer для отслеживания изменения класса
|
|
||||||
submitButtons.forEach(function(btn) {
|
|
||||||
observer.observe(btn, { attributes: true, attributeFilter: ['class'] });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user