from __future__ import annotations import os from django.contrib.contenttypes.models import ContentType from django.db.models import Count, IntegerField, OuterRef, Q, Subquery, Value from django.db.models.functions import Coalesce from django.shortcuts import get_object_or_404 from filer.models import File as FilerFile from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.response import Response from api.v1.documents.serializers import DocumentReadSerializer from communications import comments_helpers from communications.models import Message from documents.models import Document from guests.models import Guest from guests.permissions import ( is_guest_admin, ) from guests.services import ( GuestLdapService, ) from ...employees.serializers import EmployeeBriefSerializer from ..permissions import GuestAdminPermission from ..serializers import ( GuestSearchSerializer, GuestSerializer, ) class GuestViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet, ): queryset = ( Guest.objects.select_related("document_folder") .prefetch_related("documents") .order_by("last_name", "first_name", "id") ) serializer_class = GuestSerializer permission_classes = [GuestAdminPermission] parser_classes = (MultiPartParser, FormParser, JSONParser) def update(self, request, *args, **kwargs): 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) guest = serializer.save() GuestLdapService().sync_guest( guest, enqueue_on_error=True, raise_on_error=False, ) guest.refresh_from_db() return Response(self.get_serializer(guest).data) def get_queryset(self): qs = super().get_queryset() guest_ct = ContentType.objects.get_for_model(Guest) comments_subquery = ( Message.objects.filter( chat__type="comments", chat__context_content_type=guest_ct, chat__context_object_id=OuterRef("pk"), is_deleted=False, ) .values("chat") .annotate(count=Count("id")) .values("count") ) qs = qs.annotate( comments_count=Coalesce( Subquery(comments_subquery, output_field=IntegerField()), Value(0), ), visits_count=Count("visits", distinct=True), ) q = (self.request.query_params.get("q") or "").strip() if q: text_q = ( Q(last_name__icontains=q) | Q(first_name__icontains=q) | Q(patronymic__icontains=q) | Q(email__icontains=q) | Q(phone__icontains=q) | Q(organization__icontains=q) ) if q.isdigit(): text_q |= Q(id=int(q)) qs = qs.filter(text_q) if self.request.query_params.get("is_active") in {"true", "false"}: qs = qs.filter( is_active=self.request.query_params.get("is_active") == "true" ) if self.request.query_params.get("is_blacklisted") in {"true", "false"}: qs = qs.filter( is_blacklisted=self.request.query_params.get("is_blacklisted") == "true" ) ordering = self.request.query_params.get("ordering") allowed_ordering = { "last_name", "-last_name", "created_at", "-created_at", "updated_at", "-updated_at", "ldap_last_synced_at", "-ldap_last_synced_at", } if ordering in allowed_ordering: qs = qs.order_by(ordering, "id") return qs @action(detail=False, methods=["get"]) def search(self, request): serializer = GuestSearchSerializer( self.get_queryset()[:20], many=True, context=self.get_serializer_context(), ) return Response(serializer.data) def _guest_document_response( self, guest: Guest, document: Document, response_status=status.HTTP_200_OK, ): guest.refresh_from_db() return Response( { "guest": self.get_serializer(guest).data, "document": DocumentReadSerializer( document, context=self.get_serializer_context(), ).data, }, status=response_status, ) @action(detail=True, methods=["get", "post"]) def comments(self, request, pk=None): guest = self.get_object() if request.method == "GET": messages = comments_helpers.get_comments(guest) 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(guest, 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): guest = self.get_object() chat = comments_helpers.get_or_create_comments_chat(guest) 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 documents(self, request, pk=None): guest = self.get_object() document_id = request.data.get("document_id") if document_id: document = get_object_or_404(Document, pk=document_id) guest.documents.add(document) return self._guest_document_response(guest, document) upload = request.FILES.get("file") if not upload: raise ValidationError({"file": "Передайте файл или document_id."}) folder = guest.ensure_document_folder(owner=request.user) file_name = getattr(upload, "name", "") or "document" title = (request.data.get("title") or "").strip() if not title: title = os.path.splitext(os.path.basename(file_name))[0] or file_name filer_file = FilerFile.objects.create( file=upload, folder=folder, original_filename=file_name, name=file_name, owner=request.user, is_public=True, ) document = Document.objects.create( title=title, description=(request.data.get("description") or "").strip(), file=filer_file, folder=folder, uploaded_by=request.user, modified_by=request.user, sent_to_all=False, ) guest.documents.add(document) return self._guest_document_response( guest, document, response_status=status.HTTP_201_CREATED, ) @action( detail=True, methods=["delete"], url_path="documents/(?P[0-9]+)", ) def remove_document(self, request, pk=None, document_id=None): if not is_guest_admin(request.user): raise PermissionDenied("Недостаточно прав для изменения гостя.") guest = self.get_object() document = get_object_or_404(Document, pk=document_id) guest.documents.remove(document) return self._guest_document_response(guest, document) @action(detail=True, methods=["post"]) def blacklist(self, request, pk=None): guest = self.get_object() GuestLdapService().blacklist_guest(guest) guest.refresh_from_db() return Response(self.get_serializer(guest).data) @action(detail=True, methods=["post"], url_path="unblacklist") def unblacklist(self, request, pk=None): guest = self.get_object() GuestLdapService().unblacklist_guest(guest) guest.refresh_from_db() return Response(self.get_serializer(guest).data) @action(detail=True, methods=["post"], url_path="sync-ldap") def sync_ldap(self, request, pk=None): guest = self.get_object() try: GuestLdapService().sync_guest( guest, enqueue_on_error=True, raise_on_error=True, ) except Exception as exc: guest.refresh_from_db() raise ValidationError(str(exc)) from exc guest.refresh_from_db() return Response(self.get_serializer(guest).data)