240 lines
8.7 KiB
Python
Raw Permalink 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.

"""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)