Corp/backend/api/v1/guests/serializers.py
2026-07-06 21:57:37 +03:00

541 lines
18 KiB
Python

from __future__ import annotations
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from rest_framework import serializers
from communications import comments_helpers
from api.v1.serializers import Base64ImageField
from api.v1.documents.serializers import DocumentReadSerializer, FolderBriefSerializer
from api.v1.employees.serializers import EmployeeBriefSerializer
from documents.models import Document
from guests.constants import GuestVisitStatus
from guests.models import Guest, GuestVisit, GuestVisitEvent
from guests.permissions import (
can_decide_guest_visit,
can_manage_guest_account,
is_guest_admin,
)
from guests.services import (
GUEST_VISIT_PERIOD_BLOCKING_STATUSES,
GuestVisitWorkflow,
has_unread_info_response_for_user,
normalize_all_day_range,
validate_guest_visit_period_available,
validate_guest_visit_period_not_past,
)
User = get_user_model()
def _linked_task_payloads(obj, kind: str, user) -> list[dict]:
if not user or not getattr(user, "is_authenticated", False):
return []
try:
from tasks.access import task_board_access_q
from tasks.models import (
TaskBoard,
TaskLinkedObject,
TaskLinkedObjectKind,
)
except Exception:
return []
content_type = ContentType.objects.get_for_model(obj.__class__)
accessible_boards = TaskBoard.objects.filter(
is_archived=False,
).filter(task_board_access_q(user))
links = (
TaskLinkedObject.objects.filter(
kind=kind,
content_type=content_type,
object_id=obj.id,
task__board__in=accessible_boards,
)
.select_related("task", "task__board", "task__column")
.order_by("task__title", "task_id")
)
return [
{
"link_id": link.id,
"id": link.task_id,
"title": link.task.title,
"board_id": link.task.board_id,
"board_name": link.task.board.name,
"column_id": link.task.column_id,
"column_name": link.task.column.name,
"column_color": link.task.column.color,
"priority": link.task.priority,
"priority_display": link.task.get_priority_display(),
}
for link in links
]
class GuestBriefSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(read_only=True)
avatar = Base64ImageField(read_only=True)
visits_count = serializers.SerializerMethodField()
class Meta:
model = Guest
fields = (
"id",
"full_name",
"last_name",
"first_name",
"patronymic",
"organization",
"email",
"phone",
"avatar",
"visits_count",
"is_active",
"is_blacklisted",
)
def get_visits_count(self, obj):
annotated_count = getattr(obj, "visits_count", None)
if annotated_count is not None:
return annotated_count
return obj.visits.count()
class GuestSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(read_only=True)
created_by = EmployeeBriefSerializer(read_only=True)
avatar = Base64ImageField(required=False, allow_null=True)
document_folder = FolderBriefSerializer(read_only=True)
documents = DocumentReadSerializer(many=True, read_only=True)
comments_count = serializers.SerializerMethodField()
visits_count = serializers.SerializerMethodField()
linked_tasks = serializers.SerializerMethodField()
class Meta:
model = Guest
fields = (
"id",
"full_name",
"last_name",
"first_name",
"patronymic",
"birth_date",
"phone",
"email",
"avatar",
"organization",
"position",
"comments_count",
"visits_count",
"linked_tasks",
"document_folder",
"documents",
"created_by",
"is_active",
"is_blacklisted",
"ldap_username",
"ldap_upn",
"ldap_last_synced_at",
"ldap_last_error",
"created_at",
"updated_at",
)
read_only_fields = (
"id",
"full_name",
"comments_count",
"visits_count",
"linked_tasks",
"document_folder",
"documents",
"created_by",
"is_active",
"is_blacklisted",
"ldap_username",
"ldap_upn",
"ldap_last_synced_at",
"ldap_last_error",
"created_at",
"updated_at",
)
def get_comments_count(self, obj):
annotated_count = getattr(obj, "comments_count", None)
if annotated_count is not None:
return annotated_count
return comments_helpers.get_comment_count(obj)
def get_visits_count(self, obj):
annotated_count = getattr(obj, "visits_count", None)
if annotated_count is not None:
return annotated_count
return obj.visits.count()
def get_linked_tasks(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
return _linked_task_payloads(obj, "guest", user)
class GuestVisitEventSerializer(serializers.ModelSerializer):
actor = EmployeeBriefSerializer(read_only=True)
class Meta:
model = GuestVisitEvent
fields = (
"id",
"event_type",
"actor",
"from_status",
"to_status",
"comment",
"metadata",
"created_at",
)
class GuestVisitReadSerializer(serializers.ModelSerializer):
guest = GuestBriefSerializer(read_only=True)
inviter = EmployeeBriefSerializer(read_only=True)
decided_by = EmployeeBriefSerializer(read_only=True)
documents = DocumentReadSerializer(many=True, read_only=True)
events = GuestVisitEventSerializer(many=True, read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
is_active_now = serializers.BooleanField(read_only=True)
is_expired = serializers.BooleanField(read_only=True)
can_edit = serializers.SerializerMethodField()
can_submit = serializers.SerializerMethodField()
can_decide = serializers.SerializerMethodField()
can_approve = serializers.SerializerMethodField()
can_reject = serializers.SerializerMethodField()
can_request_info = serializers.SerializerMethodField()
can_cancel = serializers.SerializerMethodField()
can_revoke = serializers.SerializerMethodField()
can_return_to_work = serializers.SerializerMethodField()
can_sync_ldap = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField()
comments_count = serializers.IntegerField(read_only=True, default=0)
has_unread_info_response = serializers.SerializerMethodField()
linked_tasks = serializers.SerializerMethodField()
class Meta:
model = GuestVisit
fields = (
"id",
"guest",
"inviter",
"inviter_snapshot_name",
"inviter_snapshot_email",
"host_department",
"purpose",
"visit_comment",
"status",
"status_display",
"access_starts_at",
"access_expires_at",
"all_day",
"unlimited",
"documents",
"submitted_at",
"decided_by",
"decided_at",
"decision_comment",
"cancelled_at",
"cancel_reason",
"revoked_at",
"revoke_reason",
"expired_at",
"inviter_inactive",
"created_at",
"updated_at",
"is_active_now",
"is_expired",
"can_edit",
"can_submit",
"can_decide",
"can_approve",
"can_reject",
"can_request_info",
"can_cancel",
"can_revoke",
"can_return_to_work",
"can_sync_ldap",
"can_delete",
"comments_count",
"has_unread_info_response",
"linked_tasks",
"events",
)
def _user(self):
request = self.context.get("request")
return getattr(request, "user", None)
@staticmethod
def _inviter_is_active(obj):
return bool(
obj.inviter.is_active
and getattr(obj.inviter, "is_actually_active", True)
)
@staticmethod
def _period_is_not_past(obj):
return bool(
obj.unlimited
or not obj.access_expires_at
or obj.access_expires_at > timezone.now()
)
def get_can_edit(self, obj):
return obj.can_edit_by(self._user())
def get_can_submit(self, obj):
user = self._user()
return bool(
user
and obj.inviter_id == user.id
and obj.status in {GuestVisitStatus.DRAFT, GuestVisitStatus.NEEDS_INFO}
and self._period_is_not_past(obj)
)
def get_can_decide(self, obj):
return can_decide_guest_visit(self._user())
def get_can_approve(self, obj):
return bool(
can_decide_guest_visit(self._user())
and obj.status in {GuestVisitStatus.PENDING, GuestVisitStatus.REJECTED}
and not obj.inviter_inactive
and self._inviter_is_active(obj)
and self._period_is_not_past(obj)
)
def get_can_reject(self, obj):
return bool(
can_decide_guest_visit(self._user())
and obj.status
in {
GuestVisitStatus.PENDING,
GuestVisitStatus.NEEDS_INFO,
GuestVisitStatus.APPROVED,
}
)
def get_can_request_info(self, obj):
return bool(can_decide_guest_visit(self._user()) and obj.status == GuestVisitStatus.PENDING)
def get_can_cancel(self, obj):
user = self._user()
return bool(
user
and (is_guest_admin(user) or obj.inviter_id == user.id)
and obj.status
in {
GuestVisitStatus.DRAFT,
GuestVisitStatus.PENDING,
GuestVisitStatus.NEEDS_INFO,
GuestVisitStatus.APPROVED,
}
)
def get_can_revoke(self, obj):
return bool(
can_decide_guest_visit(self._user())
and obj.status == GuestVisitStatus.APPROVED
)
def get_can_return_to_work(self, obj):
user = self._user()
return bool(
user
and (is_guest_admin(user) or obj.inviter_id == user.id)
and obj.status
in {
GuestVisitStatus.CANCELLED,
GuestVisitStatus.REVOKED,
GuestVisitStatus.EXPIRED,
}
and not obj.inviter_inactive
and self._inviter_is_active(obj)
and self._period_is_not_past(obj)
)
def get_can_sync_ldap(self, obj):
return can_manage_guest_account(self._user())
def get_can_delete(self, obj):
user = self._user()
return bool(user and (is_guest_admin(user) or obj.inviter_id == user.id))
def get_has_unread_info_response(self, obj):
return has_unread_info_response_for_user(obj, self._user())
def get_linked_tasks(self, obj):
from tasks.models import TaskLinkedObjectKind
return _linked_task_payloads(
obj,
TaskLinkedObjectKind.GUEST_VISIT,
self._user(),
)
class GuestVisitWriteSerializer(serializers.Serializer):
guest_id = serializers.IntegerField(required=False)
guest = GuestSerializer(required=False)
guest_comment = serializers.CharField(required=False, allow_blank=True)
purpose = serializers.CharField(required=False, allow_blank=True)
visit_comment = serializers.CharField(required=False, allow_blank=True)
all_day = serializers.BooleanField(required=False, default=True)
unlimited = serializers.BooleanField(required=False, default=False)
access_starts_at = serializers.DateTimeField(required=False, allow_null=True)
access_expires_at = serializers.DateTimeField(required=False, allow_null=True)
date_from = serializers.DateField(required=False)
date_to = serializers.DateField(required=False)
document_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1),
required=False,
allow_empty=True,
)
def validate(self, attrs):
instance = getattr(self, "instance", None)
unlimited = attrs.get("unlimited", getattr(instance, "unlimited", False))
all_day = attrs.get("all_day", getattr(instance, "all_day", True))
if all_day and "date_from" in attrs and "date_to" in attrs:
starts_at, expires_at = normalize_all_day_range(
attrs.pop("date_from"),
attrs.pop("date_to"),
)
attrs["access_starts_at"] = starts_at
attrs["access_expires_at"] = expires_at
starts_at = attrs.get("access_starts_at", getattr(instance, "access_starts_at", None))
expires_at = attrs.get("access_expires_at", getattr(instance, "access_expires_at", None))
if not unlimited:
if starts_at and expires_at and starts_at >= expires_at:
raise serializers.ValidationError(
{"access_expires_at": "Окончание должно быть позже начала."}
)
else:
attrs["access_expires_at"] = None
try:
validate_guest_visit_period_not_past(
expires_at=expires_at,
unlimited=unlimited,
)
except ValueError as exc:
raise serializers.ValidationError(
{"access_expires_at": str(exc)}
) from exc
if not instance and not attrs.get("guest_id") and not attrs.get("guest"):
raise serializers.ValidationError({"guest": "Укажите гостя."})
if not instance:
if not (attrs.get("purpose") or "").strip():
raise serializers.ValidationError({"purpose": "Укажите цель приглашения."})
if not unlimited and not (starts_at and expires_at):
raise serializers.ValidationError(
{"access_starts_at": "Укажите период доступа или бессрочный доступ."}
)
if getattr(settings, "GUESTS_REQUIRE_ID_DOCUMENT", False) and not attrs.get("document_ids"):
raise serializers.ValidationError(
{"document_ids": "Прикрепите документ гостя."}
)
elif instance.status in GUEST_VISIT_PERIOD_BLOCKING_STATUSES:
try:
validate_guest_visit_period_available(
guest=instance.guest,
starts_at=starts_at,
expires_at=expires_at,
unlimited=unlimited,
exclude_pk=instance.pk,
)
except ValueError as exc:
raise serializers.ValidationError(
{"access_starts_at": str(exc)}
) from exc
return attrs
def _get_or_create_guest(self, attrs):
guest_id = attrs.pop("guest_id", None)
guest_data = attrs.pop("guest", None)
if guest_id:
try:
return Guest.objects.get(pk=guest_id)
except Guest.DoesNotExist as exc:
raise serializers.ValidationError(
{"guest_id": "Гость не найден."}
) from exc
request = self.context["request"]
serializer = GuestSerializer(data=guest_data)
serializer.is_valid(raise_exception=True)
return serializer.save(created_by=request.user)
def create(self, validated_data):
document_ids = validated_data.pop("document_ids", [])
guest_comment = (validated_data.pop("guest_comment", "") or "").strip()
guest = self._get_or_create_guest(validated_data)
try:
validate_guest_visit_period_available(
guest=guest,
starts_at=validated_data.get("access_starts_at"),
expires_at=validated_data.get("access_expires_at"),
unlimited=bool(validated_data.get("unlimited", False)),
)
visit = GuestVisitWorkflow.create_visit(
actor=self.context["request"].user,
guest=guest,
**validated_data,
)
except ValueError as exc:
raise serializers.ValidationError(
{"access_starts_at": str(exc)}
) from exc
if document_ids:
documents = Document.objects.filter(id__in=document_ids)
visit.documents.set(documents)
guest.documents.add(*documents)
if guest_comment:
comments_helpers.create_comment(
guest,
self.context["request"].user,
guest_comment,
)
return visit
def update(self, instance, validated_data):
document_ids = validated_data.pop("document_ids", None)
validated_data.pop("guest_comment", None)
validated_data.pop("guest", None)
validated_data.pop("guest_id", None)
for field in (
"purpose",
"visit_comment",
"all_day",
"unlimited",
"access_starts_at",
"access_expires_at",
):
if field in validated_data:
setattr(instance, field, validated_data[field])
instance.save()
if document_ids is not None:
documents = Document.objects.filter(id__in=document_ids)
instance.documents.set(documents)
instance.guest.documents.add(*documents)
return instance
class GuestSearchSerializer(GuestBriefSerializer):
pass