diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py index 5082683..0d20cf1 100644 --- a/lpon_site/frontend/utils.py +++ b/lpon_site/frontend/utils.py @@ -332,11 +332,6 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, """ 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 @@ -348,6 +343,9 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, if not main_field_value: return + # Нормализуем основное значение для сравнения (как в validate_for_duplicates) + normalized_main_value = normalize_string(main_field_value) + # Вызываем основной валидатор дубликатов result = validate_for_duplicates( model_class=model_class, @@ -384,8 +382,8 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, # Объединяем все найденные дубликаты в один список dup_list = ", ".join(dup_links) - # Для случая IS_DUPLICATE отключена проверка force_ignore_validate, т.к. это критическая ситуация - # и проверяемом поле часто unique=True на уровне модели. + # Для случая IS_DUPLICATE всегда выбрасываем ошибку, т.к. это критическая ситуация + # и поле часто имеет unique=True на уровне модели. raise ValidationError( mark_safe( f"ОШИБКА: Найдено ПОЛНОЕ совпадение! " @@ -395,37 +393,63 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, 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) + # Проверяем: это запрос с подтверждением (ignore_validate=1) или первоначальная проверка? + if request and request.GET.get('ignore_validate') == '1': + # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь нажал красную кнопку и подтвердил "Я проверил и уверен!") + # Тихо удаляем найденные совпадения из синонимов других записей + for duplicate_record in duplicates_queryset: + # Получаем текущие метаданные записи + dup_metadata = getattr(duplicate_record, metadata_field_name) or {} - # Кнопка подтверждения создания несмотря на синонимы - # При клике вызывает функцию markSubmitButtonsToIgnoreValidation() - # которая добавляет класс force-ignore-validation ко всем submit-кнопкам. - # Вотчер видит этот класс и добавляет onclick обработчик к кнопкам - # для добавления GET параметра ignore_validate=1 перед отправкой формы. - # Весь JS код находится в form-field-watcher.js для чистоты и переиспользования. - confirmation_button = ''' -
+ ''' - raise ValidationError( - mark_safe( - f"ВНИМАНИЕ: Найдено совпадение в синонимах! " - f"Проверьте {dup_list} " - f"или используйте синонимы из найденной записи." - f"{confirmation_button}" + raise ValidationError( + mark_safe( + f"ВНИМАНИЕ: Найдено совпадение в синонимах! " + f"Проверьте {dup_list} " + f"или используйте синонимы из найденной записи." + f"{confirmation_button}" + ) ) - ) case _: # Неизвестный или не обработанный тип совпадения diff --git a/public/static/js/form-field-watcher.js b/public/static/js/form-field-watcher.js index a2d37d3..681f880 100644 --- a/public/static/js/form-field-watcher.js +++ b/public/static/js/form-field-watcher.js @@ -18,120 +18,120 @@ // Функция для добавления GET параметра к form action кнопки function addGetParam(button, key, value) { - const form = button.form; // Находим форму, в которой лежит кнопка - const baseAction = form.getAttribute('action'); // Получаем текущий action + const form = button.form; // Находим форму, в которой лежит кнопка + const baseAction = form.getAttribute('action'); // Получаем текущий action - // Удаляем старый параметр, если он есть - let cleanAction = baseAction.split('?')[0]; + // Удаляем старый параметр, если он есть + let cleanAction = baseAction.split('?')[0]; - // Проверяем, есть ли уже в action другие GET-параметры - const separator = cleanAction === baseAction ? '?' : '&'; + // Проверяем, есть ли уже в action другие GET-параметры + const separator = cleanAction === baseAction ? '?' : '&'; - // Динамически прописываем измененный URL в formAction кнопки - button.formAction = baseAction + separator + key + '=' + value; + // Динамически прописываем измененный URL в formAction кнопки + button.formAction = baseAction + separator + key + '=' + value; } // Функция для добавления класса force-ignore-validation ко всем submit-кнопкам формы // Используется при клике на кнопку "Я проверил и уверен!" function markSubmitButtonsToIgnoreValidation() { - // Находим все submit-кнопки на странице и добавляем им класс - // form-field-watcher.js потом отследит добавление класса через MutationObserver - // и добавит соответствующие onclick обработчики - document.querySelectorAll('input[type=submit]').forEach(function(btn) { - btn.classList.add('force-ignore-validation'); - }); + // Находим все submit-кнопки на странице и добавляем им класс + // form-field-watcher.js потом отследит добавление класса через MutationObserver + // и добавит соответствующие onclick обработчики + document.querySelectorAll('input[type=submit]').forEach(function (btn) { + btn.classList.add('force-ignore-validation'); + }); } -document.addEventListener('DOMContentLoaded', function() { - // Находим все submit-кнопки администратора - let submitButtons = document.querySelectorAll('input[type=submit]'); +document.addEventListener('DOMContentLoaded', function () { + // Находим все submit-кнопки администратора + let submitButtons = document.querySelectorAll('input[type=submit]'); - // Если нет submit-кнопок, выходим (не админская форма) - if (submitButtons.length === 0) { - return; + // Если нет submit-кнопок, выходим (не админская форма) + if (submitButtons.length === 0) { + 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"); ' + + '}'); } + } - // Находим форму - let form = document.querySelector('form'); - if (!form) { - return; - } + // Функция для удаления onclick обработчика + function removeOnclickHandler(btn) { + btn.removeAttribute('onclick'); + } - // Сохраняем оригинальный action формы - let originalAction = form.getAttribute('action'); + // Отслеживаем изменения всех типов полей в форме + let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]'); - // Функция для добавления 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"); ' + - '}'); + // Функция которая срабатывает при любом изменении + 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 на кнопках + // Используем 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 на кнопках - // Используем 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); - } - } - } - }); - }); - - // Настраиваем observer для отслеживания изменения класса - submitButtons.forEach(function(btn) { - observer.observe(btn, { attributes: true, attributeFilter: ['class'] }); - }); + // Настраиваем observer для отслеживания изменения класса + submitButtons.forEach(function (btn) { + observer.observe(btn, {attributes: true, attributeFilter: ['class']}); + }); });