Adjust document acknowledgement notifications
This commit is contained in:
parent
a794ff40c8
commit
e676045a0a
@ -306,6 +306,16 @@ class DocumentViewSet(ModelViewSet):
|
||||
Response: {"ok": true, "already": bool}
|
||||
"""
|
||||
doc = self.get_object()
|
||||
if not doc.acknowledgement_required:
|
||||
return Response(
|
||||
{
|
||||
"detail": (
|
||||
"Для этого документа не требуется подтверждение "
|
||||
"ознакомления."
|
||||
)
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
obj, created = DocumentAcknowledgement.objects.get_or_create(
|
||||
document=doc, user=request.user
|
||||
)
|
||||
|
||||
@ -23,16 +23,22 @@ class MessageTemplates:
|
||||
"""Шаблоны сообщений для уведомлений documents."""
|
||||
|
||||
@staticmethod
|
||||
def document_ready(uploader_name: str, document_title: str) -> str:
|
||||
"""Шаблон для нового документа на ознакомление."""
|
||||
return (
|
||||
f'{uploader_name} загрузил документ "{document_title}". '
|
||||
f'Требуется ознакомление.'
|
||||
)
|
||||
def document_ready(
|
||||
uploader_name: str,
|
||||
document_title: str,
|
||||
acknowledgement_required: bool = True,
|
||||
) -> str:
|
||||
"""Шаблон для нового документа."""
|
||||
message = f'{uploader_name} загрузил документ "{document_title}".'
|
||||
if acknowledgement_required:
|
||||
return f"{message} Требуется ознакомление."
|
||||
return message
|
||||
|
||||
@staticmethod
|
||||
def document_ready_title() -> str:
|
||||
def document_ready_title(acknowledgement_required: bool = True) -> str:
|
||||
"""Заголовок для нового документа."""
|
||||
if not acknowledgement_required:
|
||||
return 'Новый документ'
|
||||
return 'Новый документ на ознакомление'
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -27,6 +27,29 @@ logger = logging.getLogger(__name__)
|
||||
Employee = get_user_model()
|
||||
|
||||
|
||||
def _document_ready_payload(document, uploader_name):
|
||||
"""Собирает единый текст и данные уведомления о новом документе."""
|
||||
acknowledgement_required = bool(document.acknowledgement_required)
|
||||
return {
|
||||
"description": MessageTemplates.document_ready(
|
||||
uploader_name,
|
||||
document.title,
|
||||
acknowledgement_required=acknowledgement_required,
|
||||
),
|
||||
"data": {
|
||||
"title": MessageTemplates.document_ready_title(
|
||||
acknowledgement_required=acknowledgement_required,
|
||||
),
|
||||
"document_id": document.id,
|
||||
"uploaded_by_id": (
|
||||
document.uploaded_by.id if document.uploaded_by else None
|
||||
),
|
||||
"sent_to_all": document.sent_to_all,
|
||||
"acknowledgement_required": acknowledgement_required,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def notify_document_ready(document, recipient):
|
||||
"""
|
||||
Отправляет уведомление конкретному получателю о новом документе.
|
||||
@ -41,24 +64,16 @@ def notify_document_ready(document, recipient):
|
||||
)
|
||||
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
payload = _document_ready_payload(document, uploader_name)
|
||||
|
||||
notify.send(
|
||||
sender=document.uploaded_by,
|
||||
recipient=recipient,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(
|
||||
uploader_name, document.title
|
||||
),
|
||||
description=payload["description"],
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
"title": MessageTemplates.document_ready_title(),
|
||||
"document_id": document.id,
|
||||
"uploaded_by_id": document.uploaded_by.id
|
||||
if document.uploaded_by
|
||||
else None,
|
||||
"sent_to_all": document.sent_to_all,
|
||||
},
|
||||
data=payload["data"],
|
||||
)
|
||||
|
||||
|
||||
@ -86,6 +101,7 @@ def notify_all_employees(document):
|
||||
# Создаём уведомления
|
||||
created_count = 0
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
payload = _document_ready_payload(document, uploader_name)
|
||||
|
||||
for employee in active_employees:
|
||||
try:
|
||||
@ -94,20 +110,9 @@ def notify_all_employees(document):
|
||||
recipient=employee,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(
|
||||
uploader_name, document.title
|
||||
),
|
||||
description=payload["description"],
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
"title": MessageTemplates.document_ready_title(),
|
||||
"document_id": document.id,
|
||||
"uploaded_by_id": (
|
||||
document.uploaded_by.id
|
||||
if document.uploaded_by
|
||||
else None
|
||||
),
|
||||
"sent_to_all": document.sent_to_all,
|
||||
},
|
||||
data=payload["data"],
|
||||
)
|
||||
created_count += 1
|
||||
except Exception as e:
|
||||
@ -164,6 +169,7 @@ def notify_specific_users(document, user_ids):
|
||||
# Уведомляем получателей
|
||||
created_count = 0
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
payload = _document_ready_payload(document, uploader_name)
|
||||
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
@ -174,20 +180,9 @@ def notify_specific_users(document, user_ids):
|
||||
recipient=user,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(
|
||||
uploader_name, document.title
|
||||
),
|
||||
description=payload["description"],
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
"title": MessageTemplates.document_ready_title(),
|
||||
"document_id": document.id,
|
||||
"uploaded_by_id": (
|
||||
document.uploaded_by.id
|
||||
if document.uploaded_by
|
||||
else None
|
||||
),
|
||||
"sent_to_all": document.sent_to_all,
|
||||
},
|
||||
data=payload["data"],
|
||||
)
|
||||
created_count += 1
|
||||
except Employee.DoesNotExist:
|
||||
@ -259,6 +254,7 @@ def notify_department_employees(document, department_ids):
|
||||
# Создаём уведомления
|
||||
created_count = 0
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
payload = _document_ready_payload(document, uploader_name)
|
||||
|
||||
for employee in all_employees:
|
||||
try:
|
||||
@ -267,20 +263,9 @@ def notify_department_employees(document, department_ids):
|
||||
recipient=employee,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(
|
||||
uploader_name, document.title
|
||||
),
|
||||
description=payload["description"],
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
"title": MessageTemplates.document_ready_title(),
|
||||
"document_id": document.id,
|
||||
"uploaded_by_id": (
|
||||
document.uploaded_by.id
|
||||
if document.uploaded_by
|
||||
else None
|
||||
),
|
||||
"sent_to_all": document.sent_to_all,
|
||||
},
|
||||
data=payload["data"],
|
||||
)
|
||||
created_count += 1
|
||||
except Exception as e:
|
||||
|
||||
@ -127,6 +127,12 @@ def check_all_acknowledged(sender, instance, created, **kwargs):
|
||||
|
||||
acknowledgement = instance
|
||||
document = acknowledgement.document
|
||||
if not document.acknowledgement_required:
|
||||
logger.info(
|
||||
"[signals] Skipping full acknowledgement notification "
|
||||
f"for optional document id={document.pk}"
|
||||
)
|
||||
return
|
||||
|
||||
# Определяем общее количество получателей
|
||||
if document.sent_to_all:
|
||||
|
||||
@ -8,9 +8,10 @@ from __future__ import annotations
|
||||
- Создавать — все аутентифицированные пользователи (документы создаются в draft).
|
||||
- Редактировать/удалять — только создатель, админы или обладатели модельных прав.
|
||||
- Approve/Publish — только пользователи с правом change_document (django-rules).
|
||||
- Читать/скачивать/ознакомливаться — только аутентифицированные, и только документы,
|
||||
предназначенные им (или с sent_to_all=True). Обычные пользователи видят только PUBLISHED
|
||||
документы (кроме своих собственных).
|
||||
- Читать/скачивать — только аутентифицированные, и только документы,
|
||||
предназначенные им (или с sent_to_all=True). Ознакомливаться можно только
|
||||
с документами, где acknowledgement_required=True. Обычные пользователи
|
||||
видят только PUBLISHED документы (кроме своих собственных).
|
||||
|
||||
Все функции и классы снабжены докстрингами (Google-style).
|
||||
"""
|
||||
@ -101,6 +102,7 @@ def make_document(
|
||||
uploaded_by: User,
|
||||
description: str = "desc",
|
||||
sent_to_all: bool = True,
|
||||
acknowledgement_required: bool = False,
|
||||
recipients: Iterable[User] | None = None,
|
||||
) -> Document:
|
||||
"""Создаёт Document напрямую (минует API).
|
||||
@ -110,6 +112,7 @@ def make_document(
|
||||
uploaded_by (User): Кто загрузил.
|
||||
description (str): Описание.
|
||||
sent_to_all (bool): Признак рассылки всем.
|
||||
acknowledgement_required (bool): Требуется ли ознакомление.
|
||||
recipients (Iterable[User] | None): Конкретные получатели при sent_to_all=false.
|
||||
|
||||
Returns:
|
||||
@ -124,6 +127,7 @@ def make_document(
|
||||
uploaded_by=uploaded_by,
|
||||
uploaded_at=timezone.now(),
|
||||
sent_to_all=sent_to_all,
|
||||
acknowledgement_required=acknowledgement_required,
|
||||
file=filer_file,
|
||||
)
|
||||
if not sent_to_all and recipients:
|
||||
@ -892,6 +896,107 @@ class TestFolders:
|
||||
assert archive.read("Root/Child/child.txt") == b"child payload"
|
||||
assert "Other/other.txt" not in archive.namelist()
|
||||
|
||||
|
||||
# -------------------- E3. Уведомления --------------------
|
||||
|
||||
class TestDocumentNotifications:
|
||||
"""E3. Уведомления о новых документах и ознакомлении."""
|
||||
|
||||
def test_required_document_notification_mentions_acknowledgement(
|
||||
self, monkeypatch, make_user
|
||||
):
|
||||
"""Документ на ознакомление получает требовательный текст."""
|
||||
from documents.notifications.handlers import notify_document_ready
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
calls.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"documents.notifications.handlers.notify.send",
|
||||
fake_notify_send,
|
||||
)
|
||||
author = make_user("required-document-author@example.com")
|
||||
viewer = make_user("required-document-viewer@example.com")
|
||||
doc = make_document(
|
||||
title="Регламент",
|
||||
uploaded_by=author,
|
||||
sent_to_all=False,
|
||||
acknowledgement_required=True,
|
||||
)
|
||||
|
||||
notify_document_ready(doc, viewer)
|
||||
|
||||
assert len(calls) == 1
|
||||
payload = calls[0]
|
||||
assert payload["recipient"] == viewer
|
||||
assert payload["verb"] == "document_ready"
|
||||
assert "Требуется ознакомление" in payload["description"]
|
||||
assert payload["data"]["title"] == "Новый документ на ознакомление"
|
||||
assert payload["data"]["acknowledgement_required"] is True
|
||||
|
||||
def test_optional_document_notification_is_neutral(
|
||||
self, monkeypatch, make_user
|
||||
):
|
||||
"""Документ без ознакомления уведомляет без требования ознакомиться."""
|
||||
from documents.notifications.handlers import notify_document_ready
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
calls.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"documents.notifications.handlers.notify.send",
|
||||
fake_notify_send,
|
||||
)
|
||||
author = make_user("optional-document-author@example.com")
|
||||
viewer = make_user("optional-document-viewer@example.com")
|
||||
doc = make_document(
|
||||
title="Справка",
|
||||
uploaded_by=author,
|
||||
sent_to_all=False,
|
||||
acknowledgement_required=False,
|
||||
)
|
||||
|
||||
notify_document_ready(doc, viewer)
|
||||
|
||||
assert len(calls) == 1
|
||||
payload = calls[0]
|
||||
assert payload["recipient"] == viewer
|
||||
assert payload["verb"] == "document_ready"
|
||||
assert "Требуется ознакомление" not in payload["description"]
|
||||
assert payload["description"].endswith('загрузил документ "Справка".')
|
||||
assert payload["data"]["title"] == "Новый документ"
|
||||
assert payload["data"]["acknowledgement_required"] is False
|
||||
|
||||
def test_optional_document_acknowledgement_does_not_notify_uploader(
|
||||
self, monkeypatch, make_user
|
||||
):
|
||||
"""Ручные ACK по необязательному документу не запускают итоговое уведомление."""
|
||||
calls = []
|
||||
|
||||
def fake_notify_all_acknowledged(*args):
|
||||
calls.append(args)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"documents.notifications.signals.notify_all_acknowledged",
|
||||
fake_notify_all_acknowledged,
|
||||
)
|
||||
author = make_user("optional-ack-author@example.com")
|
||||
viewer = make_user("optional-ack-viewer@example.com")
|
||||
doc = make_document(
|
||||
uploaded_by=author,
|
||||
sent_to_all=False,
|
||||
acknowledgement_required=False,
|
||||
)
|
||||
|
||||
DocumentAcknowledgement.objects.create(document=doc, user=viewer)
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
# -------------------- F. Экшен acknowledge --------------------
|
||||
|
||||
class TestAcknowledge:
|
||||
@ -906,7 +1011,9 @@ class TestAcknowledge:
|
||||
client = auth_client(any_user)
|
||||
|
||||
d = make_document(
|
||||
uploaded_by=author, sent_to_all=True
|
||||
uploaded_by=author,
|
||||
sent_to_all=True,
|
||||
acknowledgement_required=True,
|
||||
) # доступен всем аутентифицированным
|
||||
|
||||
r1 = client.post(api_urls["ack"](d.pk), {})
|
||||
@ -954,6 +1061,28 @@ class TestAcknowledge:
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
def test_ack_rejects_document_without_required_acknowledgement(
|
||||
self, auth_client, make_user, api_urls
|
||||
):
|
||||
"""Документ без обязательного ознакомления нельзя отметить через API."""
|
||||
author = make_user("optional-author@example.com")
|
||||
viewer = make_user("optional-viewer@example.com")
|
||||
client = auth_client(viewer)
|
||||
doc = make_document(
|
||||
uploaded_by=author,
|
||||
sent_to_all=True,
|
||||
acknowledgement_required=False,
|
||||
)
|
||||
|
||||
response = client.post(api_urls["ack"](doc.pk), {})
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "не требуется" in response.json()["detail"]
|
||||
assert not DocumentAcknowledgement.objects.filter(
|
||||
document=doc,
|
||||
user=viewer,
|
||||
).exists()
|
||||
|
||||
# -------------------- G. Сериализация и данные --------------------
|
||||
|
||||
class TestSerialization:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user