"""Core document CRUD viewset.""" from __future__ import annotations from django.contrib.auth import get_user_model from django.db import transaction from django.db.models import Count, Exists, OuterRef, QuerySet from rest_framework import status from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from documents.models import ( Document, DocumentAcknowledgement, DocumentDisplayState, ) from ..permissions import ( DocumentReadOrModelPerms, filter_documents_for_audience, filter_documents_for_user, user_has_any_document_model_perm, ) from ..presentation import attach_document_metrics, attach_linked_task_payloads from ..serializers import DocumentReadSerializer, DocumentWriteSerializer from .mixins import ( DocumentAcknowledgementMixin, DocumentArchiveMixin, DocumentCommentsMixin, DocumentHistoryMixin, DocumentMediaMixin, DocumentRelationsMixin, ) User = get_user_model() def _parse_integer(value): try: return int(value) except (TypeError, ValueError): return None def _parse_boolean(value): normalized = str(value).strip().lower() if normalized in {"1", "true", "yes", "on"}: return True if normalized in {"0", "false", "no", "off"}: return False return None class DocumentViewSet( DocumentArchiveMixin, DocumentAcknowledgementMixin, DocumentCommentsMixin, DocumentHistoryMixin, DocumentMediaMixin, DocumentRelationsMixin, ModelViewSet, ): """Полный CRUD по документам + экшен `acknowledge`. Создание/обновление поддерживает multipart (для файла) и назначение получателей. """ queryset: QuerySet[Document] = ( Document.objects.all() .select_related("uploaded_by") .prefetch_related( "recipients", "departments", "acknowledgement_recipients", "acknowledgement_departments", ) ) permission_classes = [DocumentReadOrModelPerms] parser_classes = (MultiPartParser, FormParser, JSONParser) def get_serializer_class(self): """Возвращает сериализатор в зависимости от действия. Returns: type: Класс сериализатора для чтения/записи. """ if self.action in ("create", "update", "partial_update"): return DocumentWriteSerializer return DocumentReadSerializer @staticmethod def _apply_query_filters(queryset, query_params): folder_id = query_params.get("folder_id") if folder_id is not None: parsed_folder_id = _parse_integer(folder_id) if parsed_folder_id is not None: queryset = queryset.filter(folder_id=parsed_folder_id) regulation = query_params.get("is_regulation") if regulation is None: regulation = query_params.get("regulation") if regulation is not None: parsed_regulation = _parse_boolean(regulation) if parsed_regulation is not None: queryset = queryset.filter(is_regulation=parsed_regulation) return queryset @staticmethod def _annotate_user_state(queryset, user): acknowledged = DocumentAcknowledgement.objects.filter( document=OuterRef("pk"), user=user, ) maximally_hidden = DocumentDisplayState.objects.filter( document=OuterRef("pk"), user=user, is_maximally_hidden=True, ) return queryset.annotate( _is_acknowledged=Exists(acknowledged), _is_maximally_hidden=Exists(maximally_hidden), ) def get_queryset(self) -> QuerySet[Document]: """Базовый queryset + аннотация флага ознакомления текущего пользователя. Поддерживает параметр ?scope=mine для фильтрации документов, доступных текущему пользователю. Поддерживает параметр ?folder_id для фильтрации по папке. Поддерживает параметр ?is_regulation=true для раздела регламентов. Returns: QuerySet[Document]: оптимизированный для списка без N+1. """ queryset = super().get_queryset() request = getattr(self, "request", None) user = getattr(request, "user", None) if request: queryset = self._apply_query_filters(queryset, request.query_params) if user and user.is_authenticated: scope = request.query_params.get("scope", "").lower() if scope == "mine": queryset = filter_documents_for_audience(queryset, user) elif not user.is_staff and not user_has_any_document_model_perm(user): queryset = filter_documents_for_user(queryset, user) queryset = self._annotate_user_state(queryset, user) return queryset.order_by("-uploaded_at") def _attach_linked_task_payloads(self, documents: list[Document]) -> None: attach_linked_task_payloads( documents, user=getattr(self.request, "user", None), ) def _attach_document_metrics(self, documents: list[Document]) -> None: attach_document_metrics(documents) def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: documents = list(page) self._attach_linked_task_payloads(documents) self._attach_document_metrics(documents) serializer = self.get_serializer(documents, many=True) return self.get_paginated_response(serializer.data) documents = list(queryset) self._attach_linked_task_payloads(documents) self._attach_document_metrics(documents) serializer = self.get_serializer(documents, many=True) return Response(serializer.data) def retrieve(self, request, *args, **kwargs): document = self.get_object() self._attach_linked_task_payloads([document]) self._attach_document_metrics([document]) serializer = self.get_serializer(document) return Response(serializer.data) @transaction.atomic def create(self, request, *args, **kwargs): """Создаёт документ и возвращает read-схему + recipient_count. recipient_count: - 0, если sent_to_all=True; - число активных пользователей из переданных recipient_ids (неактивные/несуществующие игнорируются). """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # Сохраним нормализованные ID до save(), т.к. serializer.save() их # вытаскивает. input_recipient_ids = list(serializer.validated_data.get("recipient_ids", [])) doc = serializer.save() self._attach_document_metrics([doc]) read = DocumentReadSerializer(doc, context={"request": request}) data = dict(read.data) if doc.sent_to_all: data["recipient_count"] = 0 else: active_count = User.objects.filter( is_active=True, id__in=input_recipient_ids ).aggregate(n=Count("id", distinct=True))["n"] data["recipient_count"] = active_count return Response(data, status=status.HTTP_201_CREATED) def update(self, request, *args, **kwargs): """Полная замена документа. Returns: Response: Тело ответа в формате read-сериализатора (с file_url). """ partial = kwargs.pop("partial", False) instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) doc = serializer.save() self._attach_document_metrics([doc]) read = DocumentReadSerializer(doc, context={"request": request}) return Response(read.data, status=status.HTTP_200_OK) def partial_update(self, request, *args, **kwargs): """Частичное обновление (PATCH) — аналогично update, но partial=True.""" kwargs["partial"] = True return self.update(request, *args, **kwargs)