mirror of
https://github.com/suitenumerique/docs.git
synced 2026-10-01 05:55:16 +02:00
⚡️(backend) send mention notification email asynchronously
Move the mention email off the request/response cycle into a Celery task so the SMTP round-trip no longer delays the API response. Signed-off-by: BOUKERFA Mohamed El Amine <boukerfa.ma@gmail.com>
This commit is contained in:
@@ -71,7 +71,7 @@ from core.services.search_indexers import (
|
||||
from core.services.yhub_services import YHubError, YHubService
|
||||
from core.tasks.access import reset_service_connections_in_cascade
|
||||
from core.tasks.documents import sync_service_deletions_in_cascade
|
||||
from core.tasks.mail import send_ask_for_access_mail
|
||||
from core.tasks.mail import send_ask_for_access_mail, send_mention_notification_mail
|
||||
from core.tasks.search import trigger_batch_document_indexer
|
||||
from core.utils.analytics import PosthogEventName, posthog_capture
|
||||
from core.utils.dicts import lowercase_keys
|
||||
@@ -1881,9 +1881,10 @@ class DocumentViewSet(
|
||||
def mention(self, request, *args, **kwargs):
|
||||
"""Mention a user on the document and notify them by email.
|
||||
|
||||
The mention record is always created; the email notification is
|
||||
suppressed when the same user was already notified in the same context
|
||||
(document body or thread) within the cooldown period.
|
||||
The mention record is created synchronously; the email notification is
|
||||
sent asynchronously by a Celery task, which suppresses it when the same
|
||||
user was already notified in the same context (document body or thread)
|
||||
within the cooldown period.
|
||||
"""
|
||||
# Check permissions first
|
||||
document = self.get_object()
|
||||
@@ -1895,19 +1896,7 @@ class DocumentViewSet(
|
||||
serializer.is_valid(raise_exception=True)
|
||||
mention = serializer.save(document=document, mentioned_by_user=request.user)
|
||||
|
||||
mention.notify()
|
||||
|
||||
posthog_capture(
|
||||
PosthogEventName.MENTION_CREATED,
|
||||
request.user,
|
||||
{
|
||||
"mention_id": str(mention.id),
|
||||
"mentioned_user_id": str(mention.mentioned_user_id),
|
||||
"thread_id": str(mention.thread_id) if mention.thread_id else None,
|
||||
"notified": mention.notified_at is not None,
|
||||
},
|
||||
document=document,
|
||||
)
|
||||
send_mention_notification_mail.delay(str(mention.id))
|
||||
|
||||
return drf.response.Response(
|
||||
serializer.data, status=drf.status.HTTP_201_CREATED
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from django.conf import settings
|
||||
|
||||
from core import models
|
||||
from core.utils.analytics import PosthogEventName, posthog_capture
|
||||
|
||||
from impress.celery_app import app
|
||||
|
||||
@@ -22,3 +23,25 @@ def send_ask_for_access_mail(ask_for_access_id):
|
||||
access.user.email,
|
||||
access.user.language or settings.LANGUAGE_CODE,
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def send_mention_notification_mail(mention_id):
|
||||
"""Notify a mentioned user by email, outside the request/response cycle."""
|
||||
mention = models.Mention.objects.select_related(
|
||||
"document", "mentioned_user", "mentioned_by_user"
|
||||
).get(id=mention_id)
|
||||
|
||||
notified = mention.notify()
|
||||
|
||||
posthog_capture(
|
||||
PosthogEventName.MENTION_CREATED,
|
||||
mention.mentioned_by_user,
|
||||
{
|
||||
"mention_id": str(mention.id),
|
||||
"mentioned_user_id": str(mention.mentioned_user_id),
|
||||
"thread_id": str(mention.thread_id) if mention.thread_id else None,
|
||||
"notified": notified,
|
||||
},
|
||||
document=mention.document,
|
||||
)
|
||||
|
||||
@@ -128,7 +128,9 @@ def test_api_documents_mention_authenticated_success(role):
|
||||
"mentioned_user_id": str(mentioned_user.id),
|
||||
"mentioned_by_user_id": str(user.id),
|
||||
"created_at": mention.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"notified_at": mention.notified_at.isoformat().replace("+00:00", "Z"),
|
||||
# The email is sent asynchronously, so the create response carries no
|
||||
# notification timestamp even though the stored mention is notified.
|
||||
"notified_at": None,
|
||||
}
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
@@ -352,7 +354,7 @@ def test_api_documents_mention_cooldown_same_context():
|
||||
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mention/", payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["notified_at"] is not None
|
||||
assert models.Mention.objects.get(anchor_id="block-1").notified_at is not None
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
response = client.post(
|
||||
@@ -360,7 +362,7 @@ def test_api_documents_mention_cooldown_same_context():
|
||||
{"anchor_id": "block-2", "mentioned_user_id": str(mentioned_user.id)},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["notified_at"] is None
|
||||
assert models.Mention.objects.get(anchor_id="block-2").notified_at is None
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
assert models.Mention.objects.count() == 2
|
||||
@@ -390,7 +392,9 @@ def test_api_documents_mention_cooldown_distinct_contexts():
|
||||
payload["thread_id"] = thread_id
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mention/", payload)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["notified_at"] is not None
|
||||
assert (
|
||||
models.Mention.objects.get(anchor_id=f"block-{i}").notified_at is not None
|
||||
)
|
||||
|
||||
assert len(mail.outbox) == 3
|
||||
|
||||
@@ -404,7 +408,7 @@ def test_api_documents_mention_cooldown_distinct_contexts():
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["notified_at"] is None
|
||||
assert models.Mention.objects.get(anchor_id="block-4").notified_at is None
|
||||
assert len(mail.outbox) == 3
|
||||
|
||||
# The cooldown should apply per mentioned user
|
||||
@@ -419,7 +423,7 @@ def test_api_documents_mention_cooldown_distinct_contexts():
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["notified_at"] is not None
|
||||
assert models.Mention.objects.get(anchor_id="block-5").notified_at is not None
|
||||
assert len(mail.outbox) == 4
|
||||
|
||||
|
||||
@@ -453,7 +457,7 @@ def test_api_documents_mention_cooldown_expired(settings):
|
||||
{"anchor_id": "block-2", "mentioned_user_id": str(mentioned_user.id)},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["notified_at"] is not None
|
||||
assert models.Mention.objects.get(anchor_id="block-2").notified_at is not None
|
||||
assert len(mail.outbox) == 2
|
||||
|
||||
|
||||
@@ -467,7 +471,7 @@ def test_api_documents_mention_cooldown_only_considers_notified_mentions():
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="commenter")
|
||||
mentioned_user = factories.UserFactory()
|
||||
factories.UserDocumentAccessFactory(document=document, user=mentioned_user)
|
||||
factories.MentionFactory(
|
||||
existing_mention = factories.MentionFactory(
|
||||
document=document,
|
||||
mentioned_user=mentioned_user,
|
||||
mentioned_by_user=user,
|
||||
@@ -482,7 +486,8 @@ def test_api_documents_mention_cooldown_only_considers_notified_mentions():
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["notified_at"] is not None
|
||||
new_mention = models.Mention.objects.exclude(pk=existing_mention.pk).get()
|
||||
assert new_mention.notified_at is not None
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user