174 lines
5.4 KiB
Python

"""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 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:
recipients = User.objects.filter(is_active=True, id__in=set(recipient_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:
document.acknowledgement_recipients.set(
User.objects.filter(is_active=True, id__in=set(recipient_ids))
)
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