Полный пересмотр логики поиска по email с стратегиями
Проблема: Поиск "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 <noreply@anthropic.com>
This commit is contained in:
160
myproject/customers/tests.py
Normal file
160
myproject/customers/tests.py
Normal file
@@ -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')
|
||||
@@ -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-объект в зависимости от стратегии
|
||||
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 search_by_email:
|
||||
q_objects |= Q(email__icontains=query)
|
||||
|
||||
# Для телефона ищем по нормализованному номеру и по цифрам
|
||||
if phone_normalized:
|
||||
|
||||
Reference in New Issue
Block a user