314 lines
11 KiB
Python
314 lines
11 KiB
Python
"""Write representation and validation for documents."""
|
||
|
||
import os
|
||
from typing import Any
|
||
|
||
from django.contrib.auth import get_user_model
|
||
from django.db.models import Q
|
||
from rest_framework import serializers
|
||
|
||
from documents.application import (
|
||
DocumentAudienceUpdate,
|
||
create_document,
|
||
update_document,
|
||
)
|
||
from documents.models import Document
|
||
|
||
from ..fields import FilerFileField as FilerFileSerializerField
|
||
from .recipient_ids import RecipientIDsField
|
||
|
||
User = get_user_model()
|
||
|
||
|
||
class DocumentWriteSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для записи/обновления документа.
|
||
|
||
Поддерживает:
|
||
- загрузку файла (multipart) через django-filer
|
||
- sent_to_all
|
||
- department_ids (отделы-получатели)
|
||
- recipient_ids (repeat/JSON/CSV) при sent_to_all=false
|
||
- tag_ids (теги)
|
||
"""
|
||
|
||
title = serializers.CharField(
|
||
required=False,
|
||
allow_blank=True,
|
||
max_length=255,
|
||
help_text="Название документа. Если пусто, будет взято из имени файла.",
|
||
)
|
||
extracted_text = serializers.CharField(required=False, allow_blank=True)
|
||
|
||
recipient_ids = RecipientIDsField(
|
||
write_only=True,
|
||
required=False,
|
||
help_text=("Список ID сотрудников (repeat/JSON/CSV) при sent_to_all=false."),
|
||
)
|
||
|
||
department_ids = RecipientIDsField(
|
||
write_only=True,
|
||
required=False,
|
||
help_text="Список ID отделов-получателей.",
|
||
)
|
||
|
||
acknowledgement_recipient_ids = RecipientIDsField(
|
||
write_only=True,
|
||
required=False,
|
||
help_text="Список ID сотрудников, обязанных ознакомиться.",
|
||
)
|
||
|
||
acknowledgement_department_ids = RecipientIDsField(
|
||
write_only=True,
|
||
required=False,
|
||
help_text="Список ID отделов, обязанных ознакомиться.",
|
||
)
|
||
|
||
tag_ids = RecipientIDsField(
|
||
write_only=True,
|
||
required=False,
|
||
help_text="Список ID тегов для привязки к документу.",
|
||
)
|
||
|
||
file = FilerFileSerializerField(
|
||
required=False,
|
||
allow_null=True,
|
||
help_text="Файл для загрузки через django-filer",
|
||
)
|
||
|
||
class Meta:
|
||
model = Document
|
||
fields = (
|
||
"id",
|
||
"title",
|
||
"description",
|
||
"file",
|
||
"folder",
|
||
"extracted_text",
|
||
"sent_to_all",
|
||
"is_regulation",
|
||
"acknowledgement_required",
|
||
"acknowledgement_for_all",
|
||
"department_ids",
|
||
"recipient_ids",
|
||
"acknowledgement_department_ids",
|
||
"acknowledgement_recipient_ids",
|
||
"tag_ids",
|
||
)
|
||
|
||
def _validate_requested_users(self, attrs, field_name, relation_name) -> None:
|
||
# Creation keeps the established API contract: unknown/inactive IDs are
|
||
# ignored by the application command. On update, however, only already
|
||
# linked inactive users may be retained.
|
||
if self.instance is None or field_name not in attrs:
|
||
return
|
||
requested_ids = set(attrs[field_name])
|
||
existing_ids = (
|
||
set(getattr(self.instance, relation_name).values_list("id", flat=True))
|
||
if self.instance is not None
|
||
else set()
|
||
)
|
||
allowed_ids = set(
|
||
User.objects.filter(id__in=requested_ids)
|
||
.filter(Q(is_active=True) | Q(id__in=existing_ids))
|
||
.values_list("id", flat=True)
|
||
)
|
||
if requested_ids - allowed_ids:
|
||
raise serializers.ValidationError(
|
||
{field_name: "Выбирать можно только активных сотрудников."}
|
||
)
|
||
|
||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||
"""Согласованность полей.
|
||
|
||
Args:
|
||
attrs (dict): Данные после field-level валидаций.
|
||
|
||
Returns:
|
||
dict: Те же данные.
|
||
|
||
Raises:
|
||
serializers.ValidationError: Если sent_to_all=false
|
||
и нет recipient_ids или department_ids.
|
||
"""
|
||
file_obj = attrs.get("file")
|
||
|
||
if not str(attrs.get("title") or "").strip():
|
||
if file_obj:
|
||
file_name = getattr(file_obj, "original_filename", "") or getattr(
|
||
file_obj, "name", ""
|
||
)
|
||
default_title = os.path.splitext(os.path.basename(file_name))[0].strip()
|
||
attrs["title"] = default_title or file_name or "Документ"
|
||
elif self.instance is None:
|
||
attrs["title"] = "Документ"
|
||
elif self.instance is not None:
|
||
attrs.pop("title", None)
|
||
|
||
instance = self.instance
|
||
self._validate_requested_users(attrs, "recipient_ids", "recipients")
|
||
self._validate_requested_users(
|
||
attrs,
|
||
"acknowledgement_recipient_ids",
|
||
"acknowledgement_recipients",
|
||
)
|
||
sent_to_all = attrs.get(
|
||
"sent_to_all",
|
||
instance.sent_to_all if instance is not None else True,
|
||
)
|
||
recipient_ids = set(
|
||
attrs.get(
|
||
"recipient_ids",
|
||
instance.recipients.values_list("pk", flat=True)
|
||
if instance is not None
|
||
else [],
|
||
)
|
||
)
|
||
department_ids = set(
|
||
attrs.get(
|
||
"department_ids",
|
||
instance.departments.values_list("pk", flat=True)
|
||
if instance is not None
|
||
else [],
|
||
)
|
||
)
|
||
|
||
if sent_to_all is False and not recipient_ids and not department_ids:
|
||
raise serializers.ValidationError(
|
||
{
|
||
"recipient_ids": (
|
||
"Укажите получателей, отделы или установите sent_to_all=true."
|
||
)
|
||
}
|
||
)
|
||
|
||
acknowledgement_required = attrs.get(
|
||
"acknowledgement_required",
|
||
instance.acknowledgement_required if instance is not None else True,
|
||
)
|
||
acknowledgement_for_all = attrs.get(
|
||
"acknowledgement_for_all",
|
||
instance.acknowledgement_for_all if instance is not None else True,
|
||
)
|
||
acknowledgement_recipient_ids = set(
|
||
attrs.get(
|
||
"acknowledgement_recipient_ids",
|
||
instance.acknowledgement_recipients.values_list("pk", flat=True)
|
||
if instance is not None
|
||
else [],
|
||
)
|
||
)
|
||
acknowledgement_department_ids = set(
|
||
attrs.get(
|
||
"acknowledgement_department_ids",
|
||
instance.acknowledgement_departments.values_list("pk", flat=True)
|
||
if instance is not None
|
||
else [],
|
||
)
|
||
)
|
||
|
||
if (
|
||
acknowledgement_required
|
||
and not acknowledgement_for_all
|
||
and not acknowledgement_recipient_ids
|
||
and not acknowledgement_department_ids
|
||
):
|
||
raise serializers.ValidationError(
|
||
{
|
||
"acknowledgement_recipient_ids": (
|
||
"Выберите сотрудников, отделы или включите "
|
||
"ознакомление для всех с доступом."
|
||
)
|
||
}
|
||
)
|
||
|
||
if acknowledgement_required and not acknowledgement_for_all and not sent_to_all:
|
||
invalid_departments = acknowledgement_department_ids - department_ids
|
||
if invalid_departments:
|
||
raise serializers.ValidationError(
|
||
{
|
||
"acknowledgement_department_ids": (
|
||
"Отдел для ознакомления должен иметь доступ к документу."
|
||
)
|
||
}
|
||
)
|
||
|
||
allowed_recipients = set(
|
||
User.objects.filter(is_active=True)
|
||
.filter(
|
||
Q(pk__in=recipient_ids)
|
||
| Q(
|
||
departments_links__department_id__in=department_ids,
|
||
departments_links__is_active=True,
|
||
)
|
||
| Q(
|
||
role_assignments__role__department_id__in=department_ids,
|
||
role_assignments__is_active=True,
|
||
)
|
||
)
|
||
.values_list("pk", flat=True)
|
||
)
|
||
existing_acknowledgement_ids = (
|
||
set(
|
||
instance.acknowledgement_recipients.values_list("pk", flat=True)
|
||
)
|
||
if instance is not None
|
||
else set()
|
||
)
|
||
allowed_recipients.update(
|
||
acknowledgement_recipient_ids & existing_acknowledgement_ids
|
||
)
|
||
invalid_recipients = acknowledgement_recipient_ids - allowed_recipients
|
||
if invalid_recipients:
|
||
raise serializers.ValidationError(
|
||
{
|
||
"acknowledgement_recipient_ids": (
|
||
"Сотрудник для ознакомления должен иметь доступ "
|
||
"к документу."
|
||
)
|
||
}
|
||
)
|
||
return attrs
|
||
|
||
@staticmethod
|
||
def _pop_audience_update(
|
||
validated_data: dict[str, Any],
|
||
*,
|
||
default,
|
||
) -> DocumentAudienceUpdate:
|
||
return DocumentAudienceUpdate(
|
||
recipient_ids=validated_data.pop("recipient_ids", default),
|
||
department_ids=validated_data.pop("department_ids", default),
|
||
acknowledgement_recipient_ids=validated_data.pop(
|
||
"acknowledgement_recipient_ids",
|
||
default,
|
||
),
|
||
acknowledgement_department_ids=validated_data.pop(
|
||
"acknowledgement_department_ids",
|
||
default,
|
||
),
|
||
tag_ids=validated_data.pop("tag_ids", default),
|
||
)
|
||
|
||
def create(self, validated_data: dict[str, Any]) -> Document:
|
||
"""Create a document through the atomic application command."""
|
||
audience = self._pop_audience_update(validated_data, default=())
|
||
request = self.context.get("request")
|
||
return create_document(
|
||
uploader=getattr(request, "user", None),
|
||
attributes=validated_data,
|
||
audience=audience,
|
||
)
|
||
|
||
def update(
|
||
self,
|
||
instance: Document,
|
||
validated_data: dict[str, Any],
|
||
) -> Document:
|
||
"""Update a document through the atomic application command."""
|
||
audience = self._pop_audience_update(validated_data, default=None)
|
||
return update_document(
|
||
document_id=instance.id,
|
||
attributes=validated_data,
|
||
audience=audience,
|
||
)
|