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.
@ -99,6 +122,65 @@ def extract_mentions(issue_instance):
return list(set(mentions)) return list(set(mentions))
except Exception as e: except Exception as e:
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
@ -126,61 +208,97 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
bulk_notifications = [] bulk_notifications = []
""" """
Mention Tasks Mention Tasks
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent 1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers 2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
""" """
# Get new mentions from the newer instance # Get new mentions from the newer instance
new_mentions = get_new_mentions( new_mentions = get_new_mentions(
requested_instance=requested_data, current_instance=current_instance) requested_instance=requested_data, current_instance=current_instance)
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:
_ = IssueSubscriber.objects.get_or_create( if str(issue.created_by_id) != str(actor_id) and uuid.UUID(actor_id) not in issue_assignees:
issue_id=issue_id, subscriber_id=actor_id _ = IssueSubscriber.objects.get_or_create(
) 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,
@ -237,38 +382,37 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
"identifier": str(issue.project.identifier), "identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id, "sequence_id": issue.sequence_id,
"state_name": issue.state.name, "state_name": issue.state.name,
"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:
for issue_activity in issue_activities_created:
notification = createMentionNotification(
project=project,
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)
# Create New Mentions Here
aggregated_issue_mentions = []
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,
workspace=project.workspace
)
)
IssueMention.objects.bulk_create(
aggregated_issue_mentions, batch_size=100)
IssueMention.objects.filter(
issue=issue, mention__in=removed_mention).delete()
# save new mentions for the particular issue and remove the mentions that has been deleted from the description
update_mentions_for_issue(issue=issue, project=project, new_mentions=new_mentions,
removed_mention=removed_mention)
# Bulk create notifications # Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100) Notification.objects.bulk_create(bulk_notifications, batch_size=100)