- 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
159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
# 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)
|