fix: mention notification (#2670)

* fix: mention notification

* feat: updated mentions for comments in the notification background task

* feat: added subscription for issue_comment_mentions as well

* fix: removed the print statement

* fix: double notification popup for mentioned assignees

* fix: added issue subscriber

* fix: removed creator for subscribed

* fix: creator will not be subscribed to issue

* fix: double notification removed

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
This commit is contained in:
Bavisetti Narayan 2023-11-09 18:22:13 +05:30 committed by GitHub
parent 515dba02d3
commit 894ffb6c21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,8 +1,6 @@
# Python imports # Python imports
import json import json
import uuid
# Django imports
from django.utils import timezone
# Module imports # Module imports
from plane.db.models import ( from plane.db.models import (
@ -14,6 +12,7 @@ from plane.db.models import (
Issue, Issue,
Notification, Notification,
IssueComment, IssueComment,
IssueActivity
) )
# Third Party imports # Third Party imports
@ -21,12 +20,35 @@ from celery import shared_task
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
# =========== Issue Description Html Parsing and Notification Functions ======================
def update_mentions_for_issue(issue, project, new_mentions, removed_mention):
aggregated_issue_mentions = []
for mention_id in new_mentions:
aggregated_issue_mentions.append(
IssueMention(
mention_id=mention_id,
issue=issue,
project=project,
workspace_id=project.workspace_id
)
)
IssueMention.objects.bulk_create(
aggregated_issue_mentions, batch_size=100)
IssueMention.objects.filter(
issue=issue, mention__in=removed_mention).delete()
def get_new_mentions(requested_instance, current_instance): def get_new_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue # requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database # current_instance is the older instance of the current issue, saved in the database
# extract mentions from both the instance of data # extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance) mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance) mentions_newer = extract_mentions(requested_instance)
# Getting Set Difference from mentions_newer # Getting Set Difference from mentions_newer
@ -64,25 +86,26 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions):
# If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification # If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification
if not IssueSubscriber.objects.filter( if not IssueSubscriber.objects.filter(
issue_id=issue_id, issue_id=issue_id,
subscriber=mention_id, subscriber_id=mention_id,
project=project_id, project_id=project_id,
).exists() and not IssueAssignee.objects.filter(
project_id=project_id, issue_id=issue_id,
assignee_id=mention_id
).exists() and not Issue.objects.filter(
project_id=project_id, pk=issue_id, created_by_id=mention_id
).exists(): ).exists():
mentioned_user = User.objects.get(pk=mention_id)
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
issue = Issue.objects.get(pk=issue_id)
bulk_mention_subscribers.append(IssueSubscriber( bulk_mention_subscribers.append(IssueSubscriber(
workspace=project.workspace, workspace_id=project.workspace_id,
project=project, project_id=project_id,
issue=issue, issue_id=issue_id,
subscriber=mentioned_user, subscriber_id=mention_id,
)) ))
return bulk_mention_subscribers return bulk_mention_subscribers
# Parse Issue Description & extracts mentions # Parse Issue Description & extracts mentions
def extract_mentions(issue_instance): def extract_mentions(issue_instance):
try: try:
# issue_instance has to be a dictionary passed, containing the description_html and other set of activity data. # issue_instance has to be a dictionary passed, containing the description_html and other set of activity data.
@ -101,6 +124,65 @@ def extract_mentions(issue_instance):
return [] return []
# =========== Comment Parsing and Notification Functions ======================
def extract_comment_mentions(comment_value):
try:
mentions = []
soup = BeautifulSoup(comment_value, 'html.parser')
mentions_tags = soup.find_all(
'mention-component', attrs={'target': 'users'}
)
for mention_tag in mentions_tags:
mentions.append(mention_tag['id'])
return list(set(mentions))
except Exception as e:
return []
def get_new_comment_mentions(new_value, old_value):
mentions_newer = extract_comment_mentions(new_value)
if old_value is None:
return mentions_newer
mentions_older = extract_comment_mentions(old_value)
# Getting Set Difference from mentions_newer
new_mentions = [
mention for mention in mentions_newer if mention not in mentions_older]
return new_mentions
def createMentionNotification(project, notification_comment, issue, actor_id, mention_id, issue_id, activity):
return Notification(
workspace=project.workspace,
sender="in_app:issue_activities:mentioned",
triggered_by_id=actor_id,
receiver_id=mention_id,
entity_identifier=issue_id,
entity_name="issue",
project=project,
message=notification_comment,
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(activity.get("id")),
"verb": str(activity.get("verb")),
"field": str(activity.get("field")),
"actor": str(activity.get("actor_id")),
"new_value": str(activity.get("new_value")),
"old_value": str(activity.get("old_value")),
}
},
)
@shared_task @shared_task
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance): def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
issue_activities_created = ( issue_activities_created = (
@ -137,50 +219,86 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
removed_mention = get_removed_mentions( removed_mention = get_removed_mentions(
requested_instance=requested_data, current_instance=current_instance) requested_instance=requested_data, current_instance=current_instance)
comment_mentions = []
all_comment_mentions = []
# Get New Subscribers from the mentions of the newer instance # Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions( requested_mentions = extract_mentions(
issue_instance=requested_data) issue_instance=requested_data)
mention_subscribers = extract_mentions_as_subscribers( mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id, issue_id=issue_id, mentions=requested_mentions) project_id=project_id, issue_id=issue_id, mentions=requested_mentions)
issue_subscribers = list( for issue_activity in issue_activities_created:
IssueSubscriber.objects.filter( issue_comment = issue_activity.get("issue_comment")
project_id=project_id, issue_id=issue_id) issue_comment_new_value = issue_activity.get("new_value")
.exclude(subscriber_id__in=list(new_mentions + [actor_id])) issue_comment_old_value = issue_activity.get("old_value")
.values_list("subscriber", flat=True) if issue_comment is not None:
) # TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well.
all_comment_mentions = all_comment_mentions + extract_comment_mentions(issue_comment_new_value)
new_comment_mentions = get_new_comment_mentions(old_value=issue_comment_old_value, new_value=issue_comment_new_value)
comment_mentions = comment_mentions + new_comment_mentions
comment_mention_subscribers = extract_mentions_as_subscribers( project_id=project_id, issue_id=issue_id, mentions=all_comment_mentions)
"""
We will not send subscription activity notification to the below mentioned user sets
- Those who have been newly mentioned in the issue description, we will send mention notification to them.
- When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification
- When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification
"""
issue_assignees = list( issue_assignees = list(
IssueAssignee.objects.filter( IssueAssignee.objects.filter(
project_id=project_id, issue_id=issue_id) project_id=project_id, issue_id=issue_id)
.exclude(assignee_id=actor_id) .exclude(assignee_id__in=list(new_mentions + comment_mentions))
.values_list("assignee", flat=True) .values_list("assignee", flat=True)
) )
issue_subscribers = issue_subscribers + issue_assignees issue_subscribers = list(
IssueSubscriber.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(subscriber_id__in=list(new_mentions + comment_mentions + [actor_id]))
.values_list("subscriber", flat=True)
)
issue = Issue.objects.filter(pk=issue_id).first() issue = Issue.objects.filter(pk=issue_id).first()
if (issue.created_by_id is not None and str(issue.created_by_id) != str(actor_id)):
issue_subscribers = issue_subscribers + [issue.created_by_id]
if subscriber: if subscriber:
# add the user to issue subscriber # add the user to issue subscriber
try: try:
if str(issue.created_by_id) != str(actor_id) and uuid.UUID(actor_id) not in issue_assignees:
_ = IssueSubscriber.objects.get_or_create( _ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber_id=actor_id project_id=project_id, issue_id=issue_id, subscriber_id=actor_id
) )
except Exception as e: except Exception as e:
pass pass
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
for subscriber in list(set(issue_subscribers)): issue_subscribers = list(set(issue_subscribers + issue_assignees) - {uuid.UUID(actor_id)})
for subscriber in issue_subscribers:
if subscriber in issue_subscribers:
sender = "in_app:issue_activities:subscribed"
if issue.created_by_id is not None and subscriber == issue.created_by_id:
sender = "in_app:issue_activities:created"
if subscriber in issue_assignees:
sender = "in_app:issue_activities:assigned"
for issue_activity in issue_activities_created: for issue_activity in issue_activities_created:
issue_comment = issue_activity.get("issue_comment") issue_comment = issue_activity.get("issue_comment")
if issue_comment is not None: if issue_comment is not None:
issue_comment = IssueComment.objects.get(id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id) issue_comment = IssueComment.objects.get(
id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id)
bulk_notifications.append( bulk_notifications.append(
Notification( Notification(
workspace=project.workspace, workspace=project.workspace,
sender="in_app:issue_activities", sender=sender,
triggered_by_id=actor_id, triggered_by_id=actor_id,
receiver_id=subscriber, receiver_id=subscriber,
entity_identifier=issue_id, entity_identifier=issue_id,
@ -215,15 +333,42 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
# Add Mentioned as Issue Subscribers # Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create( IssueSubscriber.objects.bulk_create(
mention_subscribers, batch_size=100) mention_subscribers + comment_mention_subscribers, batch_size=100)
last_activity = (
IssueActivity.objects.filter(issue_id=issue_id)
.order_by("-created_at")
.first()
)
actor = User.objects.get(pk=actor_id)
for mention_id in comment_mentions:
if (mention_id != actor_id):
for issue_activity in issue_activities_created:
notification = createMentionNotification(
project=project,
issue=issue,
notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}",
actor_id=actor_id,
mention_id=mention_id,
issue_id=issue_id,
activity=issue_activity
)
bulk_notifications.append(notification)
for mention_id in new_mentions: for mention_id in new_mentions:
if (mention_id != actor_id): if (mention_id != actor_id):
for issue_activity in issue_activities_created: if (
last_activity is not None
and last_activity.field == "description"
and actor_id == str(last_activity.actor_id)
):
bulk_notifications.append( bulk_notifications.append(
Notification( Notification(
workspace=project.workspace, workspace=project.workspace,
sender="in_app:issue_activities:mention", sender="in_app:issue_activities:mentioned",
triggered_by_id=actor_id, triggered_by_id=actor_id,
receiver_id=mention_id, receiver_id=mention_id,
entity_identifier=issue_id, entity_identifier=issue_id,
@ -240,35 +385,34 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
"state_group": issue.state.group, "state_group": issue.state.group,
}, },
"issue_activity": { "issue_activity": {
"id": str(issue_activity.get("id")), "id": str(last_activity.id),
"verb": str(issue_activity.get("verb")), "verb": str(last_activity.verb),
"field": str(issue_activity.get("field")), "field": str(last_activity.field),
"actor": str(issue_activity.get("actor_id")), "actor": str(last_activity.actor_id),
"new_value": str(issue_activity.get("new_value")), "new_value": str(last_activity.new_value),
"old_value": str(issue_activity.get("old_value")), "old_value": str(last_activity.old_value),
}, },
}, },
) )
) )
else:
# Create New Mentions Here for issue_activity in issue_activities_created:
aggregated_issue_mentions = [] notification = createMentionNotification(
for mention_id in new_mentions:
mentioned_user = User.objects.get(pk=mention_id)
aggregated_issue_mentions.append(
IssueMention(
mention=mentioned_user,
issue=issue,
project=project, project=project,
workspace=project.workspace issue=issue,
) notification_comment=f"You have been mentioned in the issue {issue.name}",
actor_id=actor_id,
mention_id=mention_id,
issue_id=issue_id,
activity=issue_activity
) )
bulk_notifications.append(notification)
IssueMention.objects.bulk_create( # save new mentions for the particular issue and remove the mentions that has been deleted from the description
aggregated_issue_mentions, batch_size=100) update_mentions_for_issue(issue=issue, project=project, new_mentions=new_mentions,
IssueMention.objects.filter( removed_mention=removed_mention)
issue=issue, mention__in=removed_mention).delete()
# Bulk create notifications # Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100) Notification.objects.bulk_create(bulk_notifications, batch_size=100)