From 9fc0af2c2e5f939df1f1a765c711a24d622749d1 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Tue, 11 Nov 2025 00:49:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D1=81=D0=BC=D0=BE=D1=82=D1=80=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=20email=20=D1=81=20=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: Поиск "team_x3m@" неправильно находит клиента "Наталья natulj@bk.ru" Причина: Использовался простой icontains для всех случаев Решение: Добавлена функция determine_search_strategy() которая определяет стратегию поиска на основе содержимого query: 1. email_prefix: query заканчивается на @ (например "team_x3m@") → Используется istartswith вместо icontains → Найдёт только email, начинающиеся с "team_x3m@" → НЕ найдёт "natulj@bk.ru" ✓ 2. email_domain: query начинается с @ (например "@bk") → Использует icontains для поиска по домену → Найдёт все *@bk.ru, *@bk.com и т.д. 3. email_full: query содержит обе части (например "test@bk.ru") → Поиск по полному email адресу 4. universal: query без @, 3+ символов (например "natul") → Поиск везде: по имени И по email → Это позволит найти "Наталья" и "natulj@bk.ru" 5. name_only: очень короткие запросы (1-2 символа) → Только поиск по имени (чтобы не было ложных срабатываний) Добавлены 23 unit-теста для покрытия всех сценариев: - email_prefix cases: team_x3m@, user_name@, test123@ - email_domain cases: @bk, @bk.ru, @mail.google.com - email_full cases: test@bk.ru, test@bk, user.name@mail.example.com - universal cases: natul, abc, наталь, Test123 - name_only cases: t, te, на - edge cases: пустая строка, @, множественные @ Все 23 теста проходят успешно ✓ Примеры работы после изменения: - team_x3m@ → ищет email^=team_x3m (НЕ найдёт natulj@bk.ru) - @bk → ищет все *@bk.* - natul → ищет везде (имя + email) - te → ищет только по имени (2 символа мало для email) - test@bk.ru → ищет test@bk.ru 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- myproject/customers/tests.py | 160 +++++++++++++++++++++++++++++++++++ myproject/customers/views.py | 87 ++++++++++++++++--- 2 files changed, 233 insertions(+), 14 deletions(-) create mode 100644 myproject/customers/tests.py diff --git a/myproject/customers/tests.py b/myproject/customers/tests.py new file mode 100644 index 0000000..96530c7 --- /dev/null +++ b/myproject/customers/tests.py @@ -0,0 +1,160 @@ +from django.test import TestCase +from .views import determine_search_strategy + + +class DetermineSearchStrategyTestCase(TestCase): + """ + Тесты для функции determine_search_strategy(). + + Проверяют, что функция правильно определяет стратегию поиска + на основе содержимого query. + """ + + # ===== email_prefix: query заканчивается на @ ===== + def test_email_prefix_simple(self): + """Query "team_x3m@" должен вернуть ('email_prefix', 'team_x3m')""" + strategy, search_value = determine_search_strategy('team_x3m@') + self.assertEqual(strategy, 'email_prefix') + self.assertEqual(search_value, 'team_x3m') + + def test_email_prefix_with_domain_symbol(self): + """Query "user_name@" должен вернуть ('email_prefix', 'user_name')""" + strategy, search_value = determine_search_strategy('user_name@') + self.assertEqual(strategy, 'email_prefix') + self.assertEqual(search_value, 'user_name') + + def test_email_prefix_with_numbers(self): + """Query "test123@" должен вернуть ('email_prefix', 'test123')""" + strategy, search_value = determine_search_strategy('test123@') + self.assertEqual(strategy, 'email_prefix') + self.assertEqual(search_value, 'test123') + + # ===== email_domain: query начинается с @ ===== + def test_email_domain_simple(self): + """Query "@bk" должен вернуть ('email_domain', 'bk')""" + strategy, search_value = determine_search_strategy('@bk') + self.assertEqual(strategy, 'email_domain') + self.assertEqual(search_value, 'bk') + + def test_email_domain_with_extension(self): + """Query "@bk.ru" должен вернуть ('email_domain', 'bk.ru')""" + strategy, search_value = determine_search_strategy('@bk.ru') + self.assertEqual(strategy, 'email_domain') + self.assertEqual(search_value, 'bk.ru') + + def test_email_domain_with_multiple_dots(self): + """Query "@mail.google.com" должен вернуть ('email_domain', 'mail.google.com')""" + strategy, search_value = determine_search_strategy('@mail.google.com') + self.assertEqual(strategy, 'email_domain') + self.assertEqual(search_value, 'mail.google.com') + + # ===== email_full: query содержит и локальную часть, и домен ===== + def test_email_full_simple(self): + """Query "test@bk.ru" должен вернуть ('email_full', 'test@bk.ru')""" + strategy, search_value = determine_search_strategy('test@bk.ru') + self.assertEqual(strategy, 'email_full') + self.assertEqual(search_value, 'test@bk.ru') + + def test_email_full_partial(self): + """Query "test@bk" должен вернуть ('email_full', 'test@bk')""" + strategy, search_value = determine_search_strategy('test@bk') + self.assertEqual(strategy, 'email_full') + self.assertEqual(search_value, 'test@bk') + + def test_email_full_complex(self): + """Query "user.name@mail.example.com" должен вернуть ('email_full', ...)""" + strategy, search_value = determine_search_strategy('user.name@mail.example.com') + self.assertEqual(strategy, 'email_full') + self.assertEqual(search_value, 'user.name@mail.example.com') + + # ===== universal: query без @, 3+ символов ===== + def test_universal_three_chars(self): + """Query "natul" (5 символов) должен вернуть ('universal', 'natul')""" + strategy, search_value = determine_search_strategy('natul') + self.assertEqual(strategy, 'universal') + self.assertEqual(search_value, 'natul') + + def test_universal_three_chars_exact(self): + """Query "abc" (3 символа) должен вернуть ('universal', 'abc')""" + strategy, search_value = determine_search_strategy('abc') + self.assertEqual(strategy, 'universal') + self.assertEqual(search_value, 'abc') + + def test_universal_cyrillic(self): + """Query "наталь" (6 символов) должен вернуть ('universal', 'наталь')""" + strategy, search_value = determine_search_strategy('наталь') + self.assertEqual(strategy, 'universal') + self.assertEqual(search_value, 'наталь') + + def test_universal_mixed(self): + """Query "Test123" (7 символов) должен вернуть ('universal', 'Test123')""" + strategy, search_value = determine_search_strategy('Test123') + self.assertEqual(strategy, 'universal') + self.assertEqual(search_value, 'Test123') + + # ===== name_only: очень короткие запросы (< 3 символов без @) ===== + def test_name_only_single_char(self): + """Query "t" должен вернуть ('name_only', 't')""" + strategy, search_value = determine_search_strategy('t') + self.assertEqual(strategy, 'name_only') + self.assertEqual(search_value, 't') + + def test_name_only_two_chars(self): + """Query "te" должен вернуть ('name_only', 'te')""" + strategy, search_value = determine_search_strategy('te') + self.assertEqual(strategy, 'name_only') + self.assertEqual(search_value, 'te') + + def test_name_only_two_chars_cyrillic(self): + """Query "на" (2 символа) должен вернуть ('name_only', 'на')""" + strategy, search_value = determine_search_strategy('на') + self.assertEqual(strategy, 'name_only') + self.assertEqual(search_value, 'на') + + # ===== edge cases ===== + def test_empty_string(self): + """Query "" должен вернуть ('name_only', '')""" + strategy, search_value = determine_search_strategy('') + self.assertEqual(strategy, 'name_only') + self.assertEqual(search_value, '') + + def test_only_at_symbol(self): + """Query "@" должен вернуть ('email_domain', '')""" + strategy, search_value = determine_search_strategy('@') + self.assertEqual(strategy, 'email_domain') + self.assertEqual(search_value, '') + + def test_multiple_at_symbols(self): + """Query "test@example@com" должен обработать первый @""" + strategy, search_value = determine_search_strategy('test@example@com') + self.assertEqual(strategy, 'email_full') + self.assertEqual(search_value, 'test@example@com') + + def test_spaces_in_query(self): + """Query "Ivan Petrov" должен вернуть ('universal', 'Ivan Petrov')""" + strategy, search_value = determine_search_strategy('Ivan Petrov') + self.assertEqual(strategy, 'universal') + self.assertEqual(search_value, 'Ivan Petrov') + + # ===== real-world examples ===== + def test_real_world_problematic_case(self): + """ + Real-world case: query "team_x3m@" не должен найти "natulj@bk.ru" + Используется email_prefix со istartswith вместо icontains + """ + strategy, search_value = determine_search_strategy('team_x3m@') + self.assertEqual(strategy, 'email_prefix') + # Важно: стратегия email_prefix, не universal или email_full + self.assertNotEqual(strategy, 'universal') + + def test_real_world_domain_search(self): + """Real-world case: query "@bk" должен найти все @bk.ru""" + strategy, search_value = determine_search_strategy('@bk') + self.assertEqual(strategy, 'email_domain') + self.assertEqual(search_value, 'bk') + + def test_real_world_name_search(self): + """Real-world case: query "natul" должен найти "Наталья" и "natulj@bk.ru" """ + strategy, search_value = determine_search_strategy('natul') + self.assertEqual(strategy, 'universal') + self.assertEqual(search_value, 'natul') diff --git a/myproject/customers/views.py b/myproject/customers/views.py index 0efb265..3fc5f11 100644 --- a/myproject/customers/views.py +++ b/myproject/customers/views.py @@ -111,6 +111,52 @@ def customer_delete(request, pk): # === AJAX API ENDPOINTS === +def determine_search_strategy(query): + """ + Определяет стратегию поиска на основе содержимого query. + + На основе наличия и позиции символа @ определяет, по каким полям и как искать. + + Возвращает tuple (strategy, search_value): + - 'name_only': поиск только по имени (для очень коротких запросов) + - 'email_prefix': поиск по началу email (query заканчивается на @) + - 'email_domain': поиск по домену (query начинается с @) + - 'email_full': полный поиск по email (query содержит обе части) + - 'universal': поиск везде (имя + email, для средних запросов) + + Примеры: + - 'team_x3m@' → ('email_prefix', 'team_x3m') + - '@bk' → ('email_domain', 'bk') + - 'test@bk.ru' → ('email_full', 'test@bk.ru') + - 'natul' → ('universal', 'natul') + - 'te' → ('name_only', 'te') + """ + if '@' in query: + local, _, domain = query.partition('@') + + if not local: + # Начинается с @: ищем по домену + return ('email_domain', domain) + + if not domain: + # Заканчивается на @: ищем по префиксу email + return ('email_prefix', local) + + # Есть и локальная часть, и домен: полный поиск + return ('email_full', query) + + else: + # Нет @: определяем по длине + if len(query) < 2: + return ('name_only', query) + + if len(query) >= 3: + return ('universal', query) + + # 2 символа: только имя (слишком мало для поиска по email) + return ('name_only', query) + + @require_http_methods(["GET"]) def api_search_customers(request): """ @@ -154,21 +200,34 @@ def api_search_customers(request): # Ищем по имени, email или телефону # Используем Q-объекты для OR условий - # Определяем, нужно ли искать по email - # Критерии: (@present AND 2+ chars after @) OR (no @ AND 3+ chars total) - search_by_email = False - if '@' in query: - # Если есть @, то нужно 2+ символа после @ - parts = query.split('@') - if len(parts) >= 2 and len(parts[-1]) >= 2: - search_by_email = True - elif len(query) >= 3: - # Если нет @, требуем минимум 3 символа для поиска по email - search_by_email = True + # Определяем стратегию поиска на основе содержимого query + strategy, search_value = determine_search_strategy(query) - q_objects = Q(name__icontains=query) - if search_by_email: - q_objects |= Q(email__icontains=query) + # Строим Q-объект в зависимости от стратегии + if strategy == 'name_only': + # Поиск только по имени (для коротких запросов) + q_objects = Q(name__icontains=search_value) + + elif strategy == 'email_prefix': + # Query вида "team_x3m@" — ищем email, начинающиеся с "team_x3m" + # Это решает проблему: не найдёт "natulj@bk.ru" + q_objects = Q(email__istartswith=search_value) + + elif strategy == 'email_domain': + # Query вида "@bk" — ищем все email с доменом "@bk.*" + q_objects = Q(email__icontains=f'@{search_value}') + + elif strategy == 'email_full': + # Query вида "test@bk.ru" — полный поиск по email + q_objects = Q(email__icontains=search_value) + + elif strategy == 'universal': + # Query вида "natul" (3+ символов) — ищем везде + q_objects = Q(name__icontains=search_value) | Q(email__icontains=search_value) + + else: + # На случай неизвестной стратегии (не должно быть) + q_objects = Q(name__icontains=query) # Для телефона ищем по нормализованному номеру и по цифрам if phone_normalized: