Corp/backend/api/v1/documents/serializers.py

963 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
import logging
import os
from typing import Any, Dict, Sequence, Iterable
from drf_spectacular.utils import extend_schema_field
from django.http import QueryDict
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.utils.text import slugify
from documents.models import (
Document,
DocumentAcknowledgement,
DocumentDisplayState,
)
from rest_framework import serializers
from ..employees.serializers import EmployeeBriefSerializer
from .fields import FilerFileField as FilerFileSerializerField
logger = logging.getLogger(__name__)
User = get_user_model()
@extend_schema_field(serializers.ListField(child=serializers.IntegerField()))
class RecipientIDsField(serializers.Field):
"""Принимает recipient_ids в форматах: список, JSON-строка или CSV.
Примеры:
- [1, 2, 3]
- "[1,2,3]"
- "1,2,3"
Возвращает:
list[int]: Нормализованный список целых ID.
Raises:
serializers.ValidationError: Некорректный формат/значения.
"""
def to_internal_value(self, data: Any) -> list[int]:
"""Парсит raw-значение из запроса в список int.
При multipart (QueryDict) поддерживает repeat-params:
recipient_ids=1&recipient_ids=2...
"""
# 1) Спец. случай: multipart/QueryDict с repeat-params
# DRF в обычное Field передаёт "последнее" значение, поэтому
# забираем полный список через
# parent.initial_data.getlist(self.field_name)
parent_data = getattr(
self.parent, "initial_data", None
) # type: ignore[attr-defined]
if isinstance(parent_data, QueryDict):
values = parent_data.getlist(
self.field_name
) # type: ignore[attr-defined]
if len(values) > 1:
data = values
elif len(values) == 1 and not isinstance(data, (list, tuple)):
# один элемент: оставим строкой — ниже распарсится как
# JSON/CSV/число
data = values[0]
# 2) Уже список/кортеж?
if isinstance(data, (list, tuple)):
# если это ['1','2','3'] — ок; если это ['[1,2]'] — распакуем как
# строку
if (
len(data) == 1
and isinstance(data[0], str)
and (data[0].strip().startswith("[") or "," in data[0])
):
data = data[0]
else:
vals = list(data)
# 3) Строка: JSON-массив или CSV или одно число
if isinstance(data, str):
s = data.strip()
try:
if s.startswith("[") and s.endswith("]"):
parsed = json.loads(s)
if not isinstance(parsed, list):
raise serializers.ValidationError(
"Ожидается JSON-массив ID."
)
vals = parsed
elif "," in s:
vals = [p.strip() for p in s.split(",") if p.strip()]
else:
vals = [s]
except serializers.ValidationError:
raise
except Exception as e:
raise serializers.ValidationError(
f"Неверный формат recipient_ids: {e!s}"
)
elif data in (None, ""):
vals = []
elif not isinstance(data, (list, tuple)):
raise serializers.ValidationError(
"Ожидается список, JSON-строка или CSV-строка."
)
# Приведение к int
try:
return [int(x) for x in vals]
except Exception:
raise serializers.ValidationError(
"ID получателей должны быть целыми числами."
)
def to_representation(self, value: Iterable[int]) -> list[int]:
"""Отдаёт список ID (на всякий случай; поле write_only)."""
if value is None:
return []
return list(value)
class DepartmentBriefSerializer(serializers.Serializer):
"""Краткий сериализатор отдела."""
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
class FolderSerializer(serializers.Serializer):
"""Сериализатор для папки filer.Folder."""
id = serializers.IntegerField(read_only=True)
name = serializers.CharField()
parent_id = serializers.IntegerField(
source="parent.id", read_only=True, allow_null=True
)
path = serializers.SerializerMethodField()
document_count = serializers.IntegerField(read_only=True, default=0)
def get_path(self, obj) -> str:
"""Возвращает полный путь папки."""
path_parts = []
current = obj
while current:
path_parts.insert(0, current.name)
current = current.parent
return " / ".join(path_parts)
class FolderBriefSerializer(serializers.Serializer):
"""Краткий сериализатор папки для вложенного отображения."""
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
class DocumentTagSerializer(serializers.Serializer):
"""Сериализатор для тегов документов."""
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(max_length=100)
slug = serializers.SlugField(required=False, allow_blank=True, max_length=100)
color = serializers.CharField(required=False)
description = serializers.CharField(required=False, allow_blank=True)
document_count = serializers.SerializerMethodField()
def get_document_count(self, obj) -> int:
"""Возвращает количество документов с этим тегом."""
return obj.documents.count() if hasattr(obj, "documents") else 0
def validate_slug(self, value):
"""Проверяет уникальность slug."""
from documents.models import DocumentTag
if not value:
return value
# Проверяем дубликаты (исключая текущий объект при обновлении)
qs = DocumentTag.objects.filter(slug=value)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise serializers.ValidationError(
"Tag with this slug already exists."
)
return value
def validate_name(self, value):
"""Проверяет уникальность названия тега."""
from documents.models import DocumentTag
name = value.strip()
qs = DocumentTag.objects.filter(name__iexact=name)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise serializers.ValidationError(
"Тег с таким названием уже существует."
)
return name
def _get_unique_slug(self, name: str) -> str:
"""Генерирует уникальный slug из названия."""
from documents.models import DocumentTag
base_slug = slugify(name, allow_unicode=True) or "tag"
base_slug = base_slug[:90].strip("-") or "tag"
slug = base_slug
index = 2
while DocumentTag.objects.filter(slug=slug).exists():
suffix = f"-{index}"
slug = f"{base_slug[:100 - len(suffix)]}{suffix}"
index += 1
return slug
def create(self, validated_data):
"""Создаёт новый тег документа."""
from documents.models import DocumentTag
if not validated_data.get("slug"):
validated_data["slug"] = self._get_unique_slug(
validated_data["name"]
)
return DocumentTag.objects.create(**validated_data)
def update(self, instance, validated_data):
"""Обновляет тег документа."""
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
class VersionSerializer(serializers.Serializer):
"""Сериализатор для версий документа (django-reversion)."""
id = serializers.IntegerField(read_only=True)
revision_id = serializers.IntegerField(source="revision.id", read_only=True)
date_created = serializers.DateTimeField(
source="revision.date_created", read_only=True
)
user = serializers.SerializerMethodField()
comment = serializers.CharField(
source="revision.comment", read_only=True, allow_blank=True
)
# Данные версии
data = serializers.SerializerMethodField()
def get_user(self, obj) -> dict | None:
"""Возвращает информацию о пользователе, создавшем версию."""
if not obj.revision.user:
return None
user = obj.revision.user
return {
"id": user.id,
"full_name": f"{user.last_name} {user.first_name}".strip(),
"avatar_url": getattr(user, "avatar_url", None),
}
def get_data(self, obj) -> dict:
"""Возвращает данные версии документа."""
return obj.field_dict
class ActivityItemSerializer(serializers.Serializer):
"""Сериализатор для элемента timeline активности."""
type = serializers.CharField() # 'version', 'audit', 'acknowledgement'
timestamp = serializers.DateTimeField()
user = serializers.DictField(allow_null=True)
action = serializers.CharField()
details = serializers.DictField(allow_null=True)
class DocumentReadSerializer(serializers.ModelSerializer):
"""Сериализатор для чтения документа.
Включает список получателей и флаг, ознакомился ли текущий пользователь.
"""
created_by = EmployeeBriefSerializer(read_only=True)
uploaded_by = EmployeeBriefSerializer(read_only=True)
modified_by = EmployeeBriefSerializer(read_only=True)
recipients = EmployeeBriefSerializer(many=True, read_only=True)
departments = DepartmentBriefSerializer(many=True, read_only=True)
acknowledgement_recipients = EmployeeBriefSerializer(
many=True, read_only=True
)
acknowledgement_departments = DepartmentBriefSerializer(
many=True, read_only=True
)
folder = FolderBriefSerializer(read_only=True)
folder_path = serializers.CharField(read_only=True)
tags = DocumentTagSerializer(many=True, read_only=True)
created_at = serializers.DateTimeField(read_only=True)
file_url = FilerFileSerializerField(source="file", read_only=True)
file_name = serializers.CharField(
source="file.name", read_only=True, allow_null=True
)
file_size = serializers.IntegerField(
source="file.size", read_only=True, allow_null=True
)
is_acknowledged = serializers.SerializerMethodField()
is_maximally_hidden = serializers.SerializerMethodField()
acknowledgement_required_for_user = serializers.SerializerMethodField()
acknowledged_count = serializers.SerializerMethodField()
acknowledgement_total = serializers.SerializerMethodField()
comments_count = serializers.SerializerMethodField()
linked_tasks = serializers.SerializerMethodField()
class Meta:
model = Document
fields = (
"id",
"title",
"description",
"extracted_text", # Для полнотекстового поиска
"folder", # Папка документа
"folder_path", # Полный путь папки
"tags", # Теги
"created_by", # Создатель документа
"created_at", # Дата создания
"uploaded_by",
"uploaded_at",
"modified_by",
"modified_at",
"sent_to_all",
"is_regulation",
"acknowledgement_required", # Требуется ли ознакомление
"acknowledgement_for_all",
"acknowledgement_required_for_user",
"acknowledged_count",
"acknowledgement_total",
"comments_count",
"departments",
"recipients",
"acknowledgement_departments",
"acknowledgement_recipients",
"file_url",
"file_name",
"file_size",
"is_acknowledged",
"is_maximally_hidden",
"linked_tasks",
)
def get_is_acknowledged(self, obj: Document) -> bool:
"""Возвращает флаг ознакомления текущего пользователя.
Args:
obj (Document): Документ.
Returns:
bool: True, если текущий пользователь уже ознакомился.
"""
annotated = getattr(obj, "_is_acknowledged", None)
if annotated is not None:
return bool(annotated)
request = self.context.get("request")
if not request or not request.user.is_authenticated:
return False
return DocumentAcknowledgement.objects.filter(
document=obj, user=request.user
).exists()
def get_is_maximally_hidden(self, obj: Document) -> bool:
"""Максимально ли скрыта карточка для текущего пользователя."""
annotated = getattr(obj, "_is_maximally_hidden", None)
if annotated is not None:
return bool(annotated)
request = self.context.get("request")
if not request or not request.user.is_authenticated:
return False
return DocumentDisplayState.objects.filter(
document=obj,
user=request.user,
is_maximally_hidden=True,
).exists()
def get_acknowledgement_required_for_user(self, obj: Document) -> bool:
"""Нужно ли текущему сотруднику подтверждать ознакомление."""
request = self.context.get("request")
if not request or not request.user.is_authenticated:
return False
audience_ids = getattr(obj, "_acknowledgement_user_ids", None)
if audience_ids is not None:
return request.user.pk in audience_ids
from documents.audience import user_requires_document_acknowledgement
return user_requires_document_acknowledgement(obj, request.user)
def get_acknowledged_count(self, obj: Document) -> int:
"""Количество сотрудников из аудитории, уже ознакомившихся."""
attached = getattr(obj, "_acknowledged_count", None)
if attached is not None:
return int(attached)
from documents.audience import document_acknowledgement_audience
audience = document_acknowledgement_audience(obj)
return obj.acknowledgements.filter(user__in=audience).count()
def get_acknowledgement_total(self, obj: Document) -> int:
"""Размер аудитории обязательного ознакомления."""
attached = getattr(obj, "_acknowledgement_total", None)
if attached is not None:
return int(attached)
from documents.audience import document_acknowledgement_audience
return document_acknowledgement_audience(obj).count()
def get_comments_count(self, obj: Document) -> int:
"""Количество неудалённых комментариев документа."""
attached = getattr(obj, "_comments_count", None)
if attached is not None:
return int(attached)
from communications.comments_helpers import get_comment_count
return get_comment_count(obj)
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_linked_tasks(self, obj: Document) -> list[dict]:
prefetched = getattr(obj, "_linked_task_payloads", None)
if prefetched is not None:
return prefetched
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return []
try:
from django.contrib.contenttypes.models import ContentType
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(Document)
accessible_boards = TaskBoard.objects.filter(
is_archived=False,
).filter(task_board_access_q(user))
links = (
TaskLinkedObject.objects.filter(
kind=TaskLinkedObjectKind.DOCUMENT,
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 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(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
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 = 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)
invalid_recipients = acknowledgement_recipient_ids - set(
allowed_recipients
)
if invalid_recipients:
raise serializers.ValidationError(
{
"acknowledgement_recipient_ids": (
"Сотрудник для ознакомления должен иметь доступ "
"к документу."
)
}
)
return attrs
def _set_departments(
self, doc: Document, department_ids: Sequence[int]
) -> int:
"""Привязывает отделы-получатели по списку ID.
Args:
doc (Document): Документ.
department_ids (Sequence[int]): Идентификаторы отделов.
Returns:
int: Количество привязанных отделов.
"""
from employees.models import Department
logger.info(
f"[serializers] _set_departments doc={doc.id} "
f"department_ids={list(department_ids)}"
)
if not department_ids:
doc.departments.clear()
logger.info(f"[serializers] Cleared departments for doc={doc.id}")
return 0
departments = Department.objects.filter(id__in=set(department_ids))
count = departments.count()
logger.info(
f"[serializers] Found {count} departments from "
f"{len(department_ids)} requested IDs"
)
doc.departments.set(departments)
logger.info(f"[serializers] Set {count} departments for doc={doc.id}")
return count
def _set_recipients(
self, doc: Document, recipient_ids: Sequence[int]
) -> int:
"""Привязывает получателей по списку ID (только активные).
Args:
doc (Document): Документ.
recipient_ids (Sequence[int]): Идентификаторы пользователей.
Returns:
int: Количество привязанных получателей.
"""
logger.info(
f"[serializers] _set_recipients doc={doc.id} "
f"recipient_ids={list(recipient_ids)}"
)
if not recipient_ids:
doc.recipients.clear()
logger.info(f"[serializers] Cleared recipients for doc={doc.id}")
return 0
users = User.objects.filter(is_active=True, id__in=set(recipient_ids))
count = users.count()
logger.info(
f"[serializers] Found {count} active users from "
f"{len(recipient_ids)} requested IDs"
)
doc.recipients.set(users)
logger.info(f"[serializers] Set {count} recipients for doc={doc.id}")
return count
def _set_acknowledgement_departments(
self, doc: Document, department_ids: Sequence[int]
) -> int:
from employees.models import Department
departments = Department.objects.filter(id__in=set(department_ids))
doc.acknowledgement_departments.set(departments)
return departments.count()
def _set_acknowledgement_recipients(
self, doc: Document, recipient_ids: Sequence[int]
) -> int:
users = User.objects.filter(is_active=True, id__in=set(recipient_ids))
doc.acknowledgement_recipients.set(users)
return users.count()
def _set_tags(self, doc: Document, tag_ids: Sequence[int]) -> int:
"""Привязывает теги по списку ID.
Args:
doc (Document): Документ.
tag_ids (Sequence[int]): Идентификаторы тегов.
Returns:
int: Количество привязанных тегов.
"""
from documents.models import DocumentTag
logger.info(
f"[serializers] _set_tags doc={doc.id} tag_ids={list(tag_ids)}"
)
if not tag_ids:
doc.tags.clear()
logger.info(f"[serializers] Cleared tags for doc={doc.id}")
return 0
tags = DocumentTag.objects.filter(id__in=set(tag_ids))
count = tags.count()
logger.info(
f"[serializers] Found {count} tags from {len(tag_ids)} "
f"requested IDs"
)
doc.tags.set(tags)
logger.info(f"[serializers] Set {count} tags for doc={doc.id}")
return count
def create(self, validated_data: Dict[str, Any]) -> Document:
"""Создание документа с корректной установкой
получателей и уведомлением.
Args:
validated_data (dict): Данные.
Returns:
Document: Созданный документ.
"""
recipient_ids = validated_data.pop("recipient_ids", [])
department_ids = validated_data.pop("department_ids", [])
acknowledgement_recipient_ids = validated_data.pop(
"acknowledgement_recipient_ids", []
)
acknowledgement_department_ids = validated_data.pop(
"acknowledgement_department_ids", []
)
tag_ids = validated_data.pop("tag_ids", [])
request = self.context.get("request")
uploader = getattr(request, "user", None)
sent_to_all = validated_data.get("sent_to_all", True)
logger.info(
f"[serializers] Creating document sent_to_all={sent_to_all} "
f"department_ids={list(department_ids)} "
f"recipient_ids={list(recipient_ids)} "
f"tag_ids={list(tag_ids)}"
)
# Создаём документ - сигналы из notification_signals.py сработают
# автоматически
doc = Document( # type: ignore[arg-type]
uploaded_by=uploader,
**validated_data,
)
doc._suppress_document_notifications = True
doc.save()
logger.info(
f"[serializers] Document created id={doc.id} "
f"sent_to_all={doc.sent_to_all}"
)
if (
doc.acknowledgement_required
and not doc.acknowledgement_for_all
):
self._set_acknowledgement_departments(
doc, acknowledgement_department_ids
)
self._set_acknowledgement_recipients(
doc, acknowledgement_recipient_ids
)
# Для индивидуальной доступности устанавливаем получателей.
if validated_data.get("sent_to_all") is False:
if department_ids:
logger.info(
f"[serializers] Setting departments for doc={doc.id}"
)
self._set_departments(doc, department_ids)
if recipient_ids:
logger.info(
f"[serializers] Setting recipients for doc={doc.id}"
)
self._set_recipients(doc, recipient_ids)
# Устанавливаем теги (независимо от sent_to_all)
if tag_ids:
logger.info(f"[serializers] Setting tags for doc={doc.id}")
self._set_tags(doc, tag_ids)
doc._suppress_document_notifications = False
from documents.notifications.handlers import notify_document_audience
notify_document_audience(doc)
logger.info(f"[serializers] Document creation complete id={doc.id}")
return doc
def update(
self, instance: Document, validated_data: Dict[str, Any]
) -> Document:
"""Частичное обновление документа.
Args:
instance (Document): Экземпляр.
validated_data (dict): Данные.
Returns:
Document: Обновлённый документ.
"""
recipient_ids = validated_data.pop("recipient_ids", None)
department_ids = validated_data.pop("department_ids", None)
acknowledgement_recipient_ids = validated_data.pop(
"acknowledgement_recipient_ids", None
)
acknowledgement_department_ids = validated_data.pop(
"acknowledgement_department_ids", None
)
tag_ids = validated_data.pop("tag_ids", None)
for f in (
"title",
"description",
"extracted_text",
"sent_to_all",
"is_regulation",
"acknowledgement_required",
"acknowledgement_for_all",
"file",
"folder",
):
if f in validated_data:
setattr(instance, f, validated_data[f])
instance.save()
if not instance.acknowledgement_required or instance.acknowledgement_for_all:
instance.acknowledgement_recipients.clear()
instance.acknowledgement_departments.clear()
else:
if acknowledgement_department_ids is not None:
self._set_acknowledgement_departments(
instance, acknowledgement_department_ids
)
if acknowledgement_recipient_ids is not None:
self._set_acknowledgement_recipients(
instance, acknowledgement_recipient_ids
)
# очищаем получателей и отделы при sent_to_all=true
if "sent_to_all" in validated_data and instance.sent_to_all:
instance.recipients.clear()
instance.departments.clear()
else:
if department_ids is not None:
self._set_departments(instance, department_ids)
if recipient_ids is not None:
self._set_recipients(instance, recipient_ids)
# Обновляем теги (независимо от sent_to_all)
if tag_ids is not None:
self._set_tags(instance, tag_ids)
return instance