# backend/api/v1/search/views.py """ API views для глобального поиска через django-watson. """ from __future__ import annotations from typing import Any, Dict from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request as DRFRequest from rest_framework.response import Response from django.utils.html import escape from watson.search import search as watson_search from .serializers import SearchResponseSerializer def _get_model_name(obj: Any) -> str: """Определяет тип модели для результата поиска.""" model = type(obj) model_name = model.__name__.lower() mapping = { "post": "post", "employee": "employee", "department": "department", "request": "request", "chat": "chat", "message": "message", "calendarevent": "event", "event": "schedule_event", # django-scheduler Event "procurementrequest": "procurement_request", "equipment": "equipment", "document": "document", "notification": "notification", } return mapping.get(model_name, model_name) def _format_employee(obj: Any) -> Dict[str, Any]: """Форматирует сотрудника для API.""" return { "model_name": "employee", "object_id": obj.pk, "title": f"{obj.last_name} {obj.first_name} { obj.patronymic or '' }".strip(), "description": getattr(obj.position, "name", "") if hasattr(obj, "position") else "", "meta": { "email": obj.email, "phone": str(obj.phone_number) if obj.phone_number else "", }, } def _format_department(obj: Any) -> Dict[str, Any]: """Форматирует отдел для API.""" return { "model_name": "department", "object_id": obj.pk, "title": obj.name, "description": obj.description or "", "meta": { "head": str(obj.head) if hasattr(obj, "head") and obj.head else None }, } def _format_post(obj: Any) -> Dict[str, Any]: """Форматирует пост для API.""" return { "model_name": "post", "object_id": obj.pk, "title": obj.title, "description": obj.body[:200] + "..." if len(obj.body) > 200 else obj.body, "meta": { "author": str(obj.author) if hasattr(obj, "author") else None, "created_at": obj.created_at.isoformat() if hasattr(obj, "created_at") else None, }, } def _format_request(obj: Any) -> Dict[str, Any]: """Форматирует заявление для API.""" return { "model_name": "request", "object_id": obj.pk, "title": obj.title or f"{obj.get_type_display()} - {obj.employee}", "description": obj.comment[:200] + "..." if obj.comment and len(obj.comment) > 200 else (obj.comment or ""), "meta": { "status": obj.status, "type": obj.type, "employee": str(obj.employee) if hasattr(obj, "employee") else None, }, } def _format_chat(obj: Any) -> Dict[str, Any]: """Форматирует чат для API.""" return { "model_name": "chat", "object_id": obj.pk, "title": obj.name or f"Чат #{obj.pk}", "description": obj.description or "", "meta": { "type": obj.type if hasattr(obj, "type") else None, }, } def _format_message(obj: Any) -> Dict[str, Any]: """Форматирует сообщение для API.""" return { "model_name": "message", "object_id": obj.pk, "title": f"Сообщение от {obj.author}" if hasattr(obj, "author") else f"Сообщение #{obj.pk}", "description": obj.content[:200] + "..." if len(obj.content) > 200 else obj.content, "meta": { "author": str(obj.author) if hasattr(obj, "author") else None, "chat": str(obj.chat) if hasattr(obj, "chat") else None, "chat_id": obj.chat.pk if hasattr(obj, "chat") else None, }, } def _format_event(obj: Any) -> Dict[str, Any]: """Форматирует календарное событие (CalendarEvent) для API.""" return { "model_name": "event", "object_id": obj.pk, "title": obj.title, "description": obj.description or "", "meta": { "start_date": obj.start_date.isoformat() if hasattr(obj, "start_date") else None, }, } def _format_schedule_event(obj: Any) -> Dict[str, Any]: """Форматирует событие расписания (django-scheduler Event) для API.""" return { "model_name": "schedule_event", "object_id": obj.pk, "title": obj.title, "description": obj.description or "", "meta": { "start": obj.start.isoformat() if hasattr(obj, "start") else None, "end": obj.end.isoformat() if hasattr(obj, "end") and obj.end else None, }, } def _format_procurement_request(obj: Any) -> Dict[str, Any]: """Форматирует заявку на закупку для API.""" return { "model_name": "procurement_request", "object_id": obj.pk, "title": obj.title, "description": obj.description[:200] + "..." if len(obj.description) > 200 else obj.description, "meta": { "status": obj.status if hasattr(obj, "status") else None, "requestor": str(obj.requestor) if hasattr(obj, "requestor") else None, "department": str(obj.department) if hasattr(obj, "department") else None, }, } def _format_equipment(obj: Any) -> Dict[str, Any]: """Форматирует оборудование для API.""" return { "model_name": "equipment", "object_id": obj.pk, "title": obj.name, "description": ( f"Инв. №: {obj.inventory_number}, Сер. №: {obj.serial_number}" if obj.serial_number else f"Инв. №: {obj.inventory_number}" ), "meta": { "inventory_number": obj.inventory_number, "status": obj.status if hasattr(obj, "status") else None, "location": obj.location if hasattr(obj, "location") else None, }, } def _format_document(obj: Any) -> Dict[str, Any]: """Форматирует документ для API.""" return { "model_name": "document", "object_id": obj.pk, "title": obj.title, "description": ( obj.description[:200] + "..." if obj.description and len(obj.description) > 200 else (obj.description or "") ), "meta": { "uploaded_by": str(obj.uploaded_by) if hasattr(obj, "uploaded_by") and obj.uploaded_by else None, "uploaded_at": obj.uploaded_at.isoformat() if hasattr(obj, "uploaded_at") else None, }, } def _get_notification_search_title(obj: Any) -> str: """Строит заголовок уведомления для поиска без legacy-зависимостей.""" data = getattr(obj, "data", None) or {} if isinstance(data, dict) and data.get("title"): return str(data["title"]) verb = getattr(obj, "verb", "") or "notification" return str(verb).replace("_", " ").title() def _get_notification_search_message(obj: Any) -> str: """Возвращает текст уведомления для результатов поиска.""" return getattr(obj, "description", "") or "" def _format_notification(obj: Any) -> Dict[str, Any]: """Форматирует уведомление для API.""" message = _get_notification_search_message(obj) return { "model_name": "notification", "object_id": obj.pk, "title": _get_notification_search_title(obj), "description": message[:150] + "..." if len(message) > 150 else message, "meta": { "is_read": not getattr(obj, "unread", True), "created_at": obj.timestamp.isoformat() if hasattr(obj, "timestamp") and obj.timestamp else None, }, } def _format_result(obj: Any, model_name: str) -> Dict[str, Any]: """Форматирует результат поиска в зависимости от типа модели.""" formatters = { "employee": _format_employee, "department": _format_department, "post": _format_post, "request": _format_request, "chat": _format_chat, "message": _format_message, "event": _format_event, "schedule_event": _format_schedule_event, "procurement_request": _format_procurement_request, "equipment": _format_equipment, "document": _format_document, "notification": _format_notification, } formatter = formatters.get(model_name) if formatter: return formatter(obj) # Фоллбэк для неизвестных типов return { "model_name": model_name, "object_id": obj.pk, "title": str(obj), "description": "", "meta": {}, } def _is_hr(user: Any) -> bool: """Проверяет права на просмотр всех заявлений.""" return user.has_perm("requests_app.can_view_all_requests") or user.has_perm( "requests_app.can_process_requests" ) @extend_schema( tags=["Search"], summary="Глобальный поиск по системе", parameters=[ OpenApiParameter("q", str, OpenApiParameter.QUERY, required=True), OpenApiParameter("limit", int, OpenApiParameter.QUERY), ], responses=SearchResponseSerializer, ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def search_api_view(request: DRFRequest) -> Response: """ API для глобального поиска через django-watson. Query Parameters: - q (str): Поисковый запрос - limit (int): Максимальное количество результатов на тип (default: 10) Returns: { "query": "поисковый запрос", "results": [ { "model_name": ( "post|employee|department|request|chat|message|event|" "procurement_request|equipment|document|notification" ), "object_id": 123, "title": "Заголовок", "description": "Описание", "meta": {...} }, ... ], "counts": { "post": 5, "employee": 3, ... }, "total": 15 } """ raw_q = request.query_params.get("q", "") or "" query = escape(raw_q.strip()) try: limit = int(request.query_params.get("limit", 10)) except (ValueError, TypeError): limit = 10 if not query: return Response( {"query": "", "results": [], "counts": {}, "total": 0}, status=status.HTTP_200_OK, ) # Выполняем поиск через watson search_results = watson_search(query) # Группируем и форматируем результаты results = [] model_counts: Dict[str, int] = {} for search_result in search_results: obj = search_result.object # Пропускаем удаленные объекты if obj is None: continue model_name = _get_model_name(obj) # Фильтрация по правам доступа if model_name == "request": is_hr = _is_hr(request.user) if ( not is_hr and hasattr(obj, "employee") and obj.employee != request.user ): # type: ignore[attr-defined] continue # Пропускаем чужие заявления # Фильтрация уведомлений - показываем только свои if model_name == "notification": if ( hasattr(obj, "recipient") and obj.recipient != request.user ): # type: ignore[attr-defined] continue # Пропускаем чужие уведомления # Подсчитываем model_counts[model_name] = model_counts.get(model_name, 0) + 1 # Ограничиваем количество на тип if model_counts[model_name] <= limit: formatted = _format_result(obj, model_name) results.append(formatted) # Готовим ответ response_data = { "query": query, "results": results, "counts": model_counts, "total": sum(model_counts.values()), } # Валидируем через сериализатор (опционально, для проверки структуры) serializer = SearchResponseSerializer(data=response_data) if serializer.is_valid(): return Response(serializer.validated_data, status=status.HTTP_200_OK) # Если валидация не прошла, все равно возвращаем результат (для отладки) return Response(response_data, status=status.HTTP_200_OK)