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 + + + + + + + + + + + + + + +