From d1ec83039ced27a4d6cced5500ca4ed60badc547 Mon Sep 17 00:00:00 2001
From: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Date: Thu, 27 Jun 2024 20:45:30 +0530
Subject: [PATCH] [WEB - 1749] chore: send email when a user is added to the
project (#4952)
* chore: send email when a user is added to the project
* dev: add email template for project addition
---
apiserver/plane/app/views/project/member.py | 47 +-
.../bgtasks/project_add_user_email_task.py | 87 +
.../notifications/project_addition.html | 1591 +++++++++++++++++
3 files changed, 1714 insertions(+), 11 deletions(-)
create mode 100644 apiserver/plane/bgtasks/project_add_user_email_task.py
create mode 100644 apiserver/templates/emails/notifications/project_addition.html
diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py
index 187dfc8d0..9ecb512d3 100644
--- a/apiserver/plane/app/views/project/member.py
+++ b/apiserver/plane/app/views/project/member.py
@@ -24,6 +24,8 @@ from plane.db.models import (
TeamMember,
IssueProperty,
)
+from plane.bgtasks.project_add_user_email_task import project_add_user_email
+from plane.utils.host import base_host
class ProjectMemberViewSet(BaseViewSet):
@@ -64,33 +66,29 @@ class ProjectMemberViewSet(BaseViewSet):
)
def create(self, request, slug, project_id):
+ # Get the list of members to be added to the project and their roles i.e. the user_id and the role
members = request.data.get("members", [])
# get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
+ # Check if the members array is empty
if not len(members):
return Response(
{"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
+
+ # Initialize the bulk arrays
bulk_project_members = []
bulk_issue_props = []
- project_members = (
- ProjectMember.objects.filter(
- workspace__slug=slug,
- member_id__in=[member.get("member_id") for member in members],
- )
- .values("member_id", "sort_order")
- .order_by("sort_order")
- )
-
- bulk_project_members = []
+ # Create a dictionary of the member_id and their roles
member_roles = {
member.get("member_id"): member.get("role") for member in members
}
- # Update roles in the members array based on the member_roles dictionary
+
+ # Update roles in the members array based on the member_roles dictionary and set is_active to True
for project_member in ProjectMember.objects.filter(
project_id=project_id,
member_id__in=[member.get("member_id") for member in members],
@@ -104,13 +102,27 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_project_members, ["is_active", "role"], batch_size=100
)
+ # Get the list of project members of the requested workspace with the given slug
+ project_members = (
+ ProjectMember.objects.filter(
+ workspace__slug=slug,
+ member_id__in=[member.get("member_id") for member in members],
+ )
+ .values("member_id", "sort_order")
+ .order_by("sort_order")
+ )
+
+ # Loop through requested members
for member in members:
+
+ # Get the sort orders of the member
sort_order = [
project_member.get("sort_order")
for project_member in project_members
if str(project_member.get("member_id"))
== str(member.get("member_id"))
]
+ # Create a new project member
bulk_project_members.append(
ProjectMember(
member_id=member.get("member_id"),
@@ -122,6 +134,7 @@ class ProjectMemberViewSet(BaseViewSet):
),
)
)
+ # Create a new issue property
bulk_issue_props.append(
IssueProperty(
user_id=member.get("member_id"),
@@ -130,6 +143,7 @@ class ProjectMemberViewSet(BaseViewSet):
)
)
+ # Bulk create the project members and issue properties
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,
@@ -144,7 +158,18 @@ class ProjectMemberViewSet(BaseViewSet):
project_id=project_id,
member_id__in=[member.get("member_id") for member in members],
)
+ # Send emails to notify the users
+ [
+ project_add_user_email.delay(
+ base_host(request=request, is_app=True),
+ project_member.id,
+ request.user.id,
+ )
+ for project_member in project_members
+ ]
+ # Serialize the project members
serializer = ProjectMemberRoleSerializer(project_members, many=True)
+ # Return the serialized data
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id):
diff --git a/apiserver/plane/bgtasks/project_add_user_email_task.py b/apiserver/plane/bgtasks/project_add_user_email_task.py
new file mode 100644
index 000000000..c8308465a
--- /dev/null
+++ b/apiserver/plane/bgtasks/project_add_user_email_task.py
@@ -0,0 +1,87 @@
+# Python imports
+import logging
+
+# Third party imports
+from celery import shared_task
+
+# Third party imports
+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.license.utils.instance_value import get_email_configuration
+from plane.utils.exception_logger import log_exception
+from plane.db.models import ProjectMember
+from plane.db.models import User
+
+
+@shared_task
+def project_add_user_email(current_site, project_member_id, invitor_id):
+ try:
+ # Get the invitor
+ invitor = User.objects.get(pk=invitor_id)
+ inviter_first_name = invitor.first_name
+ # Get the project member
+ project_member = ProjectMember.objects.get(pk=project_member_id)
+ # Get the project member details
+ project_name = project_member.project.name
+ workspace_name = project_member.workspace.name
+ member_email = project_member.member.email
+ project_url = f"{current_site}/{project_member.workspace.slug}/projects/{project_member.project_id}/issues"
+ # set the context
+ context = {
+ "project_name": project_name,
+ "workspace_name": workspace_name,
+ "email": member_email,
+ "inviter_first_name": inviter_first_name,
+ "project_url": project_url,
+ }
+
+ # Get the email configuration
+ (
+ EMAIL_HOST,
+ EMAIL_HOST_USER,
+ EMAIL_HOST_PASSWORD,
+ EMAIL_PORT,
+ EMAIL_USE_TLS,
+ EMAIL_USE_SSL,
+ EMAIL_FROM,
+ ) = get_email_configuration()
+
+ # Set the subject
+ subject = "You have been invited to a Plane project"
+
+ # Render the email template
+ html_content = render_to_string(
+ "emails/notifications/project_addition.html", context
+ )
+ text_content = strip_tags(html_content)
+ # Initialize the connection
+ connection = get_connection(
+ host=EMAIL_HOST,
+ port=int(EMAIL_PORT),
+ username=EMAIL_HOST_USER,
+ password=EMAIL_HOST_PASSWORD,
+ use_tls=EMAIL_USE_TLS == "1",
+ use_ssl=EMAIL_USE_SSL == "1",
+ )
+ # Send the email
+ msg = EmailMultiAlternatives(
+ subject=subject,
+ body=text_content,
+ from_email=EMAIL_FROM,
+ to=[member_email],
+ connection=connection,
+ )
+ # Attach the html content
+ msg.attach_alternative(html_content, "text/html")
+ # Send the email
+ msg.send()
+ # Log the success
+ logging.getLogger("plane").info("Email sent successfully.")
+ return
+ except Exception as e:
+ log_exception(e)
+ return
diff --git a/apiserver/templates/emails/notifications/project_addition.html b/apiserver/templates/emails/notifications/project_addition.html
new file mode 100644
index 000000000..ccf0f7a95
--- /dev/null
+++ b/apiserver/templates/emails/notifications/project_addition.html
@@ -0,0 +1,1591 @@
+
+
+
+
+
+
+
+ You are have been invited to a Plane project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ You've been invited to a Plane
+ project.
+
+
+ |
+
+
+
+
+
+ {{inviter_first_name}} has
+ invited you to work with them in
+ {{project_name}} in the workspace
+ {{workspace_name}} on Plane.
+
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ |
+
+
+
+ Despite our popularity, we
+ are humbly early-stage. We
+ are shipping fast, so please
+ reach out to us with feature
+ requests, major and minor
+ nits, and anything else you
+ find missing. We read every message, tweet, and conversation
+ and update our public roadmap.
+
+
+ |
+
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This email was sent
+ to {{email }}. Please delete
+ if you aren't the intended
+ recipient.
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+