"""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(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 @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, )