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