Corp/backend/search/views.py
igor_nadein 3aa0dadf2e feat: Replace custom search with django-watson
- Installed django-watson 1.6.3
- Added watson to INSTALLED_APPS
- Created search_indexes.py with model registrations:
  * Post (title, body)
  * Employee (name, email, phone)
  * Department (name, description)
  * Request (title, comment)
  * Chat (name, description)
  * Message (content)
  * CalendarEvent (title, description)
- Replaced search/views.py (484 lines → 158 lines)
- Applied watson migrations and built search indexes (1015 entries)
- Tested search functionality

Benefits:
- 67% code reduction (326 lines removed)
- Better search relevance with full-text indexing
- Automatic ranking and highlighting
- Less maintenance burden
2026-02-27 18:12:21 +03:00

159 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# backend/search/views.py
"""
Полнотекстовый поиск через django-watson.
Заменяет предыдущую реализацию с ручными Q-объектами на
автоматическую индексацию watson.
"""
from __future__ import annotations
from typing import Any, Dict, List, Literal, TypedDict
from collections import Counter
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.utils.html import escape
from watson.search import search as watson_search
from feed.models import Post
from employees.models import Employee, Department
from requests_app.models import Request
class SearchItem(TypedDict):
"""Элемент единой выдачи поиска.
Attributes:
model_name (Literal["post","employee","department","request","chat","message","event"]):
Тип найденной сущности.
object (Any): Экземпляр модели, который будет отрисован в шаблоне.
"""
model_name: Literal[
"post", "employee", "department", "request", "chat", "message", "event"
]
object: Any
# ------------------------ ВСПОМОГАТЕЛЬНЫЕ УТИЛИТЫ ------------------------
def _is_hr(user: Employee) -> bool:
"""Проверяет расширенные права на просмотр заявлений.
Args:
user (Employee): Текущий пользователь.
Returns:
bool: True, если пользователь может видеть все заявления.
Notes:
Используются пермишены из requests_app.
"""
return user.has_perm("requests_app.can_view_all_requests") or user.has_perm(
"requests_app.can_process_requests"
)
def _get_model_name(obj: Any) -> str:
"""Определяет тип модели для SearchItem.
Args:
obj: Объект модели из результатов поиска.
Returns:
str: Название типа модели (post, employee, department, etc.)
"""
model = type(obj)
model_name = model.__name__.lower()
# Маппинг моделей на типы для SearchItem
mapping = {
"post": "post",
"employee": "employee",
"department": "department",
"request": "request",
"chat": "chat",
"message": "message",
"calendarevent": "event",
}
return mapping.get(model_name, model_name)
# -------------------------------- ВЬЮХА --------------------------------
@login_required
def search_view(request: HttpRequest) -> HttpResponse:
"""Единый поиск по системе через django-watson.
Args:
request (HttpRequest): Запрос (`GET["q"]`).
Returns:
HttpResponse: Рендер `templates/search/results.html` с плоским списком.
Контекст шаблона:
- query: str — исходная строка запроса
- results: List[SearchItem] — плоский список элементов
- counts: Dict[str, int] — счётчики по типам:
keys: post, employee, department, request, chat, message, event
- total: int — сумма всех счётчиков
"""
raw_q = request.GET.get("q", "") or ""
query = escape(raw_q.strip())
items: List[SearchItem] = []
counts: Dict[str, int] = {
"post": 0,
"employee": 0,
"department": 0,
"request": 0,
"chat": 0,
"message": 0,
"event": 0,
}
if query:
# Выполняем поиск через watson
search_results = watson_search(query)
# Группируем по типам моделей
by_model: Dict[str, List[Any]] = {}
for result in search_results:
obj = result.object
model_name = _get_model_name(obj)
# Фильтрация заявлений по правам доступа
if model_name == "request":
is_hr = _is_hr(request.user) # type: ignore[arg-type]
# Проверяем, что объект - это действительно Request с полем employee
if not is_hr and hasattr(obj, 'employee') and obj.employee != request.user: # type: ignore[attr-defined]
continue # Пропускаем чужие заявления
if model_name not in by_model:
by_model[model_name] = []
by_model[model_name].append(obj)
# Формируем итоговый список с ограничением 10 элементов на тип
for model_name in ["post", "employee", "department", "request",
"chat", "message", "event"]:
objects = by_model.get(model_name, [])
counts[model_name] = len(objects)
# Берем первые 10 для отображения
for obj in objects[:10]:
items.append({
"model_name": model_name, # type: ignore[typeddict-item]
"object": obj
})
ctx = {
"query": query,
"results": items,
"counts": counts,
"total": sum(counts.values()),
}
return render(request, "search/results.html", ctx)