forked from github/plane
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:
parent
515dba02d3
commit
894ffb6c21
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user