From 01710200b2c3016989961a47c27fdb1aa0938af9 Mon Sep 17 00:00:00 2001 From: erjemin Date: Fri, 26 Jun 2026 18:21:55 +0300 Subject: [PATCH] =?UTF-8?q?minor:=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lpon_site/frontend/utils.py | 442 ++++++++++++++++++------------------ 1 file changed, 216 insertions(+), 226 deletions(-) diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py index eeaa745..b9b213e 100644 --- a/lpon_site/frontend/utils.py +++ b/lpon_site/frontend/utils.py @@ -609,260 +609,250 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, metadata_field_name=metadata_field_name, ) + if VALIDATE_KEY__MATCH_TYPE not in result: + return # Нет совпадений, продолжаем обработку формы + # Обрабатываем результаты проверки в зависимости от типа найденного совпадения - if VALIDATE_KEY__MATCH_TYPE in result: - match_type = result[VALIDATE_KEY__MATCH_TYPE] - duplicates_queryset = result[VALIDATE_KEY__VALUE] + match_type = result[VALIDATE_KEY__MATCH_TYPE] + duplicates_queryset = result[VALIDATE_KEY__VALUE] - # В dup_links формируем ссылки на найденные дубликаты для быстрого перехода в админке - dup_links = [] + # В dup_links формируем ссылки на найденные дубликаты для быстрого перехода в админке + dup_links = [] - # Используем match-case для удобной обработки разных типов совпадений - # С Enum вместо магических чисел код становится самодокументируемым - # В будущем легко добавить новые типы: ValidateMatchType.PARTIAL_MATCH = 2 и т.д. - match match_type: - case ValidateMatchType.IS_DUPLICATE: - # ОБРАБОТКА ТОЧНЫХ ДУБЛИКАТОВ + # Используем match-case для удобной обработки разных типов совпадений + # С Enum вместо магических чисел код становится самодокументируемым + # В будущем легко добавить новые типы: ValidateMatchType.PARTIAL_MATCH = 2 и т.д. + match match_type: + case ValidateMatchType.IS_DUPLICATE: + # ОБРАБОТКА ТОЧНЫХ ДУБЛИКАТОВ + for dup in duplicates_queryset: + # Относительная ссылка зависит от режима админки: + # При создании: /admin/app/model/add/ → ../456/change/ + # При редактировании: /admin/app/model/123/change/ → ../..456/change/ + 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) + + # Для случая IS_DUPLICATE всегда выбрасываем ошибку, т.к. это критическая ситуация + # и поле часто имеет unique=True на уровне модели. + raise ValidationError( + mark_safe( + f"ОШИБКА: Найдено ПОЛНОЕ совпадение! " + f"Измените название или отредактируйте {dup_list}." + ) + ) + + case ValidateMatchType.FIND_IN_SYNONYM: + # ОБРАБОТКА СОВПАДЕНИЙ В СИНОНИМАХ (основное поле совпадает с синонимами других) + # Проверяем: это запрос с подтверждением (ignore_validate=1) или первоначальная проверка? + if request and request.GET.get('ignore_validate') == '1': + # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь нажал красную кнопку и подтвердил "Я проверил и уверен!") + # Тихо удаляем найденные совпадения из синонимов других записей + remove_conflicting_synonyms_from_duplicates( + duplicates_queryset, + metadata_field_name, + [normalized_main_value], # Удаляем основное поле текущей записи + ) + # Выходим без ошибки в админку, т.к. пользователь "проверил и уверен!" + # Конфликтующие синонимы удалены, запись сохранится нормально + return + + else: + # РЕЖИМ: ПЕРВОНАЧАЛЬНАЯ ПРОВЕРКА + # Показываем пользователю красную кнопку подтверждения с информацией о совпадениях for dup in duplicates_queryset: - # Относительная ссылка зависит от режима админки: - # При создании: /admin/app/model/add/ → ../456/change/ - # При редактировании: /admin/app/model/123/change/ → ../..456/change/ 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) - # Для случая IS_DUPLICATE всегда выбрасываем ошибку, т.к. это критическая ситуация - # и поле часто имеет unique=True на уровне модели. + # Кнопка подтверждения создания несмотря на синонимы + # При клике вызывает функцию markSubmitButtonsToIgnoreValidation() + # которая добавляет класс force-ignore-validation ко всем submit-кнопкам. + # Вотчер видит этот класс и добавляет onclick обработчик к кнопкам + # для добавления GET параметра ignore_validate=1 перед отправкой формы. + # Весь JS код находится в form-field-watcher.js для чистоты и переиспользования. raise ValidationError( mark_safe( - f"ОШИБКА: Найдено ПОЛНОЕ совпадение! " - f"Измените название или отредактируйте {dup_list}." + f"ВНИМАНИЕ: Найдено совпадение в синонимах! " + f"Проверьте {dup_list} " + f"или используйте синонимы из найденной записи." + f"
" + f" " + f" Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить." + f"
" ) ) - case ValidateMatchType.FIND_IN_SYNONYM: - # ОБРАБОТКА СОВПАДЕНИЙ В СИНОНИМАХ (основное поле совпадает с синонимами других) - # Проверяем: это запрос с подтверждением (ignore_validate=1) или первоначальная проверка? - if request and request.GET.get('ignore_validate') == '1': - # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь нажал красную кнопку и подтвердил "Я проверил и уверен!") - # Тихо удаляем найденные совпадения из синонимов других записей - remove_conflicting_synonyms_from_duplicates( - duplicates_queryset, - metadata_field_name, - [normalized_main_value], # Удаляем основное поле текущей записи - ) - # Выходим без ошибки в админку, т.к. пользователь "проверил и уверен!" - # Конфликтующие синонимы удалены, запись сохранится нормально - return + case ValidateMatchType.EXACT_SYNONYM_MATCH: + # ОБРАБОТКА СОВПАДЕНИЙ СИНОНИМОВ (синонимы текущей записи совпадают с синонимами других) + # Проверяем: это запрос с подтверждением (ignore_validate=1) или первоначальная проверка? + if request and request.GET.get('ignore_validate') == '1': + # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь нажал красную кнопку и подтвердил "Я проверил и уверен!") + # Тихо удаляем из других записей те синонимы, которые совпадают с синонимами текущей. + remove_conflicting_synonyms_from_duplicates( + duplicates_queryset, + metadata_field_name, + metadata_dict.get(KEY_SYNONYM) or [], # Удаляем все синонимы текущей записи + ) + # Выходим без ошибки в админку, т.к. пользователь "проверил и уверен!" + # Конфликтующие синонимы удалены, запись сохранится нормально + return - else: - # РЕЖИМ: ПЕРВОНАЧАЛЬНАЯ ПРОВЕРКА - # Показываем пользователю красную кнопку подтверждения с информацией о совпадениях + else: + # РЕЖИМ: ПЕРВОНАЧАЛЬНАЯ ПРОВЕРКА + # Обрабатываем на Python (без доп запросов к БД) - duplicates_queryset уже в памяти + # Для каждого синонима текущей записи ищем, в каких записях он есть + + # Собираем текущие синонимы с нормализацией + current_synonyms_list = metadata_dict.get(KEY_SYNONYM) or [] + + # Строим словарь: {нормализованный синоним: {оригинальный синоним, запись1, запись2, ...}} + synonym_to_records = {} + for current_syn in current_synonyms_list: + normalized_syn = normalize_string(current_syn) + if normalized_syn not in synonym_to_records: + synonym_to_records[normalized_syn] = { + 'original': current_syn, + 'records': [] + } + + # Ищем этот синоним в метаданных других записей 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) + dup_metadata = getattr(dup, metadata_field_name) or {} + dup_synonyms = dup_metadata.get(KEY_SYNONYM) or [] - # Кнопка подтверждения создания несмотря на синонимы - # При клике вызывает функцию markSubmitButtonsToIgnoreValidation() - # которая добавляет класс force-ignore-validation ко всем submit-кнопкам. - # Вотчер видит этот класс и добавляет onclick обработчик к кнопкам - # для добавления GET параметра ignore_validate=1 перед отправкой формы. - # Весь JS код находится в form-field-watcher.js для чистоты и переиспользования. - confirmation_button = ''' -
- - Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить. -
- ''' + # Проверяем: есть ли текущий синоним в синонимах этой записи + for dup_syn in dup_synonyms: + if normalize_string(dup_syn) == normalized_syn: + # Добавляем запись если ее еще нет в списке + 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_link = f"#{dup.pk} '{dup_value}'" - raise ValidationError( - mark_safe( - f"ВНИМАНИЕ: Найдено совпадение в синонимах! " - f"Проверьте {dup_list} " - f"или используйте синонимы из найденной записи." - f"{confirmation_button}" - ) + # Проверяем что эту запись еще не добавили для этого синонима + if dup_link not in synonym_to_records[normalized_syn]['records']: + synonym_to_records[normalized_syn]['records'].append(dup_link) + break + + # Строим текст с детализацией по каждому синониму + synonym_details = [] + for normalized_syn, info in synonym_to_records.items(): + original_syn = info['original'] + records = info['records'] + if records: # Только если этот синоним найден в других записях + records_html = ", ".join(records) + synonym_details.append(f"'{original_syn}' найден в: {records_html}") + + # Объединяем все детали в один список + synonym_details_text = "
".join(synonym_details) if synonym_details else "Синонимы не найдены" + + # Кнопка подтверждения создания несмотря на совпадение синонимов + raise ValidationError( + mark_safe( + f"ВНИМАНИЕ: Найдено совпадение синонимов!
" + f"Синонимы совпадают:
{synonym_details_text}
" + f"Проверьте и уточните синонимы если нужно." + f"
" + f" " + f" Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить." + f"
" ) + ) - case ValidateMatchType.EXACT_SYNONYM_MATCH: - # ОБРАБОТКА СОВПАДЕНИЙ СИНОНИМОВ (синонимы текущей записи совпадают с синонимами других) - # Проверяем: это запрос с подтверждением (ignore_validate=1) или первоначальная проверка? - if request and request.GET.get('ignore_validate') == '1': - # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь нажал красную кнопку и подтвердил "Я проверил и уверен!") - # Тихо удаляем из других записей те синонимы, которые совпадают с синонимами текущей. - remove_conflicting_synonyms_from_duplicates( - duplicates_queryset, - metadata_field_name, - metadata_dict.get(KEY_SYNONYM) or [], # Удаляем все синонимы текущей записи + case ValidateMatchType.PARTIAL_MATCH: + # ОБРАБОТКА ЧАСТИЧНЫХ СОВПАДЕНИЙ (слова совпадают) + # Это просто схожесть, не критично. Показываем предупреждение, но не блокируем сохранение. + # При подтверждении просто сохраняем без каких-либо изменений в других записях. + + if request and request.GET.get('ignore_validate') == '1': + # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь подтвердил что ознакомлен) + # Тихо сохраняем запись, ничего не меняя в других записях + 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"#{dup.pk} '{dup_value}'") + dup_list = ", ".join(dup_links) + + # Кнопка для игнорирования предупреждения (без критичности) + raise ValidationError( + mark_safe( + f"ИНФОРМАЦИЯ: Найдены похожие записи со схожими словами! " + f"Проверьте: {dup_list} " + f"Это просто информация, не ошибка. Но неточности на сайте могут рассмешить" + f" (или огорчить) пользователей." + f"
" + f" " + f" Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить." + f"
" ) - # Выходим без ошибки в админку, т.к. пользователь "проверил и уверен!" - # Конфликтующие синонимы удалены, запись сохранится нормально - return + ) - else: - # РЕЖИМ: ПЕРВОНАЧАЛЬНАЯ ПРОВЕРКА - # Обрабатываем на Python (без доп запросов к БД) - duplicates_queryset уже в памяти - # Для каждого синонима текущей записи ищем, в каких записях он есть + case ValidateMatchType.PARTIAL_MATCH__RISK_SHORT_WORDS: + # ОБРАБОТКА ЧАСТИЧНЫХ СОВПАДЕНИЙ С ВЫСОКИМ РИСКОМ (очень короткие слова/синонимы) + # Найдены совпадения, но среди слов есть очень короткие (< MIN_SYNONYM_WORD_LENGTH) + # Это может быть как вероятное совпадение (B'Z, XL) так и ложное срабатывание (А, И) + # Показываем ещё более серьезное предупреждение о необходимости проверки - # Собираем текущие синонимы с нормализацией - current_synonyms_list = metadata_dict.get(KEY_SYNONYM) or [] + if request and request.GET.get('ignore_validate') == '1': + # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь подтвердил что ознакомлен с рисками) + # Тихо сохраняем запись, ничего не меняя в других записях + return - # Строим словарь: {нормализованный синоним: {оригинальный синоним, запись1, запись2, ...}} - synonym_to_records = {} - for current_syn in current_synonyms_list: - normalized_syn = normalize_string(current_syn) - if normalized_syn not in synonym_to_records: - synonym_to_records[normalized_syn] = { - 'original': current_syn, - 'records': [] - } + 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"#{dup.pk} '{dup_value}'") + dup_list = ", ".join(dup_links) - # Ищем этот синоним в метаданных других записей - for dup in duplicates_queryset: - dup_metadata = getattr(dup, metadata_field_name) or {} - dup_synonyms = dup_metadata.get(KEY_SYNONYM) or [] - - # Проверяем: есть ли текущий синоним в синонимах этой записи - for dup_syn in dup_synonyms: - if normalize_string(dup_syn) == normalized_syn: - # Добавляем запись если ее еще нет в списке - 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_link = f"#{dup.pk} '{dup_value}'" - - # Проверяем что эту запись еще не добавили для этого синонима - if dup_link not in synonym_to_records[normalized_syn]['records']: - synonym_to_records[normalized_syn]['records'].append(dup_link) - break - - # Строим текст с детализацией по каждому синониму - synonym_details = [] - for normalized_syn, info in synonym_to_records.items(): - original_syn = info['original'] - records = info['records'] - if records: # Только если этот синоним найден в других записях - records_html = ", ".join(records) - synonym_details.append(f"'{original_syn}' найден в: {records_html}") - - # Объединяем все детали в один список - synonym_details_text = "
".join(synonym_details) if synonym_details else "Синонимы не найдены" - - # Кнопка подтверждения создания несмотря на совпадение синонимов - confirmation_button = ''' -
- - Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить. -
- ''' - - raise ValidationError( - mark_safe( - f"ВНИМАНИЕ: Найдено совпадение синонимов!
" - f"Синонимы совпадают:
{synonym_details_text}
" - f"Проверьте и уточните синонимы если нужно." - f"{confirmation_button}" - ) + # Кнопка для подтверждения с указанием на риск + raise ValidationError( + mark_safe( + f"ВНИМАНИЕ: ВЫСОКИЙ РИСК ОШИБКИ! Найдены похожие записи с ОЧЕНЬ КОРОТКИМИ словами! " + f"Проверьте ОЧЕНЬ ВНИМАТЕЛЬНО: {dup_list} " + f"Совпадения могут быть двухбуквенными и даже однобуквенными словами (например:" + f" \"B'Z\" → \"B Z\" или \"R&B\" → \"R B\" ). ОЧЕНЬ ВЕЛИК РИСК ложных срабатываний!" + f"
" + f" " + f" Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить." + f"
" ) + ) - case ValidateMatchType.PARTIAL_MATCH: - # ОБРАБОТКА ЧАСТИЧНЫХ СОВПАДЕНИЙ (слова совпадают) - # Это просто схожесть, не критично. Показываем предупреждение, но не блокируем сохранение. - # При подтверждении просто сохраняем без каких-либо изменений в других записях. - - if request and request.GET.get('ignore_validate') == '1': - # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь подтвердил что ознакомлен) - # Тихо сохраняем запись, ничего не меняя в других записях - return + case _: + # Неизвестный или не обработанный тип совпадения + # В будущем сюда можно добавить логирование неожиданных типов + pass - 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"#{dup.pk} '{dup_value}'") - dup_list = ", ".join(dup_links) - - # Кнопка для игнорирования предупреждения (без критичности) - confirmation_button = ''' -
- - Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить. -
- ''' - - raise ValidationError( - mark_safe( - f"ИНФОРМАЦИЯ: Найдены похожие записи со схожими словами! " - f"Проверьте: {dup_list} " - f"Это просто информация, не ошибка. Но неточности на сайте могут рассмешить" - f" (или огорчить) пользователей. {confirmation_button}" - ) - ) - - case ValidateMatchType.PARTIAL_MATCH__RISK_SHORT_WORDS: - # ОБРАБОТКА ЧАСТИЧНЫХ СОВПАДЕНИЙ С ВЫСОКИМ РИСКОМ (очень короткие слова/синонимы) - # Найдены совпадения, но среди слов есть очень короткие (< MIN_SYNONYM_WORD_LENGTH) - # Это может быть как вероятное совпадение (B'Z, XL) так и ложное срабатывание (А, И) - # Показываем ещё более серьезное предупреждение о необходимости проверки - - if request and request.GET.get('ignore_validate') == '1': - # РЕЖИМ: ОБХОД ВАЛИДАЦИИ (пользователь подтвердил что ознакомлен с рисками) - # Тихо сохраняем запись, ничего не меняя в других записях - 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"#{dup.pk} '{dup_value}'") - dup_list = ", ".join(dup_links) - - # Кнопка для подтверждения с указанием на риск - confirmation_button = ''' -
- - Теперь нажмите стандартные кнопки сохранения снизу, чтобы сохранить. -
- ''' - - raise ValidationError( - mark_safe( - f"ВНИМАНИЕ: ВЫСОКИЙ РИСК ОШИБКИ! Найдены похожие записи с ОЧЕНЬ КОРОТКИМИ словами! " - f"Проверьте ОЧЕНЬ ВНИМАТЕЛЬНО: {dup_list} " - f"Совпадения могут быть с однобуквенными или двухбуквенными словами (например: 'B'Z' → 'B Z'). " - f"Велик риск ложных срабатываний! {confirmation_button}" - ) - ) - - case _: - # Неизвестный или не обработанный тип совпадения - # В будущем сюда можно добавить логирование неожиданных типов - pass + return def validate_and_raise_for_duplicates(