541 lines
18 KiB
Python
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
|