336 lines
12 KiB
Python

from __future__ import annotations
import logging
from django.shortcuts import get_object_or_404
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.response import Response
from communications import comments_helpers
from communications.models import ChatReadState, Message
from documents.models import Document
from guests.constants import GuestVisitEventType, GuestVisitStatus
from guests.models import GuestVisit
from guests.permissions import (
can_decide_guest_visit,
can_manage_guest_account,
is_guest_admin,
)
from guests.services import (
GuestLdapService,
GuestVisitWorkflow,
record_guest_event,
)
from ...employees.serializers import EmployeeBriefSerializer
from ..permissions import GuestVisitPermission
from ..serializers import (
GuestVisitReadSerializer,
GuestVisitWriteSerializer,
)
from .visit_query import GuestVisitQueryMixin
logger = logging.getLogger(__name__)
class GuestVisitViewSet(
GuestVisitQueryMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
# Keep the historical schema free of an inherited mixin description.
__doc__ = " "
queryset = (
GuestVisit.objects.select_related(
"guest",
"inviter",
"decided_by",
"host_department",
)
.prefetch_related("documents", "events__actor")
.order_by("-created_at")
)
permission_classes = [GuestVisitPermission]
def get_serializer_class(self):
if self.action in {"list", "retrieve"}:
return GuestVisitReadSerializer
return GuestVisitWriteSerializer
def _read_response(self, visit, response_status=status.HTTP_200_OK):
visit.refresh_from_db()
serializer = GuestVisitReadSerializer(
visit,
context=self.get_serializer_context(),
)
return Response(serializer.data, status=response_status)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
visit = serializer.save()
return self._read_response(visit, status.HTTP_201_CREATED)
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
if not instance.can_edit_by(request.user):
raise PermissionDenied("Недостаточно прав для изменения визита.")
serializer = self.get_serializer(
instance,
data=request.data,
partial=partial,
)
serializer.is_valid(raise_exception=True)
visit = serializer.save()
return self._read_response(visit)
def destroy(self, request, *args, **kwargs):
visit = self.get_object()
guest = visit.guest
visit.delete()
GuestLdapService().sync_guest(
guest,
enqueue_on_error=True,
raise_on_error=False,
)
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=True, methods=["post"])
def submit(self, request, pk=None):
visit = self.get_object()
try:
GuestVisitWorkflow.submit(visit, actor=request.user)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"])
def approve(self, request, pk=None):
if not can_decide_guest_visit(request.user):
raise PermissionDenied("Недостаточно прав для решения.")
visit = self.get_object()
try:
GuestVisitWorkflow.approve(
visit,
actor=request.user,
comment=request.data.get("comment", ""),
)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"])
def reject(self, request, pk=None):
if not can_decide_guest_visit(request.user):
raise PermissionDenied("Недостаточно прав для решения.")
visit = self.get_object()
try:
GuestVisitWorkflow.reject(
visit,
actor=request.user,
comment=request.data.get("comment", ""),
)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"], url_path="request-info")
def request_info(self, request, pk=None):
if not can_decide_guest_visit(request.user):
raise PermissionDenied("Недостаточно прав для решения.")
visit = self.get_object()
try:
GuestVisitWorkflow.request_info(
visit,
actor=request.user,
comment=request.data.get("comment", ""),
)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"], url_path="provide-info")
def provide_info(self, request, pk=None):
visit = self.get_object()
if visit.status != GuestVisitStatus.NEEDS_INFO:
raise ValidationError(
"Ответить можно только на активный запрос информации."
)
comment = (request.data.get("comment") or "").strip()
if not comment:
raise ValidationError("Комментарий обязателен.")
comments_helpers.create_comment(
visit,
request.user,
comment,
guest_visit_comment_type="info_response",
)
try:
GuestVisitWorkflow.submit(visit, actor=request.user)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"])
def cancel(self, request, pk=None):
visit = self.get_object()
if visit.inviter_id != request.user.id and not is_guest_admin(request.user):
raise PermissionDenied("Недостаточно прав для отмены.")
try:
GuestVisitWorkflow.cancel(
visit,
actor=request.user,
comment=request.data.get("comment", ""),
)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"], url_path="return-to-work")
def return_to_work(self, request, pk=None):
visit = self.get_object()
if visit.inviter_id != request.user.id and not is_guest_admin(request.user):
raise PermissionDenied("Недостаточно прав для возврата в работу.")
try:
GuestVisitWorkflow.return_to_work(
visit,
actor=request.user,
comment=request.data.get("comment", ""),
)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"])
def revoke(self, request, pk=None):
if not can_decide_guest_visit(request.user):
raise PermissionDenied("Недостаточно прав для отзыва.")
visit = self.get_object()
try:
GuestVisitWorkflow.revoke(
visit,
actor=request.user,
comment=request.data.get("comment", ""),
)
except ValueError as exc:
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["post"], url_path="sync-ldap")
def sync_ldap(self, request, pk=None):
if not can_manage_guest_account(request.user):
raise PermissionDenied("Недостаточно прав для LDAP.")
visit = self.get_object()
try:
GuestLdapService().sync_guest_for_visit(
visit,
enqueue_on_error=True,
raise_on_error=True,
)
except Exception as exc:
logger.exception(
"Guest LDAP synchronization failed",
extra={
"event": "guest.ldap_sync.failed",
"guest_visit_id": visit.pk,
"guest_id": visit.guest_id,
},
)
raise ValidationError(str(exc)) from exc
return self._read_response(visit)
@action(detail=True, methods=["get", "post"])
def comments(self, request, pk=None):
visit = self.get_object()
if request.method == "GET":
messages = list(comments_helpers.get_comments(visit))
if messages:
ChatReadState.objects.update_or_create(
chat=messages[-1].chat,
user=request.user,
defaults={
"last_read_message": messages[-1],
"unread_count": 0,
},
)
return Response(
[
{
"id": msg.id,
"author": EmployeeBriefSerializer(msg.author).data,
"text": msg.content,
"metadata": msg.system_metadata,
"created_at": msg.created_at,
}
for msg in messages
]
)
text = (request.data.get("text") or "").strip()
if not text:
raise ValidationError({"text": "Это поле не может быть пустым."})
msg = comments_helpers.create_comment(visit, request.user, text)
return Response(
{
"id": msg.id,
"author": EmployeeBriefSerializer(msg.author).data,
"text": msg.content,
"metadata": msg.system_metadata,
"created_at": msg.created_at,
},
status=status.HTTP_201_CREATED,
)
@action(
detail=True,
methods=["delete"],
url_path="comments/(?P<comment_id>[0-9]+)",
)
def delete_comment(self, request, pk=None, comment_id=None):
visit = self.get_object()
chat = comments_helpers.get_or_create_comments_chat(visit)
message = get_object_or_404(Message, id=comment_id, chat=chat)
if message.author_id != request.user.id and not is_guest_admin(request.user):
raise PermissionDenied("Недостаточно прав для удаления комментария.")
comments_helpers.delete_comment(message, request.user, soft_delete=True)
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=True, methods=["post"], url_path="documents")
def attach_document(self, request, pk=None):
visit = self.get_object()
document_id = request.data.get("document_id")
document = get_object_or_404(Document, pk=document_id)
visit.documents.add(document)
visit.guest.documents.add(document)
record_guest_event(
visit,
GuestVisitEventType.DOCUMENT_ATTACHED,
actor=request.user,
metadata={"document_id": document.id},
)
return self._read_response(visit)
@action(
detail=True,
methods=["delete"],
url_path="documents/(?P<document_id>[0-9]+)",
)
def remove_document(self, request, pk=None, document_id=None):
visit = self.get_object()
document = get_object_or_404(Document, pk=document_id)
visit.documents.remove(document)
record_guest_event(
visit,
GuestVisitEventType.DOCUMENT_REMOVED,
actor=request.user,
metadata={"document_id": document.id},
)
return self._read_response(visit)