Добавлена умная фильтрация для поиска по номерам телефонов
Проблема: Поиск "x3m" неправильно находит клиента Наталью потому что её номер
содержит цифру "3". Это происходило, потому что система искала по любым цифрам
в query, даже если это явно не номер телефона.
Решение: Добавлена функция is_query_phone_only() которая проверяет, содержит ли
query ТОЛЬКО телефонные символы (цифры, +, -, (), пробелы, точка).
Поиск по номеру телефона происходит ТОЛЬКО если:
1. Query состоит ТОЛЬКО из телефонных символов (никаких букв)
2. И количество цифр >= 3
Примеры:
- "x3m" → НЕ ищет по цифре "3" (содержит букву)
- "29" → НЕ ищет по цифрам (только 2 цифры, нужно минимум 3)
- "295" → ИЩЕТ по цифрам "295" (только цифры, 3+ символов)
- "+375291234567" → ИЩЕТ по номеру (только телефонные символы)
- "team_x3m" → НЕ ищет по цифрам (содержит буквы и _)
Изменения:
1. Добавлена функция is_query_phone_only() в views.py
2. Обновлена api_search_customers() для использования новой функции
3. Обновлена customer_list() для использования новой функции
4. Добавлены 19 unit-тестов для is_query_phone_only()
Результаты тестирования:
✓ 42 теста всего (23 для determine_search_strategy + 19 для is_query_phone_only)
✓ Все тесты проходят успешно
Критические тест-кейсы:
✓ is_query_phone_only('x3m') == False (решает исходную проблему)
✓ is_query_phone_only('295') == True
✓ is_query_phone_only('+375291234567') == True
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from .views import determine_search_strategy
|
from .views import determine_search_strategy, is_query_phone_only
|
||||||
|
|
||||||
|
|
||||||
class DetermineSearchStrategyTestCase(TestCase):
|
class DetermineSearchStrategyTestCase(TestCase):
|
||||||
@@ -158,3 +158,95 @@ class DetermineSearchStrategyTestCase(TestCase):
|
|||||||
strategy, search_value = determine_search_strategy('natul')
|
strategy, search_value = determine_search_strategy('natul')
|
||||||
self.assertEqual(strategy, 'universal')
|
self.assertEqual(strategy, 'universal')
|
||||||
self.assertEqual(search_value, 'natul')
|
self.assertEqual(search_value, 'natul')
|
||||||
|
|
||||||
|
|
||||||
|
class IsQueryPhoneOnlyTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Тесты для функции is_query_phone_only().
|
||||||
|
|
||||||
|
Проверяют, что функция правильно определяет, содержит ли query
|
||||||
|
только символы номера телефона (цифры, +, -, (), пробелы).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ===== Должны вернуть True (только телефонные символы) =====
|
||||||
|
def test_phone_only_digits(self):
|
||||||
|
"""Query '295' должен вернуть True (только цифры)"""
|
||||||
|
self.assertTrue(is_query_phone_only('295'))
|
||||||
|
|
||||||
|
def test_phone_only_single_digit(self):
|
||||||
|
"""Query '5' должен вернуть True (одна цифра)"""
|
||||||
|
self.assertTrue(is_query_phone_only('5'))
|
||||||
|
|
||||||
|
def test_phone_with_plus(self):
|
||||||
|
"""Query '+375291234567' должен вернуть True"""
|
||||||
|
self.assertTrue(is_query_phone_only('+375291234567'))
|
||||||
|
|
||||||
|
def test_phone_with_dashes(self):
|
||||||
|
"""Query '029-123-45' должен вернуть True"""
|
||||||
|
self.assertTrue(is_query_phone_only('029-123-45'))
|
||||||
|
|
||||||
|
def test_phone_with_parentheses(self):
|
||||||
|
"""Query '(029) 123-45' должен вернуть True"""
|
||||||
|
self.assertTrue(is_query_phone_only('(029) 123-45'))
|
||||||
|
|
||||||
|
def test_phone_with_spaces(self):
|
||||||
|
"""Query '029 123 45' должен вернуть True"""
|
||||||
|
self.assertTrue(is_query_phone_only('029 123 45'))
|
||||||
|
|
||||||
|
def test_phone_complex_format(self):
|
||||||
|
"""Query '+375 (29) 123-45-67' должен вернуть True"""
|
||||||
|
self.assertTrue(is_query_phone_only('+375 (29) 123-45-67'))
|
||||||
|
|
||||||
|
def test_phone_with_dot(self):
|
||||||
|
"""Query '029.123.45' должен вернуть True"""
|
||||||
|
self.assertTrue(is_query_phone_only('029.123.45'))
|
||||||
|
|
||||||
|
# ===== Должны вернуть False (содержат буквы или другие символы) =====
|
||||||
|
def test_query_with_letters_only(self):
|
||||||
|
"""Query 'abc' должен вернуть False (содержит буквы)"""
|
||||||
|
self.assertFalse(is_query_phone_only('abc'))
|
||||||
|
|
||||||
|
def test_query_with_mixed_letters_digits(self):
|
||||||
|
"""Query 'x3m' должен вернуть False (содержит буквы)"""
|
||||||
|
self.assertFalse(is_query_phone_only('x3m'))
|
||||||
|
|
||||||
|
def test_query_name_with_digits(self):
|
||||||
|
"""Query 'team_x3m' должен вернуть False (содержит буквы и _)"""
|
||||||
|
self.assertFalse(is_query_phone_only('team_x3m'))
|
||||||
|
|
||||||
|
def test_query_name_cyrillic(self):
|
||||||
|
"""Query 'Наталья' должен вернуть False (содержит кириллицу)"""
|
||||||
|
self.assertFalse(is_query_phone_only('Наталья'))
|
||||||
|
|
||||||
|
def test_query_with_underscore(self):
|
||||||
|
"""Query '123_456' должен вернуть False (содержит _)"""
|
||||||
|
self.assertFalse(is_query_phone_only('123_456'))
|
||||||
|
|
||||||
|
def test_query_with_hash(self):
|
||||||
|
"""Query '123#456' должен вернуть False (содержит #)"""
|
||||||
|
self.assertFalse(is_query_phone_only('123#456'))
|
||||||
|
|
||||||
|
def test_empty_string(self):
|
||||||
|
"""Query '' должен вернуть False (пустая строка)"""
|
||||||
|
self.assertFalse(is_query_phone_only(''))
|
||||||
|
|
||||||
|
def test_only_spaces(self):
|
||||||
|
"""Query ' ' должен вернуть True (только пробелы разрешены)"""
|
||||||
|
self.assertTrue(is_query_phone_only(' '))
|
||||||
|
|
||||||
|
# ===== Real-world cases =====
|
||||||
|
def test_real_world_case_x3m_should_not_be_phone(self):
|
||||||
|
"""
|
||||||
|
Real-world case: "x3m" содержит букву, поэтому НЕ похож на телефон.
|
||||||
|
Это критично для решения проблемы с поиском Натальи.
|
||||||
|
"""
|
||||||
|
self.assertFalse(is_query_phone_only('x3m'))
|
||||||
|
# Значит, при поиске "x3m" НЕ будет поиска по цифре "3" в телефонах
|
||||||
|
|
||||||
|
def test_real_world_case_295_should_be_phone(self):
|
||||||
|
"""Real-world case: '295' только цифры, похож на телефон"""
|
||||||
|
self.assertTrue(is_query_phone_only('295'))
|
||||||
|
|
||||||
|
def test_real_world_full_phone_number(self):
|
||||||
|
"""Real-world case: полный номер в стандартном формате"""
|
||||||
|
self.assertTrue(is_query_phone_only('+375 (29) 598-62-62'))
|
||||||
|
|||||||
@@ -41,10 +41,26 @@ def customer_list(request):
|
|||||||
# Строим Q-объект для поиска (единая функция)
|
# Строим Q-объект для поиска (единая функция)
|
||||||
q_objects = build_customer_search_query(query, strategy, search_value)
|
q_objects = build_customer_search_query(query, strategy, search_value)
|
||||||
|
|
||||||
# Добавляем поиск по телефону
|
# Добавляем поиск по телефону (умная логика)
|
||||||
if phone_normalized:
|
if phone_normalized:
|
||||||
q_objects |= Q(phone__icontains=phone_normalized)
|
q_objects |= Q(phone__icontains=phone_normalized)
|
||||||
|
|
||||||
|
# Проверяем, похож ли query на номер телефона (только цифры и минимум 3 цифры)
|
||||||
|
query_digits = ''.join(c for c in query if c.isdigit())
|
||||||
|
should_search_by_phone_digits = is_query_phone_only(query) and len(query_digits) >= 3
|
||||||
|
|
||||||
|
if should_search_by_phone_digits:
|
||||||
|
# Ищем клиентов, чьи телефоны содержат введенные цифры
|
||||||
|
customers_by_phone = Customer.objects.filter(phone__isnull=False)
|
||||||
|
matching_by_digits = []
|
||||||
|
for customer in customers_by_phone:
|
||||||
|
customer_digits = ''.join(c for c in str(customer.phone) if c.isdigit())
|
||||||
|
if query_digits in customer_digits:
|
||||||
|
matching_by_digits.append(customer.pk)
|
||||||
|
|
||||||
|
if matching_by_digits:
|
||||||
|
q_objects |= Q(pk__in=matching_by_digits)
|
||||||
|
|
||||||
customers = customers.filter(q_objects)
|
customers = customers.filter(q_objects)
|
||||||
|
|
||||||
customers = customers.order_by('-created_at')
|
customers = customers.order_by('-created_at')
|
||||||
@@ -167,6 +183,32 @@ def determine_search_strategy(query):
|
|||||||
return ('name_only', query)
|
return ('name_only', query)
|
||||||
|
|
||||||
|
|
||||||
|
def is_query_phone_only(query):
|
||||||
|
"""
|
||||||
|
Проверяет, содержит ли query только символы телефонного номера.
|
||||||
|
|
||||||
|
Возвращает True, если query состоит ТОЛЬКО из:
|
||||||
|
- цифр: 0-9
|
||||||
|
- телефонных символов: +, -, (, ), пробелов
|
||||||
|
|
||||||
|
Возвращает False, если есть буквы или другие символы (означает, что это поиск по имени/email).
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- '295' → True (только цифры)
|
||||||
|
- '+375291234567' → True (цифры и телефонные символы)
|
||||||
|
- '(029) 123-45' → True (цифры и телефонные символы)
|
||||||
|
- 'x3m' → False (содержит буквы)
|
||||||
|
- 'team_x3m' → False (содержит буквы)
|
||||||
|
- 'Иван' → False (содержит буквы)
|
||||||
|
"""
|
||||||
|
if not query:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем, что query содержит ТОЛЬКО цифры и телефонные символы
|
||||||
|
phone_chars = set('0123456789+- ().')
|
||||||
|
return all(c in phone_chars for c in query)
|
||||||
|
|
||||||
|
|
||||||
def build_customer_search_query(query, strategy, search_value):
|
def build_customer_search_query(query, strategy, search_value):
|
||||||
"""
|
"""
|
||||||
Строит Q-объект для поиска клиентов на основе стратегии.
|
Строит Q-объект для поиска клиентов на основе стратегии.
|
||||||
@@ -254,11 +296,17 @@ def api_search_customers(request):
|
|||||||
# Строим Q-объект для поиска (единая функция, используется везде)
|
# Строим Q-объект для поиска (единая функция, используется везде)
|
||||||
q_objects = build_customer_search_query(query, strategy, search_value)
|
q_objects = build_customer_search_query(query, strategy, search_value)
|
||||||
|
|
||||||
# Для телефона ищем по нормализованному номеру и по цифрам
|
# Для поиска по номерам телефонов: применяем умную логику
|
||||||
|
# Ищем по телефону ТОЛЬКО если query состоит из ТОЛЬКО цифр и телефонных символов
|
||||||
|
# (никаких букв — потому что если есть буквы, это явно поиск по имени/email, не по телефону)
|
||||||
|
|
||||||
if phone_normalized:
|
if phone_normalized:
|
||||||
q_objects |= Q(phone__icontains=phone_normalized)
|
q_objects |= Q(phone__icontains=phone_normalized)
|
||||||
|
|
||||||
if query_digits:
|
# Проверяем, похож ли query на номер телефона (только цифры/+/-/() и минимум 3 цифры)
|
||||||
|
should_search_by_phone_digits = is_query_phone_only(query) and len(query_digits) >= 3
|
||||||
|
|
||||||
|
if should_search_by_phone_digits:
|
||||||
# Ищем клиентов, чьи телефоны содержат введенные цифры
|
# Ищем клиентов, чьи телефоны содержат введенные цифры
|
||||||
customers_by_phone = Customer.objects.filter(phone__isnull=False)
|
customers_by_phone = Customer.objects.filter(phone__isnull=False)
|
||||||
matching_by_digits = []
|
matching_by_digits = []
|
||||||
|
|||||||
Reference in New Issue
Block a user