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