From 0230563ad0ea8b9ef817ce22ae2f66b3b453a5fe Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Wed, 10 Jan 2024 12:14:08 +0530
Subject: [PATCH 01/24] dev: create email notification preference model
---
apiserver/plane/db/models/__init__.py | 2 +-
apiserver/plane/db/models/notification.py | 43 ++++++++++++++++++++++-
2 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 3a07a33f3d..b2d3393244 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -85,7 +85,7 @@
from .analytic import AnalyticView
-from .notification import Notification
+from .notification import Notification, NotificationPreference
from .exporter import ExporterHistory
diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py
index 8e6a48e146..389069077c 100644
--- a/apiserver/plane/db/models/notification.py
+++ b/apiserver/plane/db/models/notification.py
@@ -1,9 +1,10 @@
# Django imports
from django.db import models
+from django.conf import settings
# Third party imports
from .base import BaseModel
-
+from .project import ProjectBaseModel
class Notification(BaseModel):
workspace = models.ForeignKey(
@@ -47,3 +48,43 @@ class Meta:
def __str__(self):
"""Return name of the notifications"""
return f"{self.receiver.email} <{self.workspace.name}>"
+
+
+def get_default_preference():
+ return {
+ "property_change": {
+ "email": True,
+ },
+ "state": {
+ "email": True,
+ },
+ "comment": {
+ "email": True,
+ },
+ "mentions": {
+ "email": True,
+ },
+ }
+
+
+class NotificationPreference(ProjectBaseModel):
+ created_by = models.JSONField(default=get_default_preference)
+ assigned = models.JSONField(default=get_default_preference)
+ subscribed = models.JSONField(default=get_default_preference)
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ related_name="notification_preferences",
+ )
+
+ class Meta:
+ unique_together = ["project", "user"]
+
+ verbose_name = "Notification Preference"
+ verbose_name_plural = "Notification Preferences"
+ db_table = "notification_preferences"
+ ordering = ("-created_at",)
+
+ def __str__(self):
+ """Return name of the notifications"""
+ return f"{self.user.email} <{self.workspace.name}>"
From 88f5d2bcced32a69411478d487fe6c33c8b9daa4 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Wed, 10 Jan 2024 18:07:35 +0530
Subject: [PATCH 02/24] dev: intiate models
---
apiserver/plane/bgtasks/notification_task.py | 97 +++++++++----------
...lnotificationlog_notificationpreference.py | 62 ++++++++++++
apiserver/plane/db/models/__init__.py | 2 +-
apiserver/plane/db/models/notification.py | 21 +++-
4 files changed, 128 insertions(+), 54 deletions(-)
create mode 100644 apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py
diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py
index 5649ad6b7d..5784bea6f0 100644
--- a/apiserver/plane/bgtasks/notification_task.py
+++ b/apiserver/plane/bgtasks/notification_task.py
@@ -13,6 +13,8 @@
Notification,
IssueComment,
IssueActivity,
+ NotificationPreference,
+ EmailNotificationLog,
)
# Third Party imports
@@ -37,9 +39,7 @@ def update_mentions_for_issue(issue, project, new_mentions, removed_mention):
)
IssueMention.objects.bulk_create(aggregated_issue_mentions, batch_size=100)
- IssueMention.objects.filter(
- issue=issue, mention__in=removed_mention
- ).delete()
+ IssueMention.objects.filter(issue=issue, mention__in=removed_mention).delete()
def get_new_mentions(requested_instance, current_instance):
@@ -95,9 +95,7 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions):
project_id=project_id,
).exists()
and not IssueAssignee.objects.filter(
- project_id=project_id,
- issue_id=issue_id,
- assignee_id=mention_id,
+ 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
@@ -125,9 +123,7 @@ def extract_mentions(issue_instance):
data = json.loads(issue_instance)
html = data.get("description_html")
soup = BeautifulSoup(html, "html.parser")
- mention_tags = soup.find_all(
- "mention-component", attrs={"target": "users"}
- )
+ mention_tags = soup.find_all("mention-component", attrs={"target": "users"})
mentions = [mention_tag["id"] for mention_tag in mention_tags]
@@ -141,9 +137,7 @@ def extract_comment_mentions(comment_value):
try:
mentions = []
soup = BeautifulSoup(comment_value, "html.parser")
- mentions_tags = soup.find_all(
- "mention-component", attrs={"target": "users"}
- )
+ 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))
@@ -165,14 +159,8 @@ def get_new_comment_mentions(new_value, old_value):
return new_mentions
-def createMentionNotification(
- project,
- notification_comment,
- issue,
- actor_id,
- mention_id,
- issue_id,
- activity,
+def create_mention_notification(
+ project, notification_comment, issue, actor_id, mention_id, issue_id, activity
):
return Notification(
workspace=project.workspace,
@@ -238,6 +226,7 @@ def notifications(
]:
# Create Notifications
bulk_notifications = []
+ bulk_email_logs = []
"""
Mention Tasks
@@ -247,12 +236,10 @@ def notifications(
# Get new mentions from the newer instance
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(
- requested_instance=requested_data,
- current_instance=current_instance,
+ requested_instance=requested_data, current_instance=current_instance
)
comment_mentions = []
@@ -261,9 +248,7 @@ def notifications(
# Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions(issue_instance=requested_data)
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
)
for issue_activity in issue_activities_created:
@@ -273,21 +258,17 @@ def notifications(
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)
+ 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,
+ 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,
+ 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
@@ -297,21 +278,15 @@ def notifications(
"""
issue_assignees = list(
- IssueAssignee.objects.filter(
- project_id=project_id, issue_id=issue_id
- )
+ IssueAssignee.objects.filter(project_id=project_id, issue_id=issue_id)
.exclude(assignee_id__in=list(new_mentions + comment_mentions))
.values_list("assignee", flat=True)
)
issue_subscribers = list(
- IssueSubscriber.objects.filter(
- project_id=project_id, issue_id=issue_id
- )
+ IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
.exclude(
- subscriber_id__in=list(
- new_mentions + comment_mentions + [actor_id]
- )
+ subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])
)
.values_list("subscriber", flat=True)
)
@@ -346,13 +321,13 @@ def notifications(
for subscriber in issue_subscribers:
if subscriber in issue_subscribers:
+ trigger = "subscribed"
sender = "in_app:issue_activities:subscribed"
- if (
- issue.created_by_id is not None
- and subscriber == issue.created_by_id
- ):
+ if issue.created_by_id is not None and subscriber == issue.created_by_id:
+ trigger = "created"
sender = "in_app:issue_activities:created"
if subscriber in issue_assignees:
+ trigger = "assigned"
sender = "in_app:issue_activities:assigned"
for issue_activity in issue_activities_created:
@@ -368,6 +343,19 @@ def notifications(
workspace_id=project.workspace_id,
)
+ preference = NotificationPreference.objects.filter(
+ user=subscriber,
+ workspace_id=project.workspace_id,
+ project_id=project_id,
+ ).values("created", "assigned", "subscribed").first()
+
+ if issue_activity.get("field") == "state":
+ preference = preference.get(trigger).get("state")
+ elif issue_activity.get("field") == "comment":
+ preference = preference.get(trigger).get("comment")
+ else:
+ preference = preference.get(trigger).get("property_change")
+
bulk_notifications.append(
Notification(
workspace=project.workspace,
@@ -409,6 +397,15 @@ def notifications(
)
)
+ if preference:
+ bulk_email_logs(
+ EmailNotificationLog(
+ user_id=subscriber,
+
+ )
+ )
+
+
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
mention_subscribers + comment_mention_subscribers, batch_size=100
@@ -425,7 +422,7 @@ def notifications(
for mention_id in comment_mentions:
if mention_id != actor_id:
for issue_activity in issue_activities_created:
- notification = createMentionNotification(
+ notification = create_mention_notification(
project=project,
issue=issue,
notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}",
@@ -477,7 +474,7 @@ def notifications(
)
else:
for issue_activity in issue_activities_created:
- notification = createMentionNotification(
+ notification = create_mention_notification(
project=project,
issue=issue,
notification_comment=f"You have been mentioned in the issue {issue.name}",
diff --git a/apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py b/apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py
new file mode 100644
index 0000000000..97a507a6e5
--- /dev/null
+++ b/apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py
@@ -0,0 +1,62 @@
+# Generated by Django 4.2.7 on 2024-01-10 08:42
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import plane.db.models.notification
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('db', '0053_auto_20240102_1315'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='EmailNotificationLog',
+ fields=[
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
+ ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
+ ('sent_at', models.DateTimeField(null=True)),
+ ('entity', models.CharField(max_length=200)),
+ ('old_value', models.CharField(blank=True, max_length=300, null=True)),
+ ('new_value', models.CharField(blank=True, max_length=300, null=True)),
+ ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
+ ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
+ ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_logs', to=settings.AUTH_USER_MODEL)),
+ ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
+ ],
+ options={
+ 'verbose_name': 'Email Notification Log',
+ 'verbose_name_plural': 'Email Notification Logs',
+ 'db_table': 'email_notification_logs',
+ 'ordering': ('-created_at',),
+ },
+ ),
+ migrations.CreateModel(
+ name='NotificationPreference',
+ fields=[
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
+ ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
+ ('created_by', models.JSONField(default=plane.db.models.notification.get_default_preference)),
+ ('assigned', models.JSONField(default=plane.db.models.notification.get_default_preference)),
+ ('subscribed', models.JSONField(default=plane.db.models.notification.get_default_preference)),
+ ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
+ ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_preferences', to=settings.AUTH_USER_MODEL)),
+ ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
+ ],
+ options={
+ 'verbose_name': 'Notification Preference',
+ 'verbose_name_plural': 'Notification Preferences',
+ 'db_table': 'notification_preferences',
+ 'ordering': ('-created_at',),
+ 'unique_together': {('project', 'user')},
+ },
+ ),
+ ]
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index b2d3393244..09430591f4 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -85,7 +85,7 @@
from .analytic import AnalyticView
-from .notification import Notification, NotificationPreference
+from .notification import Notification, NotificationPreference, EmailNotificationLog
from .exporter import ExporterHistory
diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py
index 389069077c..8ae2a1ef3f 100644
--- a/apiserver/plane/db/models/notification.py
+++ b/apiserver/plane/db/models/notification.py
@@ -67,8 +67,8 @@ def get_default_preference():
}
-class NotificationPreference(ProjectBaseModel):
- created_by = models.JSONField(default=get_default_preference)
+class UserNotificationPreference(BaseModel):
+ created = models.JSONField(default=get_default_preference)
assigned = models.JSONField(default=get_default_preference)
subscribed = models.JSONField(default=get_default_preference)
user = models.ForeignKey(
@@ -82,9 +82,24 @@ class Meta:
verbose_name = "Notification Preference"
verbose_name_plural = "Notification Preferences"
- db_table = "notification_preferences"
+ db_table = "user_notification_preferences"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the notifications"""
return f"{self.user.email} <{self.workspace.name}>"
+
+
+class EmailNotificationLog(ProjectBaseModel):
+ receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notifications")
+ triggered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="triggered_by_notifications")
+ sent_at = models.DateTimeField(null=True)
+ entity = models.CharField(max_length=200)
+ old_value = models.CharField(max_length=300, blank=True, null=True)
+ new_value = models.CharField(max_length=300, blank=True, null=True)
+
+ class Meta:
+ verbose_name = "Email Notification Log"
+ verbose_name_plural = "Email Notification Logs"
+ db_table = "email_notification_logs"
+ ordering = ("-created_at",)
From 793d85eb2b8e16a59743197290feedb9b396b8c5 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Fri, 12 Jan 2024 12:31:10 +0530
Subject: [PATCH 03/24] dev: user notification preferences
---
apiserver/plane/bgtasks/importer_task.py | 17 +++-
apiserver/plane/bgtasks/notification_task.py | 92 ++++++-------------
.../db/migrations/0056_auto_20240111_1454.py | 30 ++++++
apiserver/plane/db/models/user.py | 32 +++++++
4 files changed, 106 insertions(+), 65 deletions(-)
create mode 100644 apiserver/plane/db/migrations/0056_auto_20240111_1454.py
diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py
index f580852491..4215213635 100644
--- a/apiserver/plane/bgtasks/importer_task.py
+++ b/apiserver/plane/bgtasks/importer_task.py
@@ -24,6 +24,7 @@
Label,
User,
IssueProperty,
+ UserNotificationPreference,
)
@@ -50,10 +51,24 @@ def service_importer(service, importer_id):
for user in users
if user.get("import", False) == "invite"
],
- batch_size=10,
+ batch_size=100,
ignore_conflicts=True,
)
+ _ = UserNotificationPreference.objects.bulk_create(
+ [UserNotificationPreference(user=user) for user in new_users],
+ batch_size=100,
+ )
+
+ _ = [
+ send_welcome_slack.delay(
+ str(user.id),
+ True,
+ f"{user.email} was imported to Plane from {service}",
+ )
+ for user in new_users
+ ]
+
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py
index 5784bea6f0..03889f4f27 100644
--- a/apiserver/plane/bgtasks/notification_task.py
+++ b/apiserver/plane/bgtasks/notification_task.py
@@ -13,8 +13,6 @@
Notification,
IssueComment,
IssueActivity,
- NotificationPreference,
- EmailNotificationLog,
)
# Third Party imports
@@ -22,7 +20,7 @@
from bs4 import BeautifulSoup
-# =========== Issue Description Html Parsing and Notification Functions ======================
+# =========== Issue Description Html Parsing and notification Functions ======================
def update_mentions_for_issue(issue, project, new_mentions, removed_mention):
@@ -60,8 +58,6 @@ def get_new_mentions(requested_instance, current_instance):
# Get Removed Mention
-
-
def get_removed_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
@@ -79,8 +75,6 @@ def get_removed_mentions(requested_instance, current_instance):
# Adds mentions as subscribers
-
-
def extract_mentions_as_subscribers(project_id, issue_id, mentions):
# mentions is an array of User IDs representing the FILTERED set of mentioned users
@@ -132,7 +126,7 @@ def extract_mentions(issue_instance):
return []
-# =========== Comment Parsing and Notification Functions ======================
+# =========== Comment Parsing and notification Functions ======================
def extract_comment_mentions(comment_value):
try:
mentions = []
@@ -277,12 +271,7 @@ def notifications(
- 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(
- IssueAssignee.objects.filter(project_id=project_id, issue_id=issue_id)
- .exclude(assignee_id__in=list(new_mentions + comment_mentions))
- .values_list("assignee", flat=True)
- )
-
+ # ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
issue_subscribers = list(
IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
.exclude(
@@ -293,68 +282,50 @@ def notifications(
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:
# add the user to issue subscriber
try:
- if (
- str(issue.created_by_id) != str(actor_id)
- and uuid.UUID(actor_id) not in issue_assignees
- ):
- _ = IssueSubscriber.objects.get_or_create(
- project_id=project_id,
- 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:
pass
project = Project.objects.get(pk=project_id)
- issue_subscribers = list(
- set(issue_subscribers + issue_assignees) - {uuid.UUID(actor_id)}
- )
+ issue_assignees = IssueAssignee.objects.filter(
+ issue_id=issue_id, project_id=project_id
+ ).values_list("assignees", flat=True)
+
+ issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)})
for subscriber in issue_subscribers:
- if subscriber in issue_subscribers:
- trigger = "subscribed"
- sender = "in_app:issue_activities:subscribed"
- if issue.created_by_id is not None and subscriber == issue.created_by_id:
- trigger = "created"
+ if issue.created_by_id and issue.created_by_id == subscriber:
sender = "in_app:issue_activities:created"
- if subscriber in issue_assignees:
- trigger = "assigned"
+ elif (
+ subscriber in issue_assignees
+ and issue.created_by_id not in issue_assignees
+ ):
sender = "in_app:issue_activities:assigned"
+ else:
+ sender = "in_app:issue_activities:subscribed"
for issue_activity in issue_activities_created:
# Do not send notification for description update
if issue_activity.get("field") == "description":
continue
- issue_comment = issue_activity.get("issue_comment")
- if issue_comment is not None:
- issue_comment = IssueComment.objects.get(
- id=issue_comment,
+
+ # If activity is of issue comment fetch the comment
+ issue_comment = (
+ IssueComment.objects.get(
+ id=issue_activity.get("issue_comment"),
issue_id=issue_id,
project_id=project_id,
workspace_id=project.workspace_id,
)
-
- preference = NotificationPreference.objects.filter(
- user=subscriber,
- workspace_id=project.workspace_id,
- project_id=project_id,
- ).values("created", "assigned", "subscribed").first()
-
- if issue_activity.get("field") == "state":
- preference = preference.get(trigger).get("state")
- elif issue_activity.get("field") == "comment":
- preference = preference.get(trigger).get("comment")
- else:
- preference = preference.get(trigger).get("property_change")
+ if issue_activity.get("issue_comment")
+ else None
+ )
bulk_notifications.append(
Notification(
@@ -388,8 +359,7 @@ def notifications(
),
"issue_comment": str(
issue_comment.comment_stripped
- if issue_activity.get("issue_comment")
- is not None
+ if issue_comment is not None
else ""
),
},
@@ -397,14 +367,8 @@ def notifications(
)
)
- if preference:
- bulk_email_logs(
- EmailNotificationLog(
- user_id=subscriber,
-
- )
- )
+# ----------------------------------------------------------------------------------------------------------------- #
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
diff --git a/apiserver/plane/db/migrations/0056_auto_20240111_1454.py b/apiserver/plane/db/migrations/0056_auto_20240111_1454.py
new file mode 100644
index 0000000000..a0d0ccfa34
--- /dev/null
+++ b/apiserver/plane/db/migrations/0056_auto_20240111_1454.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.7 on 2024-01-11 14:54
+
+from django.db import migrations
+
+
+def create_notification_preferences(apps, schema_editor):
+ UserNotificationPreference = apps.get_model("db", "UserNotificationPreference")
+ User = apps.get_model("db", "User")
+
+ bulk_notification_preferences = []
+ for user_id in User.objects.filter(is_bot=False).values_list("id", flat=True):
+ bulk_notification_preferences.append(
+ UserNotificationPreference(
+ user_id=user_id,
+ created_by_id=user_id,
+ )
+ )
+ UserNotificationPreference.objects.bulk_create(
+ bulk_notification_preferences, batch_size=1000, ignore_conflicts=True
+ )
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("db", "0055_usernotificationpreference_emailnotificationlog"),
+ ]
+
+ operations = [
+ migrations.RunPython(create_notification_preferences),
+ ]
diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py
index 087162ca51..0a11439db4 100644
--- a/apiserver/plane/db/models/user.py
+++ b/apiserver/plane/db/models/user.py
@@ -13,6 +13,9 @@
)
from django.utils import timezone
+# Module imports
+from plane.db.models import UserNotificationPreference
+
def get_default_onboarding():
return {
@@ -134,3 +137,32 @@ def save(self, *args, **kwargs):
self.is_staff = True
super(User, self).save(*args, **kwargs)
+
+
+@receiver(post_save, sender=User)
+def send_welcome_slack(sender, instance, created, **kwargs):
+ try:
+ if created and not instance.is_bot:
+ # Send message on slack as well
+ if settings.SLACK_BOT_TOKEN:
+ client = WebClient(token=settings.SLACK_BOT_TOKEN)
+ try:
+ _ = client.chat_postMessage(
+ channel="#trackers",
+ text=f"New user {instance.email} has signed up and begun the onboarding journey.",
+ )
+ except SlackApiError as e:
+ print(f"Got an error: {e.response['error']}")
+ return
+ except Exception as e:
+ capture_exception(e)
+ return
+
+
+@receiver(post_save, sender=User)
+def create_user_notification(sender, instance, created, **kwargs):
+ # create preferences
+ if created and not instance.is_bot:
+ UserNotificationPreference.objects.create(
+ user=instance,
+ )
From 0bd3d161c69f4ee1272c4a46b69305694fd19c48 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Fri, 12 Jan 2024 14:34:55 +0530
Subject: [PATCH 04/24] dev: create notification logs for the user.
---
apiserver/plane/bgtasks/notification_task.py | 527 +++++++++++--------
apiserver/plane/db/models/notification.py | 5 +-
apiserver/plane/db/models/user.py | 5 +-
3 files changed, 322 insertions(+), 215 deletions(-)
diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py
index 03889f4f27..c2ad2a593d 100644
--- a/apiserver/plane/bgtasks/notification_task.py
+++ b/apiserver/plane/bgtasks/notification_task.py
@@ -10,9 +10,12 @@
User,
IssueAssignee,
Issue,
+ State,
+ EmailNotificationLog,
Notification,
IssueComment,
IssueActivity,
+ UserNotificationPreference,
)
# Third Party imports
@@ -197,223 +200,170 @@ def notifications(
requested_data,
current_instance,
):
- issue_activities_created = (
- json.loads(issue_activities_created)
- if issue_activities_created is not None
- else None
- )
- if type not in [
- "issue.activity.deleted",
- "cycle.activity.created",
- "cycle.activity.deleted",
- "module.activity.created",
- "module.activity.deleted",
- "issue_reaction.activity.created",
- "issue_reaction.activity.deleted",
- "comment_reaction.activity.created",
- "comment_reaction.activity.deleted",
- "issue_vote.activity.created",
- "issue_vote.activity.deleted",
- "issue_draft.activity.created",
- "issue_draft.activity.updated",
- "issue_draft.activity.deleted",
- ]:
- # Create Notifications
- bulk_notifications = []
- bulk_email_logs = []
-
- """
- Mention Tasks
- 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
- """
-
- # Get new mentions from the newer instance
- new_mentions = get_new_mentions(
- requested_instance=requested_data, current_instance=current_instance
- )
- removed_mention = get_removed_mentions(
- requested_instance=requested_data, current_instance=current_instance
+ try:
+ issue_activities_created = (
+ json.loads(issue_activities_created)
+ if issue_activities_created is not None
+ else None
)
+ if type not in [
+ "issue.activity.deleted",
+ "cycle.activity.created",
+ "cycle.activity.deleted",
+ "module.activity.created",
+ "module.activity.deleted",
+ "issue_reaction.activity.created",
+ "issue_reaction.activity.deleted",
+ "comment_reaction.activity.created",
+ "comment_reaction.activity.deleted",
+ "issue_vote.activity.created",
+ "issue_vote.activity.deleted",
+ "issue_draft.activity.created",
+ "issue_draft.activity.updated",
+ "issue_draft.activity.deleted",
+ ]:
+ # Create Notifications
+ bulk_notifications = []
+ bulk_email_logs = []
+
+ """
+ Mention Tasks
+ 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
+ """
+
+ # Get new mentions from the newer instance
+ new_mentions = get_new_mentions(
+ requested_instance=requested_data, current_instance=current_instance
+ )
+ removed_mention = get_removed_mentions(
+ requested_instance=requested_data, current_instance=current_instance
+ )
- comment_mentions = []
- all_comment_mentions = []
-
- # Get New Subscribers from the mentions of the newer instance
- requested_mentions = extract_mentions(issue_instance=requested_data)
- mention_subscribers = extract_mentions_as_subscribers(
- project_id=project_id, issue_id=issue_id, mentions=requested_mentions
- )
+ comment_mentions = []
+ all_comment_mentions = []
- for issue_activity in issue_activities_created:
- issue_comment = issue_activity.get("issue_comment")
- issue_comment_new_value = issue_activity.get("new_value")
- issue_comment_old_value = issue_activity.get("old_value")
- 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.
+ # Get New Subscribers from the mentions of the newer instance
+ requested_mentions = extract_mentions(issue_instance=requested_data)
+ mention_subscribers = extract_mentions_as_subscribers(
+ project_id=project_id, issue_id=issue_id, mentions=requested_mentions
+ )
- all_comment_mentions = all_comment_mentions + extract_comment_mentions(
- issue_comment_new_value
- )
+ for issue_activity in issue_activities_created:
+ issue_comment = issue_activity.get("issue_comment")
+ issue_comment_new_value = issue_activity.get("new_value")
+ issue_comment_old_value = issue_activity.get("old_value")
+ 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
+ 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_subscribers = list(
- IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
- .exclude(
- subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])
+ comment_mention_subscribers = extract_mentions_as_subscribers(
+ project_id=project_id, issue_id=issue_id, mentions=all_comment_mentions
)
- .values_list("subscriber", flat=True)
- )
-
- issue = Issue.objects.filter(pk=issue_id).first()
-
- if subscriber:
- # add the user to issue subscriber
- try:
- _ = IssueSubscriber.objects.get_or_create(
- project_id=project_id, issue_id=issue_id, subscriber_id=actor_id
+ """
+ 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_subscribers = list(
+ IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
+ .exclude(
+ subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])
)
- except Exception as e:
- pass
-
- project = Project.objects.get(pk=project_id)
-
- issue_assignees = IssueAssignee.objects.filter(
- issue_id=issue_id, project_id=project_id
- ).values_list("assignees", flat=True)
-
- issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)})
+ .values_list("subscriber", flat=True)
+ )
- for subscriber in issue_subscribers:
- if issue.created_by_id and issue.created_by_id == subscriber:
- sender = "in_app:issue_activities:created"
- elif (
- subscriber in issue_assignees
- and issue.created_by_id not in issue_assignees
- ):
- sender = "in_app:issue_activities:assigned"
- else:
- sender = "in_app:issue_activities:subscribed"
+ issue = Issue.objects.filter(pk=issue_id).first()
- for issue_activity in issue_activities_created:
- # Do not send notification for description update
- if issue_activity.get("field") == "description":
- continue
-
- # If activity is of issue comment fetch the comment
- issue_comment = (
- IssueComment.objects.get(
- id=issue_activity.get("issue_comment"),
- issue_id=issue_id,
- project_id=project_id,
- workspace_id=project.workspace_id,
- )
- if issue_activity.get("issue_comment")
- else None
- )
-
- bulk_notifications.append(
- Notification(
- workspace=project.workspace,
- sender=sender,
- triggered_by_id=actor_id,
- receiver_id=subscriber,
- entity_identifier=issue_id,
- entity_name="issue",
- project=project,
- title=issue_activity.get("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(issue_activity.get("id")),
- "verb": str(issue_activity.get("verb")),
- "field": str(issue_activity.get("field")),
- "actor": str(issue_activity.get("actor_id")),
- "new_value": str(
- issue_activity.get("new_value")
- ),
- "old_value": str(
- issue_activity.get("old_value")
- ),
- "issue_comment": str(
- issue_comment.comment_stripped
- if issue_comment is not None
- else ""
- ),
- },
- },
+ if subscriber:
+ # add the user to issue subscriber
+ try:
+ _ = IssueSubscriber.objects.get_or_create(
+ project_id=project_id, issue_id=issue_id, subscriber_id=actor_id
)
- )
+ except Exception as e:
+ pass
+ project = Project.objects.get(pk=project_id)
-# ----------------------------------------------------------------------------------------------------------------- #
+ issue_assignees = IssueAssignee.objects.filter(
+ issue_id=issue_id, project_id=project_id
+ ).values_list("assignee", flat=True)
- # Add Mentioned as Issue Subscribers
- IssueSubscriber.objects.bulk_create(
- mention_subscribers + comment_mention_subscribers, batch_size=100
- )
+ issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)})
- last_activity = (
- IssueActivity.objects.filter(issue_id=issue_id)
- .order_by("-created_at")
- .first()
- )
+ for subscriber in issue_subscribers:
+ if issue.created_by_id and issue.created_by_id == subscriber:
+ sender = "in_app:issue_activities:created"
+ elif (
+ subscriber in issue_assignees
+ and issue.created_by_id not in issue_assignees
+ ):
+ sender = "in_app:issue_activities:assigned"
+ else:
+ sender = "in_app:issue_activities:subscribed"
- actor = User.objects.get(pk=actor_id)
+ preference = UserNotificationPreference.objects.get(user_id=subscriber)
- for mention_id in comment_mentions:
- if mention_id != actor_id:
for issue_activity in issue_activities_created:
- notification = create_mention_notification(
- 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,
+ # Do not send notification for description update
+ if issue_activity.get("field") == "description":
+ continue
+
+ # Check if the value should be sent or not
+ send_email = False
+ if issue_activity.get("field") == "state" and preference.state_change:
+ send_email = True
+ elif (
+ issue_activity.get("field") == "state"
+ and preference.issue_completed
+ and State.objects.filter(
+ project_id=project_id,
+ pk=issue_activity.get("new_identifier"),
+ group="completed",
+ ).exists()
+ ):
+ send_email = True
+ elif issue_activity.get("field") == "comment" and preference.comment:
+ send_email = True
+ elif preference.property_change:
+ send_email = True
+ else:
+ send_email = False
+
+ # If activity is of issue comment fetch the comment
+ issue_comment = (
+ IssueComment.objects.filter(
+ id=issue_activity.get("issue_comment"),
+ issue_id=issue_id,
+ project_id=project_id,
+ workspace_id=project.workspace_id,
+ ).first()
+ if issue_activity.get("issue_comment")
+ else None
)
- bulk_notifications.append(notification)
-
- for mention_id in new_mentions:
- if mention_id != actor_id:
- if (
- last_activity is not None
- and last_activity.field == "description"
- and actor_id == str(last_activity.actor_id)
- ):
+
bulk_notifications.append(
Notification(
workspace=project.workspace,
- sender="in_app:issue_activities:mentioned",
+ sender=sender,
triggered_by_id=actor_id,
- receiver_id=mention_id,
+ receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
- message=f"You have been mentioned in the issue {issue.name}",
+ title=issue_activity.get("comment"),
data={
"issue": {
"id": str(issue_id),
@@ -426,36 +376,195 @@ def notifications(
"state_group": issue.state.group,
},
"issue_activity": {
- "id": str(last_activity.id),
- "verb": str(last_activity.verb),
- "field": str(last_activity.field),
- "actor": str(last_activity.actor_id),
- "new_value": str(last_activity.new_value),
- "old_value": str(last_activity.old_value),
+ "id": str(issue_activity.get("id")),
+ "verb": str(issue_activity.get("verb")),
+ "field": str(issue_activity.get("field")),
+ "actor": str(issue_activity.get("actor_id")),
+ "new_value": str(issue_activity.get("new_value")),
+ "old_value": str(issue_activity.get("old_value")),
+ "issue_comment": str(
+ issue_comment.comment_stripped
+ if issue_comment is not None
+ else ""
+ ),
},
},
)
)
- else:
+ bulk_email_logs.append(
+ EmailNotificationLog(
+ triggered_by_id=actor_id,
+ receiver_id=subscriber,
+ entity_identifier=issue_id,
+ entity_name="issue",
+ 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(issue_activity.get("id")),
+ "verb": str(issue_activity.get("verb")),
+ "field": str(issue_activity.get("field")),
+ "actor": str(issue_activity.get("actor_id")),
+ "new_value": str(issue_activity.get("new_value")),
+ "old_value": str(issue_activity.get("old_value")),
+ "issue_comment": str(
+ issue_comment.comment_stripped
+ if issue_comment is not None
+ else ""
+ ),
+ },
+ },
+ )
+ )
+
+ # ----------------------------------------------------------------------------------------------------------------- #
+
+ # Add Mentioned as Issue Subscribers
+ IssueSubscriber.objects.bulk_create(
+ mention_subscribers + comment_mention_subscribers, batch_size=100, ignore_conflicts=True,
+ )
+
+ 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:
+ preference = UserNotificationPreference.objects.get(user_id=mention_id)
for issue_activity in issue_activities_created:
notification = create_mention_notification(
project=project,
issue=issue,
- notification_comment=f"You have been mentioned in the issue {issue.name}",
+ 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)
- # 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,
- )
+ # check for email notifications
+ if preference.mention:
+ bulk_email_logs.append(
+ EmailNotificationLog(
+ triggered_by_id=actor_id,
+ receiver_id=subscriber,
+ entity_identifier=issue_id,
+ entity_name="issue",
+ data=notification.data,
+ )
+ )
+ bulk_notifications.append(notification)
- # Bulk create notifications
- Notification.objects.bulk_create(bulk_notifications, batch_size=100)
+ for mention_id in new_mentions:
+ if mention_id != actor_id:
+ preference = UserNotificationPreference.objects.get(user_id=mention_id)
+ if (
+ last_activity is not None
+ and last_activity.field == "description"
+ and actor_id == str(last_activity.actor_id)
+ ):
+ bulk_notifications.append(
+ 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=f"You have been mentioned in the issue {issue.name}",
+ 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(last_activity.id),
+ "verb": str(last_activity.verb),
+ "field": str(last_activity.field),
+ "actor": str(last_activity.actor_id),
+ "new_value": str(last_activity.new_value),
+ "old_value": str(last_activity.old_value),
+ },
+ },
+ )
+ )
+ if preference.mention:
+ bulk_email_logs.append(
+ EmailNotificationLog(
+ triggered_by_id=actor_id,
+ receiver_id=subscriber,
+ entity_identifier=issue_id,
+ entity_name="issue",
+ 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(last_activity.id),
+ "verb": str(last_activity.verb),
+ "field": str(last_activity.field),
+ "actor": str(last_activity.actor_id),
+ "new_value": str(last_activity.new_value),
+ "old_value": str(last_activity.old_value),
+ },
+ },
+ )
+ )
+ else:
+ for issue_activity in issue_activities_created:
+ notification = create_mention_notification(
+ 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,
+ )
+ if preference.mention:
+ bulk_email_logs.append(
+ EmailNotificationLog(
+ triggered_by_id=actor_id,
+ receiver_id=subscriber,
+ entity_identifier=issue_id,
+ entity_name="issue",
+ data=notification.data,
+ )
+ )
+ bulk_notifications.append(notification)
+
+ # 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,
+ )
+ print(bulk_email_logs)
+ # Bulk create notifications
+ Notification.objects.bulk_create(bulk_notifications, batch_size=100)
+ EmailNotificationLog.objects.bulk_create(bulk_email_logs, batch_size=100, ignore_conflicts=True)
+ return
+ except Exception as e:
+ print(e)
+ return
diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py
index 8ae2a1ef3f..da5c65d57c 100644
--- a/apiserver/plane/db/models/notification.py
+++ b/apiserver/plane/db/models/notification.py
@@ -2,9 +2,8 @@
from django.db import models
from django.conf import settings
-# Third party imports
-from .base import BaseModel
-from .project import ProjectBaseModel
+# Module imports
+from . import BaseModel
class Notification(BaseModel):
workspace = models.ForeignKey(
diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py
index 0a11439db4..cf9b9f68a8 100644
--- a/apiserver/plane/db/models/user.py
+++ b/apiserver/plane/db/models/user.py
@@ -13,9 +13,6 @@
)
from django.utils import timezone
-# Module imports
-from plane.db.models import UserNotificationPreference
-
def get_default_onboarding():
return {
@@ -163,6 +160,8 @@ def send_welcome_slack(sender, instance, created, **kwargs):
def create_user_notification(sender, instance, created, **kwargs):
# create preferences
if created and not instance.is_bot:
+ # Module imports
+ from plane.db.models import UserNotificationPreference
UserNotificationPreference.objects.create(
user=instance,
)
From ef7503b922c0e1d60a5899f53be568237958fc77 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Tue, 16 Jan 2024 15:17:58 +0530
Subject: [PATCH 05/24] dev: email notification stacking and sending logic
---
.../plane/bgtasks/email_notification_task.py | 132 ++++++++++++++++++
apiserver/plane/bgtasks/notification_task.py | 88 ++++++++++--
apiserver/plane/celery.py | 5 +
.../0057_emailnotificationlog_processed_at.py | 18 +++
apiserver/plane/db/models/notification.py | 15 +-
apiserver/plane/settings/common.py | 1 +
6 files changed, 242 insertions(+), 17 deletions(-)
create mode 100644 apiserver/plane/bgtasks/email_notification_task.py
create mode 100644 apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py
diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py
new file mode 100644
index 0000000000..6f50cc84d8
--- /dev/null
+++ b/apiserver/plane/bgtasks/email_notification_task.py
@@ -0,0 +1,132 @@
+# Third party imports
+from celery import shared_task
+
+# Django imports
+from django.utils import timezone
+
+# Module imports
+from plane.db.models import EmailNotificationLog, User, Issue
+
+
+@shared_task
+def stack_email_notification():
+ # get all email notifications
+ email_notifications = (
+ EmailNotificationLog.objects.filter(processed_at__isnull=True)
+ .order_by("receiver")
+ .values()
+ )
+
+ # {"issue_id" : { "actor_id": [ { data }, { data } ] }}
+
+ # Convert to unique receivers list
+ receivers = list(
+ set(
+ [
+ str(notification.get("receiver_id"))
+ for notification in email_notifications
+ ]
+ )
+ )
+ processed_notifications = []
+ # Loop through all the issues to create the emails
+ for receiver_id in receivers:
+ # Notifcation triggered for the receiver
+ receiver_notifications = [
+ notification
+ for notification in email_notifications
+ if str(notification.get("receiver_id")) == receiver_id
+ ]
+ # create payload for all issues
+ payload = {}
+ email_notification_ids = []
+ for receiver_notification in receiver_notifications:
+ payload.setdefault(
+ receiver_notification.get("entity_identifier"), {}
+ ).setdefault(str(receiver_notification.get("triggered_by_id")), []).append(
+ receiver_notification.get("data")
+ )
+ # append processed notifications
+ processed_notifications.append(receiver_notification.get("id"))
+ email_notification_ids.append(receiver_notification.get("id"))
+
+ # Create emails for all the issues
+ for issue_id, issue_data in payload.items():
+ send_email_notification.delay(
+ issue_id=issue_id,
+ issue_data=issue_data,
+ receiver_id=receiver_id,
+ email_notification_ids=email_notification_ids,
+ )
+
+ # Update the email notification log
+ EmailNotificationLog.objects.filter(pk__in=processed_notifications).update(
+ processed_at=timezone.now()
+ )
+
+
+def create_payload(payload):
+ # {"actor_id": { "key": { "old_value": [], "new_value": [] } }}
+ data = {}
+ for actor_id, changes in payload.items():
+ for change in changes:
+ issue_activity = change.get("issue_activity")
+ if issue_activity: # Ensure issue_activity is not None
+ field = issue_activity.get("field")
+ old_value = issue_activity.get("old_value")
+ new_value = issue_activity.get("new_value")
+
+ # Append old_value if it's not empty and not already in the list
+ if old_value:
+ data.setdefault(actor_id, {}).setdefault(field, {}).setdefault(
+ "old_value", []
+ ).append(old_value) if old_value not in data.setdefault(
+ actor_id, {}
+ ).setdefault(
+ field, {}
+ ).get(
+ "old_value", []
+ ) else None
+
+ # Append new_value if it's not empty and not already in the list
+ if new_value:
+ data.setdefault(actor_id, {}).setdefault(field, {}).setdefault(
+ "new_value", []
+ ).append(new_value) if new_value not in data.setdefault(
+ actor_id, {}
+ ).setdefault(
+ field, {}
+ ).get(
+ "new_value", []
+ ) else None
+
+ return data
+
+
+@shared_task
+def send_email_notification(issue_id, issue_data, receiver_id, email_notification_ids):
+ data = create_payload(payload=issue_data)
+
+ receiver = User.objects.get(pk=receiver_id)
+ issue = Issue.objects.get(pk=issue_id)
+ template_data = []
+ for actor_id, changes in data.items():
+ actor = User.objects.get(pk=actor_id)
+ template_data.append(
+ {
+ "actor_details": {
+ "avatar": actor.avatar,
+ "first_name": actor.first_name,
+ "last_name": actor.last_name,
+ },
+ "changes": changes,
+ "issue_details": {
+ "name": issue.name,
+ "identifier": f"{issue.project.identifier}-{issue.sequence_id}",
+ },
+ }
+ )
+
+ EmailNotificationLog.objects.filter(pk__in=email_notification_ids).update(
+ sent_at=timezone.now()
+ )
diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py
index c2ad2a593d..a73d39c206 100644
--- a/apiserver/plane/bgtasks/notification_task.py
+++ b/apiserver/plane/bgtasks/notification_task.py
@@ -256,12 +256,14 @@ def notifications(
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
+ 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
+ old_value=issue_comment_old_value,
+ new_value=issue_comment_new_value,
)
comment_mentions = comment_mentions + new_comment_mentions
@@ -323,7 +325,10 @@ def notifications(
# Check if the value should be sent or not
send_email = False
- if issue_activity.get("field") == "state" and preference.state_change:
+ if (
+ issue_activity.get("field") == "state"
+ and preference.state_change
+ ):
send_email = True
elif (
issue_activity.get("field") == "state"
@@ -335,7 +340,9 @@ def notifications(
).exists()
):
send_email = True
- elif issue_activity.get("field") == "comment" and preference.comment:
+ elif (
+ issue_activity.get("field") == "comment" and preference.comment
+ ):
send_email = True
elif preference.property_change:
send_email = True
@@ -427,7 +434,9 @@ def notifications(
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
- mention_subscribers + comment_mention_subscribers, batch_size=100, ignore_conflicts=True,
+ mention_subscribers + comment_mention_subscribers,
+ batch_size=100,
+ ignore_conflicts=True,
)
last_activity = (
@@ -440,7 +449,9 @@ def notifications(
for mention_id in comment_mentions:
if mention_id != actor_id:
- preference = UserNotificationPreference.objects.get(user_id=mention_id)
+ preference = UserNotificationPreference.objects.get(
+ user_id=mention_id
+ )
for issue_activity in issue_activities_created:
notification = create_mention_notification(
project=project,
@@ -460,14 +471,39 @@ def notifications(
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
- data=notification.data,
+ 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(issue_activity.get("id")),
+ "verb": str(issue_activity.get("verb")),
+ "field": str("mention"),
+ "actor": str(
+ issue_activity.get("actor_id")
+ ),
+ "new_value": str(
+ issue_activity.get("new_value")
+ ),
+ "old_value": str(
+ issue_activity.get("old_value")
+ ),
+ },
+ },
)
)
bulk_notifications.append(notification)
for mention_id in new_mentions:
if mention_id != actor_id:
- preference = UserNotificationPreference.objects.get(user_id=mention_id)
+ preference = UserNotificationPreference.objects.get(
+ user_id=mention_id
+ )
if (
last_activity is not None
and last_activity.field == "description"
@@ -522,7 +558,7 @@ def notifications(
"issue_activity": {
"id": str(last_activity.id),
"verb": str(last_activity.verb),
- "field": str(last_activity.field),
+ "field": "mention",
"actor": str(last_activity.actor_id),
"new_value": str(last_activity.new_value),
"old_value": str(last_activity.old_value),
@@ -548,7 +584,32 @@ def notifications(
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
- data=notification.data,
+ 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(issue_activity.get("id")),
+ "verb": str(issue_activity.get("verb")),
+ "field": str("mention"),
+ "actor": str(
+ issue_activity.get("actor_id")
+ ),
+ "new_value": str(
+ issue_activity.get("new_value")
+ ),
+ "old_value": str(
+ issue_activity.get("old_value")
+ ),
+ },
+ },
)
)
bulk_notifications.append(notification)
@@ -560,10 +621,11 @@ def notifications(
new_mentions=new_mentions,
removed_mention=removed_mention,
)
- print(bulk_email_logs)
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
- EmailNotificationLog.objects.bulk_create(bulk_email_logs, batch_size=100, ignore_conflicts=True)
+ EmailNotificationLog.objects.bulk_create(
+ bulk_email_logs, batch_size=100, ignore_conflicts=True
+ )
return
except Exception as e:
print(e)
diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py
index 442e728364..6872eb43c3 100644
--- a/apiserver/plane/celery.py
+++ b/apiserver/plane/celery.py
@@ -2,6 +2,7 @@
from celery import Celery
from plane.settings.redis import redis_instance
from celery.schedules import crontab
+from django.utils.timezone import timedelta
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
@@ -28,6 +29,10 @@
"task": "plane.bgtasks.file_asset_task.delete_file_asset",
"schedule": crontab(hour=0, minute=0),
},
+ "check-every-five-minutes-to-send-email-notifications": {
+ "task": "plane.bgtasks.email_notification_task.stack_email_notification",
+ "schedule": timedelta(seconds=10)
+ },
}
# Load task modules from all registered Django app configs.
diff --git a/apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py b/apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py
new file mode 100644
index 0000000000..3d773dba9b
--- /dev/null
+++ b/apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2024-01-16 07:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('db', '0056_auto_20240111_1454'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='emailnotificationlog',
+ name='processed_at',
+ field=models.DateTimeField(null=True),
+ ),
+ ]
diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py
index da5c65d57c..5f640086f8 100644
--- a/apiserver/plane/db/models/notification.py
+++ b/apiserver/plane/db/models/notification.py
@@ -88,10 +88,17 @@ def __str__(self):
"""Return name of the notifications"""
return f"{self.user.email} <{self.workspace.name}>"
-
-class EmailNotificationLog(ProjectBaseModel):
- receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notifications")
- triggered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="triggered_by_notifications")
+class EmailNotificationLog(BaseModel):
+ # receiver
+ receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="email_notifications")
+ triggered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="triggered_emails")
+ # entity - can be issues, pages, etc.
+ entity_identifier = models.UUIDField(null=True)
+ entity_name = models.CharField(max_length=255)
+ # data
+ data = models.JSONField(null=True)
+ # sent at
+ processed_at = models.DateTimeField(null=True)
sent_at = models.DateTimeField(null=True)
entity = models.CharField(max_length=200)
old_value = models.CharField(max_length=300, blank=True, null=True)
diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py
index 623583840d..444248382f 100644
--- a/apiserver/plane/settings/common.py
+++ b/apiserver/plane/settings/common.py
@@ -291,6 +291,7 @@
"plane.bgtasks.issue_automation_task",
"plane.bgtasks.exporter_expired_task",
"plane.bgtasks.file_asset_task",
+ "plane.bgtasks.email_notification_task",
)
# Sentry Settings
From 76a6e9a19ed78706ce51109ba56b0a0cefdd0e5a Mon Sep 17 00:00:00 2001
From: Prateek Shourya
Date: Tue, 16 Jan 2024 17:54:26 +0530
Subject: [PATCH 06/24] feat: email notification preference settings page.
---
.../preferences/email-notification-form.tsx | 176 ++++++++++++++++++
web/components/profile/preferences/index.ts | 1 +
.../profile/preferences/index.ts | 2 +
.../profile/preferences/layout.tsx | 25 +++
.../profile/preferences/sidebar.tsx | 43 +++++
web/pages/profile/preferences/email.tsx | 19 ++
.../theme.tsx} | 12 +-
7 files changed, 272 insertions(+), 6 deletions(-)
create mode 100644 web/components/profile/preferences/email-notification-form.tsx
create mode 100644 web/components/profile/preferences/index.ts
create mode 100644 web/layouts/settings-layout/profile/preferences/index.ts
create mode 100644 web/layouts/settings-layout/profile/preferences/layout.tsx
create mode 100644 web/layouts/settings-layout/profile/preferences/sidebar.tsx
create mode 100644 web/pages/profile/preferences/email.tsx
rename web/pages/profile/{preferences.tsx => preferences/theme.tsx} (83%)
diff --git a/web/components/profile/preferences/email-notification-form.tsx b/web/components/profile/preferences/email-notification-form.tsx
new file mode 100644
index 0000000000..eb9d313c75
--- /dev/null
+++ b/web/components/profile/preferences/email-notification-form.tsx
@@ -0,0 +1,176 @@
+import { FC } from "react";
+import { Controller, useForm } from "react-hook-form";
+// ui
+import { ToggleSwitch } from "@plane/ui";
+
+export interface EmailPreferenceValues {
+ email_notification: boolean;
+ property_change: boolean;
+ state_change: boolean;
+ issue_completed: boolean;
+ comment: boolean;
+ mention: boolean;
+}
+
+export const EmailNotificationForm: FC = () => {
+ // form data
+ const {
+ handleSubmit,
+ control,
+ formState: { isSubmitting },
+ } = useForm({
+ defaultValues: {
+ email_notification: true,
+ property_change: true,
+ state_change: true,
+ issue_completed: true,
+ comment: true,
+ mention: true,
+ },
+ });
+
+ const onSubmit = async (formData: EmailPreferenceValues) => {
+ console.log(formData);
+ };
+
+ return (
+ <>
+
+
+
Email notifications
+
+ Get emails to find out what’s going on when you’re not on Plane. You can turn them off anytime
+
+
+
+ (
+ onChange(!value)} size="sm" />
+ )}
+ />
+
+
+ Send me email notifications for:
+ {/* Notification Settings */}
+
+
+
+
Property changes
+
+ You’ll be notified about the property changes of an issue you’re a subscriber to you.
+
+
+
+ (
+ onChange(!value)}
+ className="w-3.5 h-3.5 mx-2 cursor-pointer !border-custom-border-100"
+ />
+ )}
+ />
+
+
+
+
+
State Change
+
+ You’ll be notified about the state changes to the issues you’re a subscriber to
+
+
+
+ (
+ onChange(!value)}
+ className="w-3.5 h-3.5 mx-2 cursor-pointer"
+ />
+ )}
+ />
+
+
+
+
+
Issue completed
+
+ We’ll notify you only with the issue is moved to completed state or state group
+
+
+
+ (
+ onChange(!value)}
+ className="w-3.5 h-3.5 mx-2 cursor-pointer"
+ />
+ )}
+ />
+
+
+
+
+
Comments
+
+ You will be notified when somebody comments on an issue you’re subscribed to
+
+
+
+ (
+ onChange(!value)}
+ className="w-3.5 h-3.5 mx-2 cursor-pointer"
+ />
+ )}
+ />
+
+
+
+
+
Mentions
+
+ You’ll be notified every time someone mentions you in any issue.
+
+
+
+ (
+ onChange(!value)}
+ className="w-3.5 h-3.5 mx-2 cursor-pointer"
+ />
+ )}
+ />
+
+
+
+ {/*
+
+ {isSubmitting ? "Saving..." : "Save changes"}
+
+
*/}
+ >
+ );
+};
diff --git a/web/components/profile/preferences/index.ts b/web/components/profile/preferences/index.ts
new file mode 100644
index 0000000000..ddda5712c5
--- /dev/null
+++ b/web/components/profile/preferences/index.ts
@@ -0,0 +1 @@
+export * from "./email-notification-form";
\ No newline at end of file
diff --git a/web/layouts/settings-layout/profile/preferences/index.ts b/web/layouts/settings-layout/profile/preferences/index.ts
new file mode 100644
index 0000000000..34e2302584
--- /dev/null
+++ b/web/layouts/settings-layout/profile/preferences/index.ts
@@ -0,0 +1,2 @@
+export * from "./layout";
+export * from "./sidebar";
\ No newline at end of file
diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx
new file mode 100644
index 0000000000..9d17350a98
--- /dev/null
+++ b/web/layouts/settings-layout/profile/preferences/layout.tsx
@@ -0,0 +1,25 @@
+import { FC, ReactNode } from "react";
+// layout
+import { ProfileSettingsLayout } from "layouts/settings-layout";
+import { ProfilePreferenceSettingsSidebar } from "./sidebar";
+
+interface IProfilePreferenceSettingsLayout {
+ children: ReactNode;
+ header?: ReactNode;
+}
+
+export const ProfilePreferenceSettingsLayout: FC = (props) => {
+ const { children, header } = props;
+
+ return (
+
+
+
+
+ {header}
+ {children}
+
+
+
+ );
+};
diff --git a/web/layouts/settings-layout/profile/preferences/sidebar.tsx b/web/layouts/settings-layout/profile/preferences/sidebar.tsx
new file mode 100644
index 0000000000..d1eec12331
--- /dev/null
+++ b/web/layouts/settings-layout/profile/preferences/sidebar.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { useRouter } from "next/router";
+import Link from "next/link";
+
+export const ProfilePreferenceSettingsSidebar = () => {
+ const router = useRouter();
+
+ const profilePreferenceLinks: Array<{
+ label: string;
+ href: string;
+ }> = [
+ {
+ label: "Theme",
+ href: `/profile/preferences/theme`,
+ },
+ {
+ label: "Email",
+ href: `/profile/preferences/email`,
+ },
+ ];
+ return (
+
+
+
Preference
+
+ {profilePreferenceLinks.map((link) => (
+
+
+ {link.label}
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx
new file mode 100644
index 0000000000..3f5cef9cd5
--- /dev/null
+++ b/web/pages/profile/preferences/email.tsx
@@ -0,0 +1,19 @@
+import { ReactElement } from "react";
+// layouts
+import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences";
+// components
+import { EmailNotificationForm } from "components/profile/preferences";
+// type
+import { NextPageWithLayout } from "lib/types";
+
+const ProfilePreferencesThemePage: NextPageWithLayout = () => (
+
+
+
+);
+
+ProfilePreferencesThemePage.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};
+
+export default ProfilePreferencesThemePage;
diff --git a/web/pages/profile/preferences.tsx b/web/pages/profile/preferences/theme.tsx
similarity index 83%
rename from web/pages/profile/preferences.tsx
rename to web/pages/profile/preferences/theme.tsx
index 23c85134ef..51386bc29d 100644
--- a/web/pages/profile/preferences.tsx
+++ b/web/pages/profile/preferences/theme.tsx
@@ -5,7 +5,7 @@ import { useTheme } from "next-themes";
import { useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// layouts
-import { ProfileSettingsLayout } from "layouts/settings-layout";
+import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences";
// components
import { CustomThemeSelector, ThemeSwitch } from "components/core";
// ui
@@ -15,7 +15,7 @@ import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes";
// type
import { NextPageWithLayout } from "lib/types";
-const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
+const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
// states
const [currentTheme, setCurrentTheme] = useState(null);
// store hooks
@@ -48,7 +48,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
return (
<>
{currentUser ? (
-
+
Preferences
@@ -72,8 +72,8 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
);
});
-ProfilePreferencesPage.getLayout = function getLayout(page: ReactElement) {
- return
{page} ;
+ProfilePreferencesThemePage.getLayout = function getLayout(page: ReactElement) {
+ return
{page} ;
};
-export default ProfilePreferencesPage;
+export default ProfilePreferencesThemePage;
From 211cde8d5e5cdeec19d91818850cc575c31a4a7c Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Thu, 18 Jan 2024 18:20:03 +0530
Subject: [PATCH 07/24] dev: delete subscribers
---
apiserver/plane/app/views/notification.py | 56 +++++++++++++++++--
.../plane/bgtasks/issue_activites_task.py | 15 +++++
2 files changed, 66 insertions(+), 5 deletions(-)
diff --git a/apiserver/plane/app/views/notification.py b/apiserver/plane/app/views/notification.py
index 15eef9cf0c..3112c5b39a 100644
--- a/apiserver/plane/app/views/notification.py
+++ b/apiserver/plane/app/views/notification.py
@@ -1,5 +1,5 @@
# Django imports
-from django.db.models import Q
+from django.db.models import Q, OuterRef, Exists
from django.utils import timezone
# Third party imports
@@ -71,11 +71,29 @@ def list(self, request, slug):
# Subscribed issues
if type == "watching":
- issue_ids = IssueSubscriber.objects.filter(
- workspace__slug=slug, subscriber_id=request.user.id
- ).values_list("issue_id", flat=True)
+ issue_ids = (
+ IssueSubscriber.objects.filter(
+ workspace__slug=slug, subscriber_id=request.user.id
+ )
+ .annotate(
+ created=Exists(
+ Issue.objects.filter(
+ created_by=request.user, pk=OuterRef("issue_id")
+ )
+ )
+ )
+ .annotate(
+ assigned=Exists(
+ IssueAssignee.objects.filter(
+ pk=OuterRef("issue_id"), assignee=request.user
+ )
+ )
+ )
+ .filter(created=False, assigned=False)
+ .values_list("issue_id", flat=True)
+ )
notifications = notifications.filter(
- entity_identifier__in=issue_ids
+ entity_identifier__in=issue_ids,
)
# Assigned Issues
@@ -295,3 +313,31 @@ def create(self, request, slug):
updated_notifications, ["read_at"], batch_size=100
)
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
+
+
+class UserNotificationPreferenceEndpoint(BaseAPIView):
+ model = UserNotificationPreference
+ serializer_class = UserNotificationPreferenceSerializer
+
+ # request the object
+ def get(self, request):
+ user_notification_preference = UserNotificationPreference.objects.get(
+ user=request.user
+ )
+ serializer = UserNotificationPreferenceSerializer(
+ user_notification_preference
+ )
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ # update the object
+ def patch(self, request):
+ user_notification_preference = UserNotificationPreference.objects.get(
+ user=request.user
+ )
+ serializer = UserNotificationPreferenceSerializer(
+ user_notification_preference, data=request.data, partial=True
+ )
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index 686f06a20b..7e8a27862a 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -376,6 +376,21 @@ def track_assignees(
epoch=epoch,
)
)
+ bulk_subscribers.append(
+ IssueSubscriber(
+ subscriber_id=assignee.id,
+ issue_id=issue_id,
+ workspace_id=workspace_id,
+ project_id=project_id,
+ created_by_id=assignee.id,
+ updated_by_id=assignee.id,
+ )
+ )
+
+ # Create assignees subscribers to the issue and ignore if already
+ IssueSubscriber.objects.bulk_create(
+ bulk_subscribers, batch_size=10, ignore_conflicts=True
+ )
for dropped_assignee in dropped_assginees:
assignee = User.objects.get(pk=dropped_assignee)
From f0806ba7a7f877955ec91d3200925eb42c437cb1 Mon Sep 17 00:00:00 2001
From: LAKHAN BAHETI
Date: Thu, 18 Jan 2024 20:24:26 +0530
Subject: [PATCH 08/24] dev: issue update ui implementation in email
notification
---
.../emails/notifications/issue-updates.html | 280 ++++++++++++++++++
1 file changed, 280 insertions(+)
create mode 100644 apiserver/templates/emails/notifications/issue-updates.html
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
new file mode 100644
index 0000000000..f8a1931779
--- /dev/null
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -0,0 +1,280 @@
+
+
+
+
+
+
+
+
+ Your unique Plane login code is code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plane
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PLW-1 updates
+
+
+ workspacename/projectname/issueId: Sign up flow
+
+
+
+ updates were made to the issue by
+
+ Srinivas Pendem
+
+
+
+
+
+ {{ Updates / Comments }}
+
+
+
+
+
+
+ Srinivas Pendem
+
+
+ 11:36 am IST
+
+
+
+
+
+
+
+ Assignee :
+
+
+ {{ assginee.name }}
+
+
+
+ {{ assginee.name }}
+
+
+
+
+
+
+ Assignee :
+
+
+ {{ assginee.name }}
+
+
+
+ +{{length}} more
+
+
+
+
+
+
+ Assignee :
+
+
+
+
+ {{ assginee.name }}
+
+
+
+ {{ assginee.name }}
+
+
+ +{{length}} more
+
+
+
+
+
+
+
+ Due Date :
+
+ 28 Jun, 2023
+
+
+
+
+ Duplicate :
+
+ PLW-2
+
+
+
+
+
+
+
+
+
+
+
+ Bhavesh Raja
+
+
+ Connect with folks in finance and operations to verify the data
+ provided before presenting it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From ba51a85f52cd532af7b88a898003f0bc56a55f33 Mon Sep 17 00:00:00 2001
From: Prateek Shourya
Date: Thu, 18 Jan 2024 21:10:24 +0530
Subject: [PATCH 09/24] chore: integrate email notification endpoint.
---
packages/types/src/users.d.ts | 8 ++
.../preferences/email-notification-form.tsx | 134 +++++++++++++-----
web/pages/profile/preferences/email.tsx | 27 +++-
web/services/user.service.ts | 18 ++-
4 files changed, 142 insertions(+), 45 deletions(-)
diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts
index bbca953f61..81c8abcd5f 100644
--- a/packages/types/src/users.d.ts
+++ b/packages/types/src/users.d.ts
@@ -166,6 +166,14 @@ export interface IUserProjectsRole {
[projectId: string]: EUserProjectRoles;
}
+export interface IUserEmailNotificationSettings {
+ property_change: boolean;
+ state_change: boolean;
+ comment: boolean;
+ mention: boolean;
+ issue_completed: boolean;
+}
+
// export interface ICurrentUser {
// id: readonly string;
// avatar: string;
diff --git a/web/components/profile/preferences/email-notification-form.tsx b/web/components/profile/preferences/email-notification-form.tsx
index eb9d313c75..a54deef1a6 100644
--- a/web/components/profile/preferences/email-notification-form.tsx
+++ b/web/components/profile/preferences/email-notification-form.tsx
@@ -1,36 +1,88 @@
-import { FC } from "react";
+import { FC, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// ui
-import { ToggleSwitch } from "@plane/ui";
+import { Button, ToggleSwitch } from "@plane/ui";
+// hooks
+import useToast from "hooks/use-toast";
+// services
+import { UserService } from "services/user.service";
+// types
+import { IUserEmailNotificationSettings } from "@plane/types";
-export interface EmailPreferenceValues {
- email_notification: boolean;
- property_change: boolean;
- state_change: boolean;
- issue_completed: boolean;
- comment: boolean;
- mention: boolean;
+interface IEmailNotificationFormProps {
+ data: IUserEmailNotificationSettings;
}
-export const EmailNotificationForm: FC = () => {
+// services
+const userService = new UserService();
+
+export const EmailNotificationForm: FC = (props) => {
+ const { data } = props;
+ // states
+ const [notificationSettings, setNotificationSettings] = useState(false);
+ const [isUpdateAllSettings, setIsUpdateAllSettings] = useState(false);
+ // toast
+ const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
+ watch,
control,
- formState: { isSubmitting },
- } = useForm({
+ setValue,
+ reset,
+ formState: { isSubmitting, isDirty, dirtyFields },
+ } = useForm({
defaultValues: {
- email_notification: true,
- property_change: true,
- state_change: true,
- issue_completed: true,
- comment: true,
- mention: true,
+ ...data,
},
});
- const onSubmit = async (formData: EmailPreferenceValues) => {
- console.log(formData);
+ useEffect(() => {
+ // Update notificationSettings whenever any of the data values change
+ setNotificationSettings(
+ watch("comment") ||
+ watch("issue_completed") ||
+ watch("mention") ||
+ watch("property_change") ||
+ watch("state_change")
+ );
+ }, [
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ watch, watch("comment"), watch("issue_completed"), watch("mention"), watch("property_change"), watch("state_change"),
+ ]);
+
+ const onSubmit = async (formData: IUserEmailNotificationSettings) => {
+ // Get the dirty fields from the form data and create a payload
+ let payload = {};
+ Object.keys(dirtyFields).forEach((key) => {
+ payload = {
+ ...payload,
+ [key]: formData[key as keyof IUserEmailNotificationSettings],
+ };
+ });
+ await userService
+ .updateCurrentUserEmailNotificationSettings(payload)
+ .then(() =>
+ setToastAlert({
+ title: "Success",
+ type: "success",
+ message: "Email Notification Settings updated successfully",
+ })
+ )
+ .catch((err) => console.error(err))
+ .finally(() => setIsUpdateAllSettings(false));
+ };
+
+ const updateAllSettings = async (value: boolean) => {
+ setNotificationSettings(value);
+ setIsUpdateAllSettings(true);
+ reset({
+ comment: value,
+ issue_completed: value,
+ mention: value,
+ property_change: value,
+ state_change: value,
+ });
};
return (
@@ -39,27 +91,25 @@ export const EmailNotificationForm: FC = () => {
Email notifications
- Get emails to find out what’s going on when you’re not on Plane. You can turn them off anytime
+ Stay in the loop on Issues you are subscribed to. Enable this to get notified.
- (
- onChange(!value)} size="sm" />
- )}
+ updateAllSettings(!notificationSettings)}
+ size="sm"
/>
-
Send me email notifications for:
+
Notify me when:
{/* Notification Settings */}
Property changes
- You’ll be notified about the property changes of an issue you’re a subscriber to you.
+ Notify me when issue’s properties like assignees, priority, estimates or anything else changes.
@@ -81,7 +131,7 @@ export const EmailNotificationForm: FC = () => {
State Change
- You’ll be notified about the state changes to the issues you’re a subscriber to
+ Notify me when the issues moves to a different state
@@ -92,7 +142,10 @@ export const EmailNotificationForm: FC = () => {
onChange(!value)}
+ onChange={() => {
+ if (!value) setValue("issue_completed", true);
+ onChange(!value);
+ }}
className="w-3.5 h-3.5 mx-2 cursor-pointer"
/>
)}
@@ -102,9 +155,7 @@ export const EmailNotificationForm: FC = () => {
Issue completed
-
- We’ll notify you only with the issue is moved to completed state or state group
-
+
Notify me only when an issue is completed
{
Comments
- You will be notified when somebody comments on an issue you’re subscribed to
+ Notify me when someone leaves a comment on the issue
@@ -147,7 +198,7 @@ export const EmailNotificationForm: FC = () => {
Mentions
- You’ll be notified every time someone mentions you in any issue.
+ Notify me only when someone mentions me in the comments or description
@@ -166,11 +217,16 @@ export const EmailNotificationForm: FC = () => {
- {/*
-
+
+
{isSubmitting ? "Saving..." : "Save changes"}
-
*/}
+
>
);
};
diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx
index 3f5cef9cd5..714d8b5558 100644
--- a/web/pages/profile/preferences/email.tsx
+++ b/web/pages/profile/preferences/email.tsx
@@ -1,16 +1,33 @@
import { ReactElement } from "react";
+import useSWR from "swr";
// layouts
import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences";
// components
import { EmailNotificationForm } from "components/profile/preferences";
+// services
+import { UserService } from "services/user.service";
// type
import { NextPageWithLayout } from "lib/types";
-const ProfilePreferencesThemePage: NextPageWithLayout = () => (
-
-
-
-);
+// services
+const userService = new UserService();
+
+const ProfilePreferencesThemePage: NextPageWithLayout = () => {
+ // fetching user email notification settings
+ const { data } = useSWR("CURRENT_USER_EMAIL_NOTIFICATION_SETTINGS", () =>
+ userService.currentUserEmailNotificationSettings()
+ );
+
+ if (!data) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
ProfilePreferencesThemePage.getLayout = function getLayout(page: ReactElement) {
return
{page} ;
diff --git a/web/services/user.service.ts b/web/services/user.service.ts
index 2608b6d17d..71004ed24e 100644
--- a/web/services/user.service.ts
+++ b/web/services/user.service.ts
@@ -10,7 +10,7 @@ import type {
IUserProfileProjectSegregation,
IUserSettings,
IUserWorkspaceDashboard,
- TIssueMap,
+ IUserEmailNotificationSettings,
} from "@plane/types";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
@@ -69,6 +69,14 @@ export class UserService extends APIService {
});
}
+ async currentUserEmailNotificationSettings(): Promise
{
+ return this.get("/api/notification-preferences/")
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
async updateUser(data: Partial): Promise {
return this.patch("/api/users/me/", data)
.then((response) => response?.data)
@@ -97,6 +105,14 @@ export class UserService extends APIService {
});
}
+ async updateCurrentUserEmailNotificationSettings(data: Partial): Promise {
+ return this.patch("/api/notification-preferences/", data)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
async getUserActivity(): Promise {
return this.get(`/api/users/me/activities/`)
.then((response) => response?.data)
From a967fd9cadcb567666d5869bec22295ba9f57615 Mon Sep 17 00:00:00 2001
From: Prateek Shourya
Date: Thu, 18 Jan 2024 21:13:50 +0530
Subject: [PATCH 10/24] chore: remove toggle switch.
---
.../preferences/email-notification-form.tsx | 52 ++-----------------
1 file changed, 4 insertions(+), 48 deletions(-)
diff --git a/web/components/profile/preferences/email-notification-form.tsx b/web/components/profile/preferences/email-notification-form.tsx
index a54deef1a6..ee019e1dde 100644
--- a/web/components/profile/preferences/email-notification-form.tsx
+++ b/web/components/profile/preferences/email-notification-form.tsx
@@ -1,7 +1,7 @@
-import { FC, useEffect, useState } from "react";
+import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
// ui
-import { Button, ToggleSwitch } from "@plane/ui";
+import { Button } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
// services
@@ -18,18 +18,13 @@ const userService = new UserService();
export const EmailNotificationForm: FC = (props) => {
const { data } = props;
- // states
- const [notificationSettings, setNotificationSettings] = useState(false);
- const [isUpdateAllSettings, setIsUpdateAllSettings] = useState(false);
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
- watch,
control,
setValue,
- reset,
formState: { isSubmitting, isDirty, dirtyFields },
} = useForm({
defaultValues: {
@@ -37,20 +32,6 @@ export const EmailNotificationForm: FC = (props) =>
},
});
- useEffect(() => {
- // Update notificationSettings whenever any of the data values change
- setNotificationSettings(
- watch("comment") ||
- watch("issue_completed") ||
- watch("mention") ||
- watch("property_change") ||
- watch("state_change")
- );
- }, [
- // eslint-disable-next-line react-hooks/exhaustive-deps
- watch, watch("comment"), watch("issue_completed"), watch("mention"), watch("property_change"), watch("state_change"),
- ]);
-
const onSubmit = async (formData: IUserEmailNotificationSettings) => {
// Get the dirty fields from the form data and create a payload
let payload = {};
@@ -69,20 +50,7 @@ export const EmailNotificationForm: FC = (props) =>
message: "Email Notification Settings updated successfully",
})
)
- .catch((err) => console.error(err))
- .finally(() => setIsUpdateAllSettings(false));
- };
-
- const updateAllSettings = async (value: boolean) => {
- setNotificationSettings(value);
- setIsUpdateAllSettings(true);
- reset({
- comment: value,
- issue_completed: value,
- mention: value,
- property_change: value,
- state_change: value,
- });
+ .catch((err) => console.error(err));
};
return (
@@ -94,13 +62,6 @@ export const EmailNotificationForm: FC = (props) =>
Stay in the loop on Issues you are subscribed to. Enable this to get notified.
-
- updateAllSettings(!notificationSettings)}
- size="sm"
- />
-
Notify me when:
{/* Notification Settings */}
@@ -218,12 +179,7 @@ export const EmailNotificationForm: FC
= (props) =>
-
+
{isSubmitting ? "Saving..." : "Save changes"}
From 6bebb96c50ce2cae565d2702e6c16ccd1153b4fd Mon Sep 17 00:00:00 2001
From: Ramesh Kumar Chandra
Date: Fri, 19 Jan 2024 15:24:28 +0530
Subject: [PATCH 11/24] chore: added labels part
---
.../emails/notifications/issue-updates.html | 47 +++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index f8a1931779..510ded8101 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -173,6 +173,53 @@
PLW-2
+
+
+
+
+
+
+ Labels :
+
+
+
+
+
+
+ Improvement
+
+
+
+
+ Jinx
+
+
+
+
+ Ready to ship
+
+
+
+
+ Ready to ship
+
+
+
+
+ Feature
+
+
+
+
+ + 5more
+
+
+
+
+
+
+
+
From b3de9e0eb6621ce5f91dfd6baf37ee9848a4ab3a Mon Sep 17 00:00:00 2001
From: LAKHAN BAHETI
Date: Fri, 19 Jan 2024 15:36:47 +0530
Subject: [PATCH 12/24] fix: refactored base design with tables
---
.../emails/notifications/issue-updates.html | 588 +++++++++++-------
1 file changed, 365 insertions(+), 223 deletions(-)
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index 510ded8101..fc94508e9d 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -51,270 +51,412 @@
-
+
-
- PLW-1 updates
-
-
- workspacename/projectname/issueId: Sign up flow
-
+
+
+
+
+ PLW-1 updates
+
+
+ workspacename/projectname/issueId: Sign up flow
+
+
+
+
-
+
+
updates were made to the issue by
Srinivas Pendem
-
-
-
- {{ Updates / Comments }}
-
-
-
-
-
-
- Srinivas Pendem
-
-
- 11:36 am IST
+
+
+
+
+
+
+
+
+
+ {{ Updates / Comments }}
-
-
-
-
-
-
- Assignee :
-
-
- {{ assginee.name }}
-
-
-
- {{ assginee.name }}
-
-
-
-
-
-
- Assignee :
-
-
- {{ assginee.name }}
-
-
-
- +{{length}} more
-
-
-
-
-
-
- Assignee :
-
-
-
-
- {{ assginee.name }}
-
-
-
- {{ assginee.name }}
-
-
- +{{length}} more
-
-
-
-
-
-
-
- Due Date :
-
- 28 Jun, 2023
-
-
-
-
- Duplicate :
-
- PLW-2
-
-
-
-
-
-
-
-
- Labels :
-
-
-
-
-
-
- Improvement
-
-
-
-
- Jinx
-
-
-
-
- Ready to ship
-
-
-
-
- Ready to ship
-
-
-
-
- Feature
-
-
-
-
- + 5more
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Srinivas Pendem
+
+
+
+
+ 11:36 am IST
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Assignee :
+
+
+
+ {{ assginee.name }}
+
+
+
+
+
+
+ {{ assginee.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Due Date :
+
+
+
+ 28 Jun, 2023
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Duplicate :
+
+
+
+ PLW-2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Assignee :
+
+
+
+ {{ assginee.name }}
+
+
+
+ +{{length}} more
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Assignee :
+
+
+
+ {{ assginee.name }}
+
+
+
+
+
+
+ {{ assginee.name }}
+
+
+
+
+ +{{length}} more
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Labels
+ :
+
+
+
+
+
+
+ Improvement
+
+
+
+
+ Jinx
+
+
+
+
+ Ready to ship
+
+
+
+
+ Ready to ship
+
+
+
+
+ Feature
+
+
+
+
+
+ + 5more
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- Bhavesh Raja
-
-
- Connect with folks in finance and operations to verify the data
- provided before presenting it.
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Srinivas Pendem
+
+
+
+
+
+
+ Connect with folks in finance and operations to
+ verify the data
+ provided before presenting it.
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
From a471a031759da287e3d0a990d4a0a3680c17f689 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 13:37:12 +0530
Subject: [PATCH 13/24] dev: email notification templates
---
.../plane/bgtasks/email_notification_task.py | 115 ++++++++--
.../plane/bgtasks/issue_activites_task.py | 35 +--
apiserver/plane/bgtasks/notification_task.py | 200 +++++++++++++-----
.../emails/notifications/issue-updates.html | 124 +++--------
4 files changed, 288 insertions(+), 186 deletions(-)
diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py
index 6f50cc84d8..676f0f7353 100644
--- a/apiserver/plane/bgtasks/email_notification_task.py
+++ b/apiserver/plane/bgtasks/email_notification_task.py
@@ -1,11 +1,16 @@
# Third party imports
from celery import shared_task
+import os
# Django imports
from django.utils import timezone
+from django.core.mail import EmailMultiAlternatives, get_connection
+from django.template.loader import render_to_string
+from django.utils.html import strip_tags
# Module imports
from plane.db.models import EmailNotificationLog, User, Issue
+from plane.license.utils.instance_value import get_email_configuration
@shared_task
@@ -17,7 +22,8 @@ def stack_email_notification():
.values()
)
- # {"issue_id" : { "actor_id": [ { data }, { data } ] }}
+ # Create the below format for each of the issues
+ # {"issue_id" : { "actor_id1": [ { data }, { data } ], "actor_id2": [ { data }, { data } ] }}
# Convert to unique receivers list
receivers = list(
@@ -43,7 +49,9 @@ def stack_email_notification():
for receiver_notification in receiver_notifications:
payload.setdefault(
receiver_notification.get("entity_identifier"), {}
- ).setdefault(str(receiver_notification.get("triggered_by_id")), []).append(
+ ).setdefault(
+ str(receiver_notification.get("triggered_by_id")), []
+ ).append(
receiver_notification.get("data")
)
# append processed notifications
@@ -51,10 +59,10 @@ def stack_email_notification():
email_notification_ids.append(receiver_notification.get("id"))
# Create emails for all the issues
- for issue_id, issue_data in payload.items():
+ for issue_id, notification_data in payload.items():
send_email_notification.delay(
issue_id=issue_id,
- issue_data=issue_data,
+ notification_data=notification_data,
receiver_id=receiver_id,
email_notification_ids=email_notification_ids,
)
@@ -65,10 +73,10 @@ def stack_email_notification():
)
-def create_payload(payload):
- # {"actor_id": { "key": { "old_value": [], "new_value": [] } }}
+def create_payload(notification_data):
+ # return format {"actor_id": { "key": { "old_value": [], "new_value": [] } }}
data = {}
- for actor_id, changes in payload.items():
+ for actor_id, changes in notification_data.items():
for change in changes:
issue_activity = change.get("issue_activity")
if issue_activity: # Ensure issue_activity is not None
@@ -78,9 +86,11 @@ def create_payload(payload):
# Append old_value if it's not empty and not already in the list
if old_value:
- data.setdefault(actor_id, {}).setdefault(field, {}).setdefault(
- "old_value", []
- ).append(old_value) if old_value not in data.setdefault(
+ data.setdefault(actor_id, {}).setdefault(
+ field, {}
+ ).setdefault("old_value", []).append(
+ old_value
+ ) if old_value not in data.setdefault(
actor_id, {}
).setdefault(
field, {}
@@ -90,9 +100,11 @@ def create_payload(payload):
# Append new_value if it's not empty and not already in the list
if new_value:
- data.setdefault(actor_id, {}).setdefault(field, {}).setdefault(
- "new_value", []
- ).append(new_value) if new_value not in data.setdefault(
+ data.setdefault(actor_id, {}).setdefault(
+ field, {}
+ ).setdefault("new_value", []).append(
+ new_value
+ ) if new_value not in data.setdefault(
actor_id, {}
).setdefault(
field, {}
@@ -104,17 +116,32 @@ def create_payload(payload):
@shared_task
-def send_email_notification(issue_id, issue_data, receiver_id, email_notification_ids):
- data = create_payload(payload=issue_data)
+def send_email_notification(
+ issue_id, notification_data, receiver_id, email_notification_ids
+):
+ base_api = "http://localhost:3000"
+ data = create_payload(payload=notification_data)
+
+ # Get email configurations
+ (
+ EMAIL_HOST,
+ EMAIL_HOST_USER,
+ EMAIL_HOST_PASSWORD,
+ EMAIL_PORT,
+ EMAIL_USE_TLS,
+ EMAIL_FROM,
+ ) = get_email_configuration()
receiver = User.objects.get(pk=receiver_id)
issue = Issue.objects.get(pk=issue_id)
template_data = []
+ total_changes = 0
for actor_id, changes in data.items():
actor = User.objects.get(pk=actor_id)
+ total_changes = total_changes + len(changes)
template_data.append(
{
- "actor_details": {
+ "actor_detail": {
"avatar": actor.avatar,
"first_name": actor.first_name,
"last_name": actor.last_name,
@@ -126,7 +153,57 @@ def send_email_notification(issue_id, issue_data, receiver_id, email_notificatio
},
}
)
-
- EmailNotificationLog.objects.filter(pk__in=email_notification_ids).update(
- sent_at=timezone.now()
+
+ summary = ""
+ if len(template_data) == 1:
+ summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} made {total_changes} to the issue"
+ else:
+ summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} and others made {total_changes} to the issue"
+
+ # Send the mail
+ subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
+ context = {
+ "data": template_data,
+ "summary": summary,
+ "issue": {
+ "issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
+ "name": issue.name,
+ },
+ "receiver": {
+ "email": receiver.email,
+ },
+ "issue_unsubscribe": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
+ "user_preference": f"{base_api}/profile/preferences/email"
+ }
+ html_content = render_to_string(
+ "emails/notifications/issue-updates.html", context
)
+ text_content = strip_tags(html_content)
+
+ try:
+ connection = get_connection(
+ host=EMAIL_HOST,
+ port=int(EMAIL_PORT),
+ username=EMAIL_HOST_USER,
+ password=EMAIL_HOST_PASSWORD,
+ use_tls=EMAIL_USE_TLS == "1",
+ )
+
+ msg = EmailMultiAlternatives(
+ subject=subject,
+ body=text_content,
+ from_email=EMAIL_FROM,
+ to=[receiver.email],
+ connection=connection,
+ )
+ msg.attach_alternative(html_content, "text/html")
+ msg.send()
+
+ EmailNotificationLog.objects.filter(
+ pk__in=email_notification_ids
+ ).update(sent_at=timezone.now())
+ print("Email Sent")
+ return
+ except Exception as e:
+ print(e)
+ return
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index 7e8a27862a..e8d0cc4b80 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -111,15 +111,15 @@ def track_parent(
issue_activities,
epoch,
):
- if current_instance.get("parent") != requested_data.get("parent"):
+ if current_instance.get("parent_id") != requested_data.get("parent_id"):
old_parent = (
- Issue.objects.filter(pk=current_instance.get("parent")).first()
- if current_instance.get("parent") is not None
+ Issue.objects.filter(pk=current_instance.get("parent_id")).first()
+ if current_instance.get("parent_id") is not None
else None
)
new_parent = (
- Issue.objects.filter(pk=requested_data.get("parent")).first()
- if requested_data.get("parent") is not None
+ Issue.objects.filter(pk=requested_data.get("parent_id")).first()
+ if requested_data.get("parent_id") is not None
else None
)
@@ -188,9 +188,11 @@ def track_state(
issue_activities,
epoch,
):
- if current_instance.get("state") != requested_data.get("state"):
- new_state = State.objects.get(pk=requested_data.get("state", None))
- old_state = State.objects.get(pk=current_instance.get("state", None))
+ if current_instance.get("state_id") != requested_data.get("state_id"):
+ new_state = State.objects.get(pk=requested_data.get("state_id", None))
+ old_state = State.objects.get(
+ pk=current_instance.get("state_id", None)
+ )
issue_activities.append(
IssueActivity(
@@ -288,10 +290,10 @@ def track_labels(
epoch,
):
requested_labels = set(
- [str(lab) for lab in requested_data.get("labels", [])]
+ [str(lab) for lab in requested_data.get("label_ids", [])]
)
current_labels = set(
- [str(lab) for lab in current_instance.get("labels", [])]
+ [str(lab) for lab in current_instance.get("label_ids", [])]
)
added_labels = requested_labels - current_labels
@@ -350,10 +352,10 @@ def track_assignees(
epoch,
):
requested_assignees = set(
- [str(asg) for asg in requested_data.get("assignees", [])]
+ [str(asg) for asg in requested_data.get("assignee_ids", [])]
)
current_assignees = set(
- [str(asg) for asg in current_instance.get("assignees", [])]
+ [str(asg) for asg in current_instance.get("assignee_ids", [])]
)
added_assignees = requested_assignees - current_assignees
@@ -556,14 +558,14 @@ def update_issue_activity(
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
- "parent": track_parent,
+ "parent_id": track_parent,
"priority": track_priority,
- "state": track_state,
+ "state_id": track_state,
"description_html": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
- "labels": track_labels,
- "assignees": track_assignees,
+ "label_ids": track_labels,
+ "assignee_ids": track_assignees,
"estimate_point": track_estimate_points,
"archived_at": track_archive_at,
"closed_to": track_closed_to,
@@ -578,6 +580,7 @@ def update_issue_activity(
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key)
+ print(key, func)
if func is not None:
func(
requested_data=requested_data,
diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py
index a73d39c206..26dc83f856 100644
--- a/apiserver/plane/bgtasks/notification_task.py
+++ b/apiserver/plane/bgtasks/notification_task.py
@@ -234,19 +234,25 @@ def notifications(
# Get new mentions from the newer instance
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(
- 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
- requested_mentions = extract_mentions(issue_instance=requested_data)
+ requested_mentions = extract_mentions(
+ issue_instance=requested_data
+ )
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,
)
for issue_activity in issue_activities_created:
@@ -268,7 +274,9 @@ def notifications(
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
+ 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
@@ -279,9 +287,13 @@ def notifications(
# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
issue_subscribers = list(
- IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
+ IssueSubscriber.objects.filter(
+ project_id=project_id, issue_id=issue_id
+ )
.exclude(
- subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])
+ subscriber_id__in=list(
+ new_mentions + comment_mentions + [actor_id]
+ )
)
.values_list("subscriber", flat=True)
)
@@ -303,7 +315,9 @@ def notifications(
issue_id=issue_id, project_id=project_id
).values_list("assignee", flat=True)
- issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)})
+ issue_subscribers = list(
+ set(issue_subscribers) - {uuid.UUID(actor_id)}
+ )
for subscriber in issue_subscribers:
if issue.created_by_id and issue.created_by_id == subscriber:
@@ -316,7 +330,9 @@ def notifications(
else:
sender = "in_app:issue_activities:subscribed"
- preference = UserNotificationPreference.objects.get(user_id=subscriber)
+ preference = UserNotificationPreference.objects.get(
+ user_id=subscriber
+ )
for issue_activity in issue_activities_created:
# Do not send notification for description update
@@ -341,7 +357,8 @@ def notifications(
):
send_email = True
elif (
- issue_activity.get("field") == "comment" and preference.comment
+ issue_activity.get("field") == "comment"
+ and preference.comment
):
send_email = True
elif preference.property_change:
@@ -361,6 +378,7 @@ def notifications(
else None
)
+ # Create in app notification
bulk_notifications.append(
Notification(
workspace=project.workspace,
@@ -386,9 +404,15 @@ def notifications(
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
- "actor": str(issue_activity.get("actor_id")),
- "new_value": str(issue_activity.get("new_value")),
- "old_value": str(issue_activity.get("old_value")),
+ "actor": str(
+ issue_activity.get("actor_id")
+ ),
+ "new_value": str(
+ issue_activity.get("new_value")
+ ),
+ "old_value": str(
+ issue_activity.get("old_value")
+ ),
"issue_comment": str(
issue_comment.comment_stripped
if issue_comment is not None
@@ -398,37 +422,55 @@ def notifications(
},
)
)
- bulk_email_logs.append(
- EmailNotificationLog(
- triggered_by_id=actor_id,
- receiver_id=subscriber,
- entity_identifier=issue_id,
- entity_name="issue",
- 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(issue_activity.get("id")),
- "verb": str(issue_activity.get("verb")),
- "field": str(issue_activity.get("field")),
- "actor": str(issue_activity.get("actor_id")),
- "new_value": str(issue_activity.get("new_value")),
- "old_value": str(issue_activity.get("old_value")),
- "issue_comment": str(
- issue_comment.comment_stripped
- if issue_comment is not None
- else ""
- ),
+ # Create email notification
+ if send_email:
+ bulk_email_logs.append(
+ EmailNotificationLog(
+ triggered_by_id=actor_id,
+ receiver_id=subscriber,
+ entity_identifier=issue_id,
+ entity_name="issue",
+ data={
+ "issue": {
+ "id": str(issue_id),
+ "name": str(issue.name),
+ "identifier": str(
+ issue.project.identifier
+ ),
+ "project_id": str(issue.project.id),
+ "workspace_slug": str(
+ issue.project.workspace.slug
+ ),
+ "sequence_id": issue.sequence_id,
+ "state_name": issue.state.name,
+ "state_group": issue.state.group,
+ },
+ "issue_activity": {
+ "id": str(issue_activity.get("id")),
+ "verb": str(
+ issue_activity.get("verb")
+ ),
+ "field": str(
+ issue_activity.get("field")
+ ),
+ "actor": str(
+ issue_activity.get("actor_id")
+ ),
+ "new_value": str(
+ issue_activity.get("new_value")
+ ),
+ "old_value": str(
+ issue_activity.get("old_value")
+ ),
+ "issue_comment": str(
+ issue_comment.comment_stripped
+ if issue_comment is not None
+ else ""
+ ),
+ },
},
- },
+ )
)
- )
# ----------------------------------------------------------------------------------------------------------------- #
@@ -475,14 +517,26 @@ def notifications(
"issue": {
"id": str(issue_id),
"name": str(issue.name),
- "identifier": str(issue.project.identifier),
+ "identifier": str(
+ issue.project.identifier
+ ),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
+ "project_id": str(
+ issue.project.id
+ ),
+ "workspace_slug": str(
+ issue.project.workspace.slug
+ ),
},
"issue_activity": {
- "id": str(issue_activity.get("id")),
- "verb": str(issue_activity.get("verb")),
+ "id": str(
+ issue_activity.get("id")
+ ),
+ "verb": str(
+ issue_activity.get("verb")
+ ),
"field": str("mention"),
"actor": str(
issue_activity.get("actor_id")
@@ -523,18 +577,28 @@ def notifications(
"issue": {
"id": str(issue_id),
"name": str(issue.name),
- "identifier": str(issue.project.identifier),
+ "identifier": str(
+ issue.project.identifier
+ ),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
+ "project_id": str(issue.project.id),
+ "workspace_slug": str(
+ issue.project.workspace.slug
+ ),
},
"issue_activity": {
"id": str(last_activity.id),
"verb": str(last_activity.verb),
"field": str(last_activity.field),
"actor": str(last_activity.actor_id),
- "new_value": str(last_activity.new_value),
- "old_value": str(last_activity.old_value),
+ "new_value": str(
+ last_activity.new_value
+ ),
+ "old_value": str(
+ last_activity.old_value
+ ),
},
},
)
@@ -550,7 +614,9 @@ def notifications(
"issue": {
"id": str(issue_id),
"name": str(issue.name),
- "identifier": str(issue.project.identifier),
+ "identifier": str(
+ issue.project.identifier
+ ),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
@@ -559,9 +625,15 @@ def notifications(
"id": str(last_activity.id),
"verb": str(last_activity.verb),
"field": "mention",
- "actor": str(last_activity.actor_id),
- "new_value": str(last_activity.new_value),
- "old_value": str(last_activity.old_value),
+ "actor": str(
+ last_activity.actor_id
+ ),
+ "new_value": str(
+ last_activity.new_value
+ ),
+ "old_value": str(
+ last_activity.old_value
+ ),
},
},
)
@@ -596,17 +668,27 @@ def notifications(
"state_group": issue.state.group,
},
"issue_activity": {
- "id": str(issue_activity.get("id")),
- "verb": str(issue_activity.get("verb")),
+ "id": str(
+ issue_activity.get("id")
+ ),
+ "verb": str(
+ issue_activity.get("verb")
+ ),
"field": str("mention"),
"actor": str(
- issue_activity.get("actor_id")
+ issue_activity.get(
+ "actor_id"
+ )
),
"new_value": str(
- issue_activity.get("new_value")
+ issue_activity.get(
+ "new_value"
+ )
),
"old_value": str(
- issue_activity.get("old_value")
+ issue_activity.get(
+ "old_value"
+ )
),
},
},
@@ -622,7 +704,9 @@ def notifications(
removed_mention=removed_mention,
)
# Bulk create notifications
- Notification.objects.bulk_create(bulk_notifications, batch_size=100)
+ Notification.objects.bulk_create(
+ bulk_notifications, batch_size=100
+ )
EmailNotificationLog.objects.bulk_create(
bulk_email_logs, batch_size=100, ignore_conflicts=True
)
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index fc94508e9d..66ec71b1e2 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -59,10 +59,10 @@
- PLW-1 updates
+ {{ issue.issue_identifier }} updates
- workspacename/projectname/issueId: Sign up flow
+ {{ issue.name }}
@@ -71,11 +71,7 @@
style="background-color: #F0F0F3; height: 1px; border: 0;margin-top: 15px; margin-bottom: 15px;" />
- updates were made to the issue by
-
- Srinivas Pendem
-
-
+ {{ summary }}
@@ -87,12 +83,13 @@
- {{ Updates / Comments }}
+ Updates
+ {% for update in data %}
@@ -103,14 +100,14 @@
-
- Srinivas Pendem
+ {{ update.actor_detail.first_name }} {{ change.actor_detail.last_name }}
@@ -123,8 +120,9 @@
-
+
+ {% %}
@@ -140,25 +138,30 @@
Assignee :
+
+ {% for assignee in update.changes.assignees.old_value %}
- {{ assginee.name }}
+ {{ assginee }} {% endfor %}
+ {% for assignee in update.changes.assignees.new_value %}
- {{ assginee.name }}
+ {{ assginee }} {% endfor %}
-
+ {% endif %}
+
+ {% if update.changes.target_date %}
@@ -176,14 +179,15 @@
- 28 Jun, 2023
+ { update.changes.target_date.new_value[0] }
-
+ {% endif %}
+ {% if update.changes.duplicate %}
@@ -198,85 +202,18 @@
Duplicate :
-
- PLW-2
+ {{ new_value }}
+ {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
- Assignee :
-
-
-
- {{ assginee.name }}
-
-
-
- +{{length}} more
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Assignee :
-
-
-
- {{ assginee.name }}
-
-
-
-
-
-
- {{ assginee.name }}
-
-
-
-
- +{{length}} more
-
-
-
-
-
-
+ {% endif %}
+ {% if update.changes.labels %}
@@ -338,7 +275,8 @@
-
+ {% endif %}
+ {% endfor %}
@@ -408,12 +346,12 @@
This email was sent to
akhil@plane.so.
+ style="color: #3A5BC7; font-weight: 500; text-decoration: none;">{{ receiver.email }}
If you'd rather not
receive this
kind of
- email,
you can unsubscribe to
- the issue or
manage
+ email, you can unsubscribe to
+ the issue or
manage
your email
preferences .
From ff698a2cac20e062079e8f78849ccd22464643a0 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 13:40:30 +0530
Subject: [PATCH 14/24] dev: template updates
---
apiserver/plane/bgtasks/email_notification_task.py | 2 +-
apiserver/templates/emails/notifications/issue-updates.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py
index 676f0f7353..532dbaaefd 100644
--- a/apiserver/plane/bgtasks/email_notification_task.py
+++ b/apiserver/plane/bgtasks/email_notification_task.py
@@ -120,7 +120,7 @@ def send_email_notification(
issue_id, notification_data, receiver_id, email_notification_ids
):
base_api = "http://localhost:3000"
- data = create_payload(payload=notification_data)
+ data = create_payload(notification_data=notification_data)
# Get email configurations
(
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index 66ec71b1e2..2b0886937e 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -122,7 +122,7 @@
- {% %}
+ {% if update.changes.assignees %}
From 6ed7bff4e1e4fdb735f5ef1f1c1d194e39dab462 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 14:25:15 +0530
Subject: [PATCH 15/24] dev: update models
---
apiserver/plane/app/serializers/__init__.py | 2 +-
.../plane/app/serializers/notification.py | 9 ++-
apiserver/plane/app/views/notification.py | 3 +-
...lnotificationlog_notificationpreference.py | 62 -------------------
.../db/migrations/0056_auto_20240111_1454.py | 30 ---------
.../0057_emailnotificationlog_processed_at.py | 18 ------
apiserver/plane/db/models/__init__.py | 2 +-
apiserver/plane/db/models/notification.py | 35 ++++++++---
apiserver/plane/db/models/user.py | 8 +++
9 files changed, 46 insertions(+), 123 deletions(-)
delete mode 100644 apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py
delete mode 100644 apiserver/plane/db/migrations/0056_auto_20240111_1454.py
delete mode 100644 apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py
diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py
index 094328fffa..b9ee019dd2 100644
--- a/apiserver/plane/app/serializers/__init__.py
+++ b/apiserver/plane/app/serializers/__init__.py
@@ -114,7 +114,7 @@
from .analytic import AnalyticViewSerializer
-from .notification import NotificationSerializer
+from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
from .exporter import ExporterHistorySerializer
diff --git a/apiserver/plane/app/serializers/notification.py b/apiserver/plane/app/serializers/notification.py
index 70d8762418..2152fcf0f9 100644
--- a/apiserver/plane/app/serializers/notification.py
+++ b/apiserver/plane/app/serializers/notification.py
@@ -1,7 +1,7 @@
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
-from plane.db.models import Notification
+from plane.db.models import Notification, UserNotificationPreference
class NotificationSerializer(BaseSerializer):
@@ -12,3 +12,10 @@ class NotificationSerializer(BaseSerializer):
class Meta:
model = Notification
fields = "__all__"
+
+
+class UserNotificationPreferenceSerializer(BaseSerializer):
+
+ class Meta:
+ model = UserNotificationPreference
+ fields = "__all__"
diff --git a/apiserver/plane/app/views/notification.py b/apiserver/plane/app/views/notification.py
index 3112c5b39a..ebe8e50822 100644
--- a/apiserver/plane/app/views/notification.py
+++ b/apiserver/plane/app/views/notification.py
@@ -15,8 +15,9 @@
IssueSubscriber,
Issue,
WorkspaceMember,
+ UserNotificationPreference,
)
-from plane.app.serializers import NotificationSerializer
+from plane.app.serializers import NotificationSerializer, UserNotificationPreferenceSerializer
class NotificationViewSet(BaseViewSet, BasePaginator):
diff --git a/apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py b/apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py
deleted file mode 100644
index 97a507a6e5..0000000000
--- a/apiserver/plane/db/migrations/0054_emailnotificationlog_notificationpreference.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# Generated by Django 4.2.7 on 2024-01-10 08:42
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import plane.db.models.notification
-import uuid
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('db', '0053_auto_20240102_1315'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='EmailNotificationLog',
- fields=[
- ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
- ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
- ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
- ('sent_at', models.DateTimeField(null=True)),
- ('entity', models.CharField(max_length=200)),
- ('old_value', models.CharField(blank=True, max_length=300, null=True)),
- ('new_value', models.CharField(blank=True, max_length=300, null=True)),
- ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
- ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
- ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_logs', to=settings.AUTH_USER_MODEL)),
- ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
- ],
- options={
- 'verbose_name': 'Email Notification Log',
- 'verbose_name_plural': 'Email Notification Logs',
- 'db_table': 'email_notification_logs',
- 'ordering': ('-created_at',),
- },
- ),
- migrations.CreateModel(
- name='NotificationPreference',
- fields=[
- ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
- ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
- ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
- ('created_by', models.JSONField(default=plane.db.models.notification.get_default_preference)),
- ('assigned', models.JSONField(default=plane.db.models.notification.get_default_preference)),
- ('subscribed', models.JSONField(default=plane.db.models.notification.get_default_preference)),
- ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
- ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_preferences', to=settings.AUTH_USER_MODEL)),
- ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
- ],
- options={
- 'verbose_name': 'Notification Preference',
- 'verbose_name_plural': 'Notification Preferences',
- 'db_table': 'notification_preferences',
- 'ordering': ('-created_at',),
- 'unique_together': {('project', 'user')},
- },
- ),
- ]
diff --git a/apiserver/plane/db/migrations/0056_auto_20240111_1454.py b/apiserver/plane/db/migrations/0056_auto_20240111_1454.py
deleted file mode 100644
index a0d0ccfa34..0000000000
--- a/apiserver/plane/db/migrations/0056_auto_20240111_1454.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 4.2.7 on 2024-01-11 14:54
-
-from django.db import migrations
-
-
-def create_notification_preferences(apps, schema_editor):
- UserNotificationPreference = apps.get_model("db", "UserNotificationPreference")
- User = apps.get_model("db", "User")
-
- bulk_notification_preferences = []
- for user_id in User.objects.filter(is_bot=False).values_list("id", flat=True):
- bulk_notification_preferences.append(
- UserNotificationPreference(
- user_id=user_id,
- created_by_id=user_id,
- )
- )
- UserNotificationPreference.objects.bulk_create(
- bulk_notification_preferences, batch_size=1000, ignore_conflicts=True
- )
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("db", "0055_usernotificationpreference_emailnotificationlog"),
- ]
-
- operations = [
- migrations.RunPython(create_notification_preferences),
- ]
diff --git a/apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py b/apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py
deleted file mode 100644
index 3d773dba9b..0000000000
--- a/apiserver/plane/db/migrations/0057_emailnotificationlog_processed_at.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 4.2.7 on 2024-01-16 07:08
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('db', '0056_auto_20240111_1454'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='emailnotificationlog',
- name='processed_at',
- field=models.DateTimeField(null=True),
- ),
- ]
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 09430591f4..d9096bd01f 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -85,7 +85,7 @@
from .analytic import AnalyticView
-from .notification import Notification, NotificationPreference, EmailNotificationLog
+from .notification import Notification, UserNotificationPreference, EmailNotificationLog
from .exporter import ExporterHistory
diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py
index 5f640086f8..b42ae54a92 100644
--- a/apiserver/plane/db/models/notification.py
+++ b/apiserver/plane/db/models/notification.py
@@ -67,26 +67,43 @@ def get_default_preference():
class UserNotificationPreference(BaseModel):
- created = models.JSONField(default=get_default_preference)
- assigned = models.JSONField(default=get_default_preference)
- subscribed = models.JSONField(default=get_default_preference)
+ # user it is related to
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notification_preferences",
)
+ # workspace if it is applicable
+ workspace = models.ForeignKey(
+ "db.Workspace",
+ on_delete=models.CASCADE,
+ related_name="workspace_notification_preferences",
+ null=True,
+ )
+ # project
+ project = models.ForeignKey(
+ "db.Project",
+ on_delete=models.CASCADE,
+ related_name="project_notification_preferences",
+ null=True,
+ )
- class Meta:
- unique_together = ["project", "user"]
+ # preference fields
+ property_change = models.BooleanField(default=True)
+ state_change = models.BooleanField(default=True)
+ comment = models.BooleanField(default=True)
+ mention = models.BooleanField(default=True)
+ issue_completed = models.BooleanField(default=True)
- verbose_name = "Notification Preference"
- verbose_name_plural = "Notification Preferences"
+ class Meta:
+ verbose_name = "UserNotificationPreference"
+ verbose_name_plural = "UserNotificationPreferences"
db_table = "user_notification_preferences"
ordering = ("-created_at",)
def __str__(self):
- """Return name of the notifications"""
- return f"{self.user.email} <{self.workspace.name}>"
+ """Return the user"""
+ return f"<{self.user}>"
class EmailNotificationLog(BaseModel):
# receiver
diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py
index cf9b9f68a8..6f8a82e567 100644
--- a/apiserver/plane/db/models/user.py
+++ b/apiserver/plane/db/models/user.py
@@ -11,8 +11,16 @@
UserManager,
PermissionsMixin,
)
+from django.db.models.signals import post_save
+from django.conf import settings
+from django.dispatch import receiver
from django.utils import timezone
+# Third party imports
+from sentry_sdk import capture_exception
+from slack_sdk import WebClient
+from slack_sdk.errors import SlackApiError
+
def get_default_onboarding():
return {
From 7aeffac288029ec9ab78e7dbcb4daa2c4f91c027 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 14:55:00 +0530
Subject: [PATCH 16/24] dev: update template for labels and new migrations
---
.../plane/bgtasks/email_notification_task.py | 4 +-
.../plane/bgtasks/issue_activites_task.py | 3 +-
...ficationpreference_emailnotificationlog.py | 184 ++++++++++++++++++
.../db/migrations/0057_auto_20240122_0901.py | 28 +++
.../emails/notifications/issue-updates.html | 40 +---
5 files changed, 224 insertions(+), 35 deletions(-)
create mode 100644 apiserver/plane/db/migrations/0056_usernotificationpreference_emailnotificationlog.py
create mode 100644 apiserver/plane/db/migrations/0057_auto_20240122_0901.py
diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py
index 532dbaaefd..809bd7b975 100644
--- a/apiserver/plane/bgtasks/email_notification_task.py
+++ b/apiserver/plane/bgtasks/email_notification_task.py
@@ -156,9 +156,9 @@ def send_email_notification(
summary = ""
if len(template_data) == 1:
- summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} made {total_changes} to the issue"
+ summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} made {total_changes} changes to the issue"
else:
- summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} and others made {total_changes} to the issue"
+ summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} and others made {total_changes} changes to the issue"
# Send the mail
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index e8d0cc4b80..e93bf1c5db 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -24,6 +24,7 @@
IssueReaction,
CommentReaction,
IssueComment,
+ IssueSubscriber,
)
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
@@ -361,6 +362,7 @@ def track_assignees(
added_assignees = requested_assignees - current_assignees
dropped_assginees = current_assignees - requested_assignees
+ bulk_subscribers = []
for added_asignee in added_assignees:
assignee = User.objects.get(pk=added_asignee)
issue_activities.append(
@@ -580,7 +582,6 @@ def update_issue_activity(
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key)
- print(key, func)
if func is not None:
func(
requested_data=requested_data,
diff --git a/apiserver/plane/db/migrations/0056_usernotificationpreference_emailnotificationlog.py b/apiserver/plane/db/migrations/0056_usernotificationpreference_emailnotificationlog.py
new file mode 100644
index 0000000000..2e6645945a
--- /dev/null
+++ b/apiserver/plane/db/migrations/0056_usernotificationpreference_emailnotificationlog.py
@@ -0,0 +1,184 @@
+# Generated by Django 4.2.7 on 2024-01-22 08:55
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("db", "0055_auto_20240108_0648"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="UserNotificationPreference",
+ fields=[
+ (
+ "created_at",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="Created At"
+ ),
+ ),
+ (
+ "updated_at",
+ models.DateTimeField(
+ auto_now=True, verbose_name="Last Modified At"
+ ),
+ ),
+ (
+ "id",
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ unique=True,
+ ),
+ ),
+ ("property_change", models.BooleanField(default=True)),
+ ("state_change", models.BooleanField(default=True)),
+ ("comment", models.BooleanField(default=True)),
+ ("mention", models.BooleanField(default=True)),
+ ("issue_completed", models.BooleanField(default=True)),
+ (
+ "created_by",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="%(class)s_created_by",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Created By",
+ ),
+ ),
+ (
+ "project",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="project_notification_preferences",
+ to="db.project",
+ ),
+ ),
+ (
+ "updated_by",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="%(class)s_updated_by",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Last Modified By",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="notification_preferences",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "workspace",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="workspace_notification_preferences",
+ to="db.workspace",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "UserNotificationPreference",
+ "verbose_name_plural": "UserNotificationPreferences",
+ "db_table": "user_notification_preferences",
+ "ordering": ("-created_at",),
+ },
+ ),
+ migrations.CreateModel(
+ name="EmailNotificationLog",
+ fields=[
+ (
+ "created_at",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="Created At"
+ ),
+ ),
+ (
+ "updated_at",
+ models.DateTimeField(
+ auto_now=True, verbose_name="Last Modified At"
+ ),
+ ),
+ (
+ "id",
+ models.UUIDField(
+ db_index=True,
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ unique=True,
+ ),
+ ),
+ ("entity_identifier", models.UUIDField(null=True)),
+ ("entity_name", models.CharField(max_length=255)),
+ ("data", models.JSONField(null=True)),
+ ("processed_at", models.DateTimeField(null=True)),
+ ("sent_at", models.DateTimeField(null=True)),
+ ("entity", models.CharField(max_length=200)),
+ (
+ "old_value",
+ models.CharField(blank=True, max_length=300, null=True),
+ ),
+ (
+ "new_value",
+ models.CharField(blank=True, max_length=300, null=True),
+ ),
+ (
+ "created_by",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="%(class)s_created_by",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Created By",
+ ),
+ ),
+ (
+ "receiver",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="email_notifications",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "triggered_by",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="triggered_emails",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "updated_by",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="%(class)s_updated_by",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Last Modified By",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Email Notification Log",
+ "verbose_name_plural": "Email Notification Logs",
+ "db_table": "email_notification_logs",
+ "ordering": ("-created_at",),
+ },
+ ),
+ ]
diff --git a/apiserver/plane/db/migrations/0057_auto_20240122_0901.py b/apiserver/plane/db/migrations/0057_auto_20240122_0901.py
new file mode 100644
index 0000000000..9204d43b3f
--- /dev/null
+++ b/apiserver/plane/db/migrations/0057_auto_20240122_0901.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.7 on 2024-01-22 09:01
+
+from django.db import migrations
+
+def create_notification_preferences(apps, schema_editor):
+ UserNotificationPreference = apps.get_model("db", "UserNotificationPreference")
+ User = apps.get_model("db", "User")
+
+ bulk_notification_preferences = []
+ for user_id in User.objects.filter(is_bot=False).values_list("id", flat=True):
+ bulk_notification_preferences.append(
+ UserNotificationPreference(
+ user_id=user_id,
+ created_by_id=user_id,
+ )
+ )
+ UserNotificationPreference.objects.bulk_create(
+ bulk_notification_preferences, batch_size=1000, ignore_conflicts=True
+ )
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("db", "0056_usernotificationpreference_emailnotificationlog"),
+ ]
+
+ operations = [
+ migrations.RunPython(create_notification_preferences)
+ ]
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index 2b0886937e..aae2cfb92b 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -230,41 +230,14 @@
+ {% for label in update.changes.labels.new_value %}
- Improvement
-
-
-
-
- Jinx
-
-
-
-
- Ready to ship
-
-
-
-
- Ready to ship
-
-
-
-
- Feature
+ {{ label }}
-
-
-
- + 5more
+ {% endfor %}
@@ -276,8 +249,9 @@
{% endif %}
- {% endfor %}
+ {% if update.changes.comment %}
+ { for comment in update.changes.comment }
@@ -292,7 +266,7 @@
- Srinivas Pendem
+
@@ -313,6 +287,8 @@
+ {% endif %}
+ {% endfor %}
From d269c31d205d40b2170aa44bfae41066526b0488 Mon Sep 17 00:00:00 2001
From: Prateek Shourya
Date: Mon, 22 Jan 2024 14:28:00 +0530
Subject: [PATCH 17/24] fix: profile settings preference sidebar.
---
web/constants/profile.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/constants/profile.ts b/web/constants/profile.ts
index 3f17bb329d..063bb7e440 100644
--- a/web/constants/profile.ts
+++ b/web/constants/profile.ts
@@ -33,7 +33,7 @@ export const PROFILE_ACTION_LINKS: {
{
key: "preferences",
label: "Preferences",
- href: `/profile/preferences`,
+ href: `/profile/preferences/theme`,
highlight: (pathname: string) => pathname.includes("/profile/preferences"),
Icon: Settings2,
},
From 02531f6ed8bcaca40e78e4f36462ef299d95763f Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 15:06:45 +0530
Subject: [PATCH 18/24] dev: update preference endpoints
---
apiserver/plane/app/urls/notification.py | 6 ++++++
apiserver/plane/app/views/__init__.py | 1 +
web/services/user.service.ts | 4 ++--
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/apiserver/plane/app/urls/notification.py b/apiserver/plane/app/urls/notification.py
index 0c96e5f15b..0bbf4f3c79 100644
--- a/apiserver/plane/app/urls/notification.py
+++ b/apiserver/plane/app/urls/notification.py
@@ -5,6 +5,7 @@
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
+ UserNotificationPreferenceEndpoint,
)
@@ -63,4 +64,9 @@
),
name="mark-all-read-notifications",
),
+ path(
+ "users/me/notification-preferences/",
+ UserNotificationPreferenceEndpoint.as_view(),
+ name="user-notification-preferences",
+ ),
]
diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py
index d3c4f4baf4..457738fe31 100644
--- a/apiserver/plane/app/views/__init__.py
+++ b/apiserver/plane/app/views/__init__.py
@@ -165,6 +165,7 @@
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
+ UserNotificationPreferenceEndpoint,
)
from .exporter import ExportIssuesEndpoint
diff --git a/web/services/user.service.ts b/web/services/user.service.ts
index 71004ed24e..13ffa9c51e 100644
--- a/web/services/user.service.ts
+++ b/web/services/user.service.ts
@@ -70,7 +70,7 @@ export class UserService extends APIService {
}
async currentUserEmailNotificationSettings(): Promise {
- return this.get("/api/notification-preferences/")
+ return this.get("/api/users/me/notification-preferences/")
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
@@ -106,7 +106,7 @@ export class UserService extends APIService {
}
async updateCurrentUserEmailNotificationSettings(data: Partial): Promise {
- return this.patch("/api/notification-preferences/", data)
+ return this.patch("/api/users/me/notification-preferences/", data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
From 73e84224fb92852316dad787ed9b05069b7eb46e Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 15:25:35 +0530
Subject: [PATCH 19/24] dev: update the schedule to 5 minutes
---
apiserver/plane/celery.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py
index 6872eb43c3..0912e276af 100644
--- a/apiserver/plane/celery.py
+++ b/apiserver/plane/celery.py
@@ -31,7 +31,7 @@
},
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
- "schedule": timedelta(seconds=10)
+ "schedule": crontab(minute='*/5')
},
}
From 5b7787550bf42c9f94415b8cf42914de27a1c455 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 19:21:37 +0530
Subject: [PATCH 20/24] dev: update template with priority data
---
.../plane/bgtasks/email_notification_task.py | 56 +-
apiserver/plane/bgtasks/notification_task.py | 1 +
.../emails/notifications/issue-updates.html | 1237 ++++++++++++-----
3 files changed, 923 insertions(+), 371 deletions(-)
diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py
index 809bd7b975..c6b7f61cfc 100644
--- a/apiserver/plane/bgtasks/email_notification_task.py
+++ b/apiserver/plane/bgtasks/email_notification_task.py
@@ -1,12 +1,15 @@
+import json
+from datetime import datetime
+
# Third party imports
from celery import shared_task
-import os
# Django imports
from django.utils import timezone
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
+from django.conf import settings
# Module imports
from plane.db.models import EmailNotificationLog, User, Issue
@@ -81,8 +84,8 @@ def create_payload(notification_data):
issue_activity = change.get("issue_activity")
if issue_activity: # Ensure issue_activity is not None
field = issue_activity.get("field")
- old_value = issue_activity.get("old_value")
- new_value = issue_activity.get("new_value")
+ old_value = str(issue_activity.get("old_value"))
+ new_value = str(issue_activity.get("new_value"))
# Append old_value if it's not empty and not already in the list
if old_value:
@@ -112,6 +115,13 @@ def create_payload(notification_data):
"new_value", []
) else None
+ if not data.get("actor_id", {}).get("activity_time", False):
+ data[actor_id]["activity_time"] = str(
+ datetime.fromisoformat(
+ issue_activity.get("activity_time").rstrip("Z")
+ ).strftime("%Y-%m-%d %H:%M:%S")
+ )
+
return data
@@ -119,7 +129,7 @@ def create_payload(notification_data):
def send_email_notification(
issue_id, notification_data, receiver_id, email_notification_ids
):
- base_api = "http://localhost:3000"
+ base_api = "https://app.plane.so"
data = create_payload(notification_data=notification_data)
# Get email configurations
@@ -136,13 +146,28 @@ def send_email_notification(
issue = Issue.objects.get(pk=issue_id)
template_data = []
total_changes = 0
+ comments = []
for actor_id, changes in data.items():
actor = User.objects.get(pk=actor_id)
total_changes = total_changes + len(changes)
+ comment = changes.pop("comment", False)
+ if comment:
+ comments.append(
+ {
+ "actor_comments": comment,
+ "actor_detail": {
+ "avatar_url": actor.avatar,
+ "first_name": actor.first_name,
+ "last_name": actor.last_name,
+ },
+ "activity_time": str(activity_time),
+ }
+ )
+ activity_time = changes.pop("activity_time")
template_data.append(
{
"actor_detail": {
- "avatar": actor.avatar,
+ "avatar_url": actor.avatar,
"first_name": actor.first_name,
"last_name": actor.last_name,
},
@@ -151,14 +176,20 @@ def send_email_notification(
"name": issue.name,
"identifier": f"{issue.project.identifier}-{issue.sequence_id}",
},
+ "activity_time": str(activity_time),
}
)
- summary = ""
- if len(template_data) == 1:
- summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} made {total_changes} changes to the issue"
- else:
- summary = f"{template_data[0]['actor_detail']['first_name']} {template_data[0]['actor_detail']['last_name']} and others made {total_changes} changes to the issue"
+ span = f""""""
+
+ summary = "updates were made to the issue by"
# Send the mail
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
@@ -168,13 +199,16 @@ def send_email_notification(
"issue": {
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
"name": issue.name,
+ "issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
},
"receiver": {
"email": receiver.email,
},
"issue_unsubscribe": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
- "user_preference": f"{base_api}/profile/preferences/email"
+ "user_preference": f"{base_api}/profile/preferences/email",
+ "comments": comments,
}
+ print(json.dumps(context))
html_content = render_to_string(
"emails/notifications/issue-updates.html", context
)
diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py
index 26dc83f856..bcd0f85437 100644
--- a/apiserver/plane/bgtasks/notification_task.py
+++ b/apiserver/plane/bgtasks/notification_task.py
@@ -467,6 +467,7 @@ def notifications(
if issue_comment is not None
else ""
),
+ "activity_time": issue_activity.get("created_at"),
},
},
)
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index aae2cfb92b..047c9c7e12 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -1,383 +1,900 @@
-
-
-
+
+
- Your unique Plane login code is code
+ Updates on Issue
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plane
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- Plane
-
-
-
+
+
+ {{ issue.identifier }} updates
+
+
+ {{ issue.name }}
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ issue.issue_identifier }} updates
-
-
- {{ issue.name }}
-
-
-
-
-
-
-
- {{ summary }}
-
-
-
-
-
-
-
-
-
- Updates
-
-
-
+
+
+
+ {{ summary }}
+
+ {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }}
+
+
+
+
+
+
+
+
+ Updates
+
+
+
+
+ {% for update in data %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ update.actor_detail.first_name }} {{ update.actor_detail.last_name }}
+
+
+
+
+ {{ update.activity_time }}
+
+
+
+
+
+
+
+ {% if update.changes.assignee %}
+
+
+
+
+
+
+
+ Assignee:
+
+
+ {% for assignee in update.changes.assignee.new_value %}
+
+
+ {{ assignee }}
+
+
+ {% endfor %}
+
+
+
+
+
+ {{ assginee.name }}
+
+
+
+
+
+
+ {% endif %}
+
+ {% if update.changes.target_date %}
+
+
+
+
+
+
+
+
+
+ Due Date:
+
+
+
+
+ {{ update.changes.target_date.new_value.0 }}
+
+
+
+
+
+
+ {% endif %} -->
+
+ {% if update.changes.duplicate %}
+
+
+
+
+
+
+
+
+
+ Duplicate:
+
+
+ {% for dup in update.changes.duplicate.new_value %}
+
+
+ {{ dup }}
+
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+ {% if update.changes.labels %}
+
+
+
+
+
+
+
+
+
+ Labels:
+
+
-
- {% for update in data %}
-
+
+
+
+ {% for label in update.changes.labels.new_value %}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ update.actor_detail.first_name }} {{ change.actor_detail.last_name }}
-
-
-
-
- 11:36 am IST
-
-
-
-
-
-
-
-
- {% if update.changes.assignees %}
-
-
-
-
-
-
-
-
-
-
- Assignee :
-
-
-
- {% for assignee in update.changes.assignees.old_value %}
-
- {{ assginee }}
{% endfor %}
-
-
-
-
-
- {% for assignee in update.changes.assignees.new_value %}
-
- {{ assginee }}
{% endfor %}
-
-
-
-
-
-
- {% endif %}
-
- {% if update.changes.target_date %}
-
-
-
-
-
-
-
-
-
-
- Due Date :
-
-
-
- { update.changes.target_date.new_value[0] }
-
-
-
-
-
- {% endif %}
-
- {% if update.changes.duplicate %}
-
-
-
-
-
-
-
-
-
- Duplicate :
-
-
- {% for new_value in update.changes.duplicate.new_value %}
-
- {{ new_value }}
- {% endfor %}
-
-
-
-
-
- {% endif %}
- {% if update.changes.labels %}
-
-
-
-
-
-
-
- Labels
- :
-
-
-
-
- {% for label in update.changes.labels.new_value %}
-
-
- {{ label }}
-
-
- {% endfor %}
-
-
-
-
-
-
-
-
+
+ {{ label }}
+
-
- {% endif %}
- {% if update.changes.comment %}
-
- { for comment in update.changes.comment }
+ {% endfor %}
+
+
+
+
+
+
+
+ {% endif %}
+
+ {% if update.changes.state %}
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Connect with folks in finance and operations to
- verify the data
- provided before presenting it.
-
-
+
+
+
+
+
+ State:
+
+
+
+
+ {{ update.changes.state.old_value.0 }}
+
+
+ ->
-
-
-
-
-
-
+
+
+ {{ update.changes.state.new_value.0 }}
+
+
- {% endif %}
- {% endfor %}
-
-
+
+
+
+
+
+ {% endif %}
+
+ {% if update.changes.link %}
+
+
+
+
+
+ {% endif %}
+
+ {% if update.changes.priority %}
+
+
+
+
+
+
+
+
+
+ Priority:
+
+
+
+
+ {{ update.changes.priority.old_value.0 }}
+
+
+
+ ->
+
+
+
+ {{ update.changes.priority.new_value.0 }}
+
+
+
+
+
+
+ {% endif %}
+
+ {% if update.changes.blocking %}
+
+
+
+
+
+
+
+
+
+ Blocking:
+
+
+ {% for bl in update.changes.blocking.new_value %}
+
+
+ {{ bl }}
+
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+
+
+ {% endfor %}
+
+
+ {% if comments %}
+
+
+
+
+
+ Comments
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ S
+
+
+
+ {% for comment in comments %}
+
+
+
+
+
+ {{ comment.actor_detail.first_name }} {{
+ comment.actor_detail.last_name }}
+
+
+
+ {% for actor_comment in comment.actor_comments %}
+
+
+
+ {{ actor_comment.comment }}
+
+
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ View issue
+
+
+
+
+
+
+
+
-
+
-
+
-
-
+
-
-
-
\ No newline at end of file
+
+
From e2f54db65c866d00381c74fffe4028c74161787c Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Mon, 22 Jan 2024 20:38:01 +0530
Subject: [PATCH 21/24] dev: update templates
---
.../emails/notifications/issue-updates.html | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index 047c9c7e12..99470535f9 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -209,7 +209,7 @@
- {% if update.changes.assignee %}
+ {% if update.changes.assignees %}
- {% for assignee in update.changes.assignee.new_value %}
+ {% for assignee in update.changes.assignees.old_value %}
+ {% endif %}
+ {% for assignee in update.changes.assignees.new_value %}
- {{ assginee.name }}
+ {{ assginee }}
+ {% endfor %}
@@ -754,8 +758,7 @@
margin-left: 8px;
"
>
- {{ comment.actor_detail.first_name }} {{
- comment.actor_detail.last_name }}
+ {{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }}
@@ -777,7 +780,7 @@
border-radius: 4px;
"
>
- {{ actor_comment.comment }}
+ {{ actor_comment.new_value }}
From 73d9ff8133fb1142f8d025f7443eb902b8ec710a Mon Sep 17 00:00:00 2001
From: Prateek Shourya
Date: Tue, 23 Jan 2024 12:28:15 +0530
Subject: [PATCH 22/24] chore: enable `issue subscribe` button for all users.
---
web/components/issues/issue-detail/sidebar.tsx | 7 +------
web/components/issues/issue-detail/subscription.tsx | 7 +------
web/components/issues/peek-overview/view.tsx | 7 +------
3 files changed, 3 insertions(+), 18 deletions(-)
diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx
index 6b249f4bd5..b4f8ac9976 100644
--- a/web/components/issues/issue-detail/sidebar.tsx
+++ b/web/components/issues/issue-detail/sidebar.tsx
@@ -154,12 +154,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
{currentUser && !is_archived && (fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
-
+
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx
index d20bc53cf6..b57e75beda 100644
--- a/web/components/issues/issue-detail/subscription.tsx
+++ b/web/components/issues/issue-detail/subscription.tsx
@@ -11,14 +11,12 @@ export type TIssueSubscription = {
workspaceSlug: string;
projectId: string;
issueId: string;
- currentUserId: string;
};
export const IssueSubscription: FC
= observer((props) => {
- const { workspaceSlug, projectId, issueId, currentUserId } = props;
+ const { workspaceSlug, projectId, issueId } = props;
// hooks
const {
- issue: { getIssueById },
subscription: { getSubscriptionByIssueId },
createSubscription,
removeSubscription,
@@ -27,7 +25,6 @@ export const IssueSubscription: FC = observer((props) => {
// state
const [loading, setLoading] = useState(false);
- const issue = getIssueById(issueId);
const subscription = getSubscriptionByIssueId(issueId);
const handleSubscription = async () => {
@@ -51,8 +48,6 @@ export const IssueSubscription: FC = observer((props) => {
}
};
- if (issue?.created_by === currentUserId || issue?.assignee_ids?.includes(currentUserId)) return <>>;
-
return (
= observer((props) => {
{currentUser && !is_archived && (
-
+
)}
From 11923417f62702d45897707717ba5b1f23a6734a Mon Sep 17 00:00:00 2001
From: NarayanBavisetti
Date: Tue, 23 Jan 2024 16:16:17 +0530
Subject: [PATCH 23/24] chore: notification handling for external api
---
apiserver/plane/app/views/cycle.py | 3 ++
apiserver/plane/app/views/inbox.py | 2 ++
apiserver/plane/app/views/issue.py | 22 +++++++++++++
apiserver/plane/app/views/module.py | 3 ++
.../plane/bgtasks/issue_activites_task.py | 33 ++++++++++---------
.../plane/bgtasks/issue_automation_task.py | 2 ++
6 files changed, 50 insertions(+), 15 deletions(-)
diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py
index 2d459d15b8..08a0c7697b 100644
--- a/apiserver/plane/app/views/cycle.py
+++ b/apiserver/plane/app/views/cycle.py
@@ -530,6 +530,7 @@ def destroy(self, request, slug, project_id, pk):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
# Delete the cycle
cycle.delete()
@@ -721,6 +722,7 @@ def create(self, request, slug, project_id, cycle_id):
}
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
# Return all Cycle Issues
@@ -753,6 +755,7 @@ def destroy(self, request, slug, project_id, cycle_id, issue_id):
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
cycle_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py
index ff88bfdab5..8cbf949121 100644
--- a/apiserver/plane/app/views/inbox.py
+++ b/apiserver/plane/app/views/inbox.py
@@ -200,6 +200,7 @@ def create(self, request, slug, project_id, inbox_id):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
# create an inbox issue
InboxIssue.objects.create(
@@ -277,6 +278,7 @@ def partial_update(self, request, slug, project_id, inbox_id, issue_id):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
issue_serializer.save()
else:
diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py
index ec8b4da5ef..1119990f40 100644
--- a/apiserver/plane/app/views/issue.py
+++ b/apiserver/plane/app/views/issue.py
@@ -260,6 +260,7 @@ def create(self, request, slug, project_id):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
issue = (
self.get_queryset().filter(pk=serializer.data["id"]).first()
@@ -298,6 +299,7 @@ def partial_update(self, request, slug, project_id, pk=None):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
issue = self.get_queryset().filter(pk=pk).first()
return Response(
@@ -321,6 +323,7 @@ def destroy(self, request, slug, project_id, pk=None):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -580,6 +583,7 @@ def create(self, request, slug, project_id, issue_id):
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -609,6 +613,7 @@ def partial_update(self, request, slug, project_id, issue_id, pk):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -633,6 +638,7 @@ def destroy(self, request, slug, project_id, issue_id, pk):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -833,6 +839,7 @@ def post(self, request, slug, project_id, issue_id):
project_id=str(project_id),
current_instance=json.dumps({"parent": str(sub_issue_id)}),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
for sub_issue_id in sub_issue_ids
]
@@ -893,6 +900,7 @@ def create(self, request, slug, project_id, issue_id):
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -922,6 +930,7 @@ def partial_update(self, request, slug, project_id, issue_id, pk):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -945,6 +954,7 @@ def destroy(self, request, slug, project_id, issue_id, pk):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
issue_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1001,6 +1011,7 @@ def post(self, request, slug, project_id, issue_id):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1017,6 +1028,7 @@ def delete(self, request, slug, project_id, issue_id, pk):
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1195,6 +1207,7 @@ def unarchive(self, request, slug, project_id, pk=None):
IssueSerializer(issue).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
issue.archived_at = None
issue.save()
@@ -1340,6 +1353,7 @@ def create(self, request, slug, project_id, issue_id):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1365,6 +1379,7 @@ def destroy(self, request, slug, project_id, issue_id, reaction_code):
}
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1405,6 +1420,7 @@ def create(self, request, slug, project_id, comment_id):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1431,6 +1447,7 @@ def destroy(self, request, slug, project_id, comment_id, reaction_code):
}
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1559,6 +1576,7 @@ def create(self, request, slug, project_id, issue_id):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
if relation_type == "blocking":
@@ -1603,6 +1621,7 @@ def remove_relation(self, request, slug, project_id, issue_id):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1774,6 +1793,7 @@ def create(self, request, slug, project_id):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1804,6 +1824,7 @@ def partial_update(self, request, slug, project_id, pk):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1830,5 +1851,6 @@ def destroy(self, request, slug, project_id, pk=None):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py
index 09d763ab75..58be79ca02 100644
--- a/apiserver/plane/app/views/module.py
+++ b/apiserver/plane/app/views/module.py
@@ -310,6 +310,7 @@ def destroy(self, request, slug, project_id, pk):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
module.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -488,6 +489,7 @@ def create(self, request, slug, project_id, module_id):
}
),
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
issues = self.get_queryset().values_list("issue_id", flat=True)
@@ -519,6 +521,7 @@ def destroy(self, request, slug, project_id, module_id, issue_id):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
module_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index e93bf1c5db..c2ad48f231 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -1562,6 +1562,7 @@ def issue_activity(
project_id,
epoch,
subscriber=True,
+ notification=False,
):
try:
issue_activities = []
@@ -1642,22 +1643,24 @@ def issue_activity(
)
except Exception as e:
capture_exception(e)
+
- notifications.delay(
- type=type,
- issue_id=issue_id,
- actor_id=actor_id,
- project_id=project_id,
- subscriber=subscriber,
- issue_activities_created=json.dumps(
- IssueActivitySerializer(
- issue_activities_created, many=True
- ).data,
- cls=DjangoJSONEncoder,
- ),
- requested_data=requested_data,
- current_instance=current_instance,
- )
+ if notification:
+ notifications.delay(
+ type=type,
+ issue_id=issue_id,
+ actor_id=actor_id,
+ project_id=project_id,
+ subscriber=subscriber,
+ issue_activities_created=json.dumps(
+ IssueActivitySerializer(
+ issue_activities_created, many=True
+ ).data,
+ cls=DjangoJSONEncoder,
+ ),
+ requested_data=requested_data,
+ current_instance=current_instance,
+ )
return
except Exception as e:
diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py
index 718b60355e..974a545fcd 100644
--- a/apiserver/plane/bgtasks/issue_automation_task.py
+++ b/apiserver/plane/bgtasks/issue_automation_task.py
@@ -87,6 +87,7 @@ def archive_old_issues():
current_instance=json.dumps({"archived_at": None}),
subscriber=False,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
for issue in issues_to_update
]
@@ -169,6 +170,7 @@ def close_old_issues():
current_instance=None,
subscriber=False,
epoch=int(timezone.now().timestamp()),
+ notification=True,
)
for issue in issues_to_update
]
From b82e20e93bcc48c8ef9eda6eb7352698bd65b0e7 Mon Sep 17 00:00:00 2001
From: pablohashescobar
Date: Tue, 23 Jan 2024 17:41:03 +0530
Subject: [PATCH 24/24] dev: update origin request
---
apiserver/plane/app/views/cycle.py | 3 +++
apiserver/plane/app/views/inbox.py | 2 ++
apiserver/plane/app/views/issue.py | 24 ++++++++++++++++++-
apiserver/plane/app/views/module.py | 3 +++
.../plane/bgtasks/email_notification_task.py | 6 ++---
.../plane/bgtasks/issue_activites_task.py | 7 +++++-
apiserver/plane/celery.py | 2 +-
.../emails/notifications/issue-updates.html | 2 +-
8 files changed, 42 insertions(+), 7 deletions(-)
diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py
index 08a0c7697b..3c54f7f95a 100644
--- a/apiserver/plane/app/views/cycle.py
+++ b/apiserver/plane/app/views/cycle.py
@@ -531,6 +531,7 @@ def destroy(self, request, slug, project_id, pk):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
# Delete the cycle
cycle.delete()
@@ -723,6 +724,7 @@ def create(self, request, slug, project_id, cycle_id):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
# Return all Cycle Issues
@@ -756,6 +758,7 @@ def destroy(self, request, slug, project_id, cycle_id, issue_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
cycle_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py
index 8cbf949121..0f8e68656a 100644
--- a/apiserver/plane/app/views/inbox.py
+++ b/apiserver/plane/app/views/inbox.py
@@ -201,6 +201,7 @@ def create(self, request, slug, project_id, inbox_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
# create an inbox issue
InboxIssue.objects.create(
@@ -279,6 +280,7 @@ def partial_update(self, request, slug, project_id, inbox_id, issue_id):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
issue_serializer.save()
else:
diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py
index 901830cce4..5ea02e40e9 100644
--- a/apiserver/plane/app/views/issue.py
+++ b/apiserver/plane/app/views/issue.py
@@ -260,6 +260,7 @@ def create(self, request, slug, project_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
issue = (
self.get_queryset().filter(pk=serializer.data["id"]).first()
@@ -299,6 +300,7 @@ def partial_update(self, request, slug, project_id, pk=None):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
issue = self.get_queryset().filter(pk=pk).first()
return Response(
@@ -323,6 +325,7 @@ def destroy(self, request, slug, project_id, pk=None):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -531,7 +534,7 @@ def get(self, request, slug, project_id, issue_id):
if request.GET.get("activity_type", None) == "issue-property":
return Response(issue_activities, status=status.HTTP_200_OK)
-
+
if request.GET.get("activity_type", None) == "issue-comment":
return Response(issue_comments, status=status.HTTP_200_OK)
@@ -599,6 +602,7 @@ def create(self, request, slug, project_id, issue_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -629,6 +633,7 @@ def partial_update(self, request, slug, project_id, issue_id, pk):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -654,6 +659,7 @@ def destroy(self, request, slug, project_id, issue_id, pk):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -857,6 +863,7 @@ def post(self, request, slug, project_id, issue_id):
current_instance=json.dumps({"parent": str(sub_issue_id)}),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
for sub_issue_id in sub_issue_ids
]
@@ -917,6 +924,7 @@ def create(self, request, slug, project_id, issue_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -947,6 +955,7 @@ def partial_update(self, request, slug, project_id, issue_id, pk):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -971,6 +980,7 @@ def destroy(self, request, slug, project_id, issue_id, pk):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
issue_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1028,6 +1038,7 @@ def post(self, request, slug, project_id, issue_id):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1045,6 +1056,7 @@ def delete(self, request, slug, project_id, issue_id, pk):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1224,6 +1236,7 @@ def unarchive(self, request, slug, project_id, pk=None):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
issue.archived_at = None
issue.save()
@@ -1370,6 +1383,7 @@ def create(self, request, slug, project_id, issue_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1396,6 +1410,7 @@ def destroy(self, request, slug, project_id, issue_id, reaction_code):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1437,6 +1452,7 @@ def create(self, request, slug, project_id, comment_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1464,6 +1480,7 @@ def destroy(self, request, slug, project_id, comment_id, reaction_code):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1593,6 +1610,7 @@ def create(self, request, slug, project_id, issue_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
if relation_type == "blocking":
@@ -1638,6 +1656,7 @@ def remove_relation(self, request, slug, project_id, issue_id):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1810,6 +1829,7 @@ def create(self, request, slug, project_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1841,6 +1861,7 @@ def partial_update(self, request, slug, project_id, pk):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1868,5 +1889,6 @@ def destroy(self, request, slug, project_id, pk=None):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py
index 58be79ca02..969adc2a5d 100644
--- a/apiserver/plane/app/views/module.py
+++ b/apiserver/plane/app/views/module.py
@@ -311,6 +311,7 @@ def destroy(self, request, slug, project_id, pk):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
module.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -490,6 +491,7 @@ def create(self, request, slug, project_id, module_id):
),
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
issues = self.get_queryset().values_list("issue_id", flat=True)
@@ -522,6 +524,7 @@ def destroy(self, request, slug, project_id, module_id, issue_id):
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
+ origin=request.META.get("HTTP_ORIGIN"),
)
module_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py
index c6b7f61cfc..86b6d938e4 100644
--- a/apiserver/plane/bgtasks/email_notification_task.py
+++ b/apiserver/plane/bgtasks/email_notification_task.py
@@ -14,7 +14,7 @@
# Module imports
from plane.db.models import EmailNotificationLog, User, Issue
from plane.license.utils.instance_value import get_email_configuration
-
+from plane.settings.redis import redis_instance
@shared_task
def stack_email_notification():
@@ -129,7 +129,8 @@ def create_payload(notification_data):
def send_email_notification(
issue_id, notification_data, receiver_id, email_notification_ids
):
- base_api = "https://app.plane.so"
+ ri = redis_instance()
+ base_api = (ri.get(str(issue_id)).decode())
data = create_payload(notification_data=notification_data)
# Get email configurations
@@ -160,7 +161,6 @@ def send_email_notification(
"first_name": actor.first_name,
"last_name": actor.last_name,
},
- "activity_time": str(activity_time),
}
)
activity_time = changes.pop("activity_time")
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index c2ad48f231..4a036ec318 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -28,7 +28,7 @@
)
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
-
+from plane.settings.redis import redis_instance
# Track Changes in name
def track_name(
@@ -1563,6 +1563,7 @@ def issue_activity(
epoch,
subscriber=True,
notification=False,
+ origin=None,
):
try:
issue_activities = []
@@ -1571,6 +1572,10 @@ def issue_activity(
workspace_id = project.workspace_id
if issue_id is not None:
+ if origin:
+ ri = redis_instance()
+ # set the request origin in redis
+ ri.set(str(issue_id), origin, ex=600)
issue = Issue.objects.filter(pk=issue_id).first()
if issue:
try:
diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py
index 0912e276af..11a88de57a 100644
--- a/apiserver/plane/celery.py
+++ b/apiserver/plane/celery.py
@@ -31,7 +31,7 @@
},
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
- "schedule": crontab(minute='*/5')
+ "schedule": crontab(minute='*/1')
},
}
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index 99470535f9..2bf5b69eca 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -780,7 +780,7 @@
border-radius: 4px;
"
>
- {{ actor_comment.new_value }}
+ {{ actor_comment.new_value.0 }}