"""Application commands for document persistence and audience updates.""" import logging from collections.abc import Mapping, Sequence from dataclasses import dataclass from typing import Any from django.contrib.auth import get_user_model from django.db import transaction from django.db.models import Q from documents.models import Document, DocumentTag from documents.notifications.handlers import notify_document_audience from employees.models import Department logger = logging.getLogger(__name__) User = get_user_model() @dataclass(frozen=True) class DocumentAudienceUpdate: recipient_ids: Sequence[int] | None = None department_ids: Sequence[int] | None = None acknowledgement_recipient_ids: Sequence[int] | None = None acknowledgement_department_ids: Sequence[int] | None = None tag_ids: Sequence[int] | None = None def _set_departments(document: Document, department_ids: Sequence[int]) -> None: departments = Department.objects.filter(id__in=set(department_ids)) document.departments.set(departments) logger.debug( "Document departments updated", extra={ "event": "document.departments.updated", "document_id": document.id, "requested_count": len(department_ids), "updated_count": departments.count(), }, ) def _set_recipients(document: Document, recipient_ids: Sequence[int]) -> None: existing_ids = set(document.recipients.values_list("id", flat=True)) recipients = User.objects.visible_in_ui(include_ids=existing_ids).filter( id__in=set(recipient_ids) ).filter( Q(is_active=True) | Q(id__in=existing_ids) ) document.recipients.set(recipients) logger.debug( "Document recipients updated", extra={ "event": "document.recipients.updated", "document_id": document.id, "requested_count": len(recipient_ids), "updated_count": recipients.count(), }, ) def _set_acknowledgement_departments( document: Document, department_ids: Sequence[int], ) -> None: document.acknowledgement_departments.set( Department.objects.filter(id__in=set(department_ids)) ) def _set_acknowledgement_recipients( document: Document, recipient_ids: Sequence[int], ) -> None: existing_ids = set( document.acknowledgement_recipients.values_list("id", flat=True) ) recipients = User.objects.visible_in_ui(include_ids=existing_ids).filter( id__in=set(recipient_ids) ).filter( Q(is_active=True) | Q(id__in=existing_ids) ) document.acknowledgement_recipients.set(recipients) def _set_tags(document: Document, tag_ids: Sequence[int]) -> None: tags = DocumentTag.objects.filter(id__in=set(tag_ids)) document.tags.set(tags) logger.debug( "Document tags updated", extra={ "event": "document.tags.updated", "document_id": document.id, "requested_count": len(tag_ids), "updated_count": tags.count(), }, ) def _apply_audience_update( document: Document, audience: DocumentAudienceUpdate, ) -> None: if not document.acknowledgement_required or document.acknowledgement_for_all: document.acknowledgement_recipients.clear() document.acknowledgement_departments.clear() else: if audience.acknowledgement_department_ids is not None: _set_acknowledgement_departments( document, audience.acknowledgement_department_ids, ) if audience.acknowledgement_recipient_ids is not None: _set_acknowledgement_recipients( document, audience.acknowledgement_recipient_ids, ) if document.sent_to_all: document.recipients.clear() document.departments.clear() else: if audience.department_ids is not None: _set_departments(document, audience.department_ids) if audience.recipient_ids is not None: _set_recipients(document, audience.recipient_ids) if audience.tag_ids is not None: _set_tags(document, audience.tag_ids) @transaction.atomic def create_document( *, uploader, attributes: Mapping[str, Any], audience: DocumentAudienceUpdate, ) -> Document: """Create a document, apply its audience and emit one final notification.""" logger.info( "Document creation started", extra={ "event": "document.creation.started", "sent_to_all": attributes.get("sent_to_all", True), "department_count": len(audience.department_ids or ()), "recipient_count": len(audience.recipient_ids or ()), "tag_count": len(audience.tag_ids or ()), "uploader_id": uploader.id if uploader else None, }, ) document = Document(uploaded_by=uploader, **dict(attributes)) document._suppress_document_notifications = True document.save() _apply_audience_update(document, audience) document._suppress_document_notifications = False notify_document_audience(document) logger.info( "Document created", extra={ "event": "document.created", "document_id": document.id, "sent_to_all": document.sent_to_all, "uploader_id": uploader.id if uploader else None, }, ) return document @transaction.atomic def update_document( *, document_id: int, attributes: Mapping[str, Any], audience: DocumentAudienceUpdate, ) -> Document: """Update document fields and all supplied audience relations atomically.""" document = Document.objects.select_for_update().get(pk=document_id) for field, value in attributes.items(): setattr(document, field, value) document.save() _apply_audience_update(document, audience) return document