From 7e9f543483fb802b5c140e41bb61fbd1a36e5172 Mon Sep 17 00:00:00 2001 From: Rodrigo Martin Date: Fri, 5 Apr 2024 12:27:21 -0300 Subject: [PATCH] feat: send email after course reset completion (#34460) * feat: send email after course reset completion * fix: lint test * fix: clean code * fix: correct expected email parts * fix: logs * fix: email assertion --- lms/djangoapps/support/message_types.py | 18 +++++++ lms/djangoapps/support/tasks.py | 53 +++++++++++++++++++ lms/djangoapps/support/tests/test_tasks.py | 21 ++++++++ .../edx_ace/wholecoursereset/email/body.html | 39 ++++++++++++++ .../edx_ace/wholecoursereset/email/body.txt | 8 +++ .../wholecoursereset/email/from_name.txt | 1 + .../edx_ace/wholecoursereset/email/head.html | 1 + .../wholecoursereset/email/subject.txt | 4 ++ 8 files changed, 145 insertions(+) create mode 100644 lms/djangoapps/support/message_types.py create mode 100644 lms/templates/support/edx_ace/wholecoursereset/email/body.html create mode 100644 lms/templates/support/edx_ace/wholecoursereset/email/body.txt create mode 100644 lms/templates/support/edx_ace/wholecoursereset/email/from_name.txt create mode 100644 lms/templates/support/edx_ace/wholecoursereset/email/head.html create mode 100644 lms/templates/support/edx_ace/wholecoursereset/email/subject.txt diff --git a/lms/djangoapps/support/message_types.py b/lms/djangoapps/support/message_types.py new file mode 100644 index 00000000000..40931cf33e3 --- /dev/null +++ b/lms/djangoapps/support/message_types.py @@ -0,0 +1,18 @@ +""" +ACE message types for support-related emails. +""" + +from openedx.core.djangoapps.ace_common.message import BaseMessageType + + +class WholeCourseReset(BaseMessageType): + """ + A message to the user when whole course reset was successful. + """ + + APP_LABEL = 'support' + Name = 'wholecoursereset' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.options['transactional'] = True diff --git a/lms/djangoapps/support/tasks.py b/lms/djangoapps/support/tasks.py index cf00823d98e..e274f80e0e4 100644 --- a/lms/djangoapps/support/tasks.py +++ b/lms/djangoapps/support/tasks.py @@ -13,6 +13,15 @@ from lms.djangoapps.instructor.enrollment import reset_student_attempts from lms.djangoapps.support.models import CourseResetAudit from lms.djangoapps.grades.api import clear_user_course_grades +from openedx.core.djangoapps.ace_common.template_context import get_base_template_context + +from edx_ace import ace +from django.contrib.sites.models import Site +from edx_ace.recipient import Recipient +from openedx.core.lib.celery.task_utils import emulate_http_request +from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY +from openedx.core.djangoapps.user_api.preferences.api import get_user_preference +from lms.djangoapps.support.message_types import WholeCourseReset log = logging.getLogger(__name__) @@ -35,6 +44,46 @@ def get_blocks(course): return blocks +@shared_task +@set_code_owner_attribute +def send_reset_course_completion_email(course, user): + """ + Sends email to a learner when whole course reset is complete. + """ + site = Site.objects.get_current() + + message_context = get_base_template_context(site) + message_context.update({ + 'course_title': course.display_name, + }) + + try: + log.info( + f"Sending whole course reset email to {user.profile.name} (Email: {user.email}) " + f"from course {course.display_name} (CourseId: {course.id})" + ) + with emulate_http_request(site=site, user=user): + msg = WholeCourseReset(context=message_context).personalize( + recipient=Recipient(user.id, user.email), + language=get_user_preference(user, LANGUAGE_KEY), + user_context={'full_name': user.profile.name} + ) + ace.send(msg) + except Exception as exc: # pylint: disable=broad-except + log.exception( + f"Whole course reset email to {user.profile.name} (Email: {user.email}) " + f"from course {course.display_name} (CourseId: {course.id}) failed." + f"Error: {exc.response['Error']['Code']}" + ) + return False + else: + log.info( + f"Whole course reset email sent successfully to {user.profile.name} (Email: {user.email}) " + f"from course {course.display_name} (CourseId: {course.id})" + ) + return True + + @shared_task @set_code_owner_attribute def reset_student_course(course_id, learner_email, reset_by_user_email): @@ -79,6 +128,10 @@ def reset_student_course(course_id, learner_email, reset_by_user_email): clear_user_course_grades(user.id, course.id) update_audit_status(course_reset_audit, CourseResetAudit.CourseResetStatus.COMPLETE) + + # Send email upon completion + send_reset_course_completion_email(course, user) + except Exception as e: # pylint: disable=broad-except logging.exception(f'Error occurred for Course Audit with ID {course_reset_audit.id}: {e}.') update_audit_status(course_reset_audit, CourseResetAudit.CourseResetStatus.FAILED) diff --git a/lms/djangoapps/support/tests/test_tasks.py b/lms/djangoapps/support/tests/test_tasks.py index 181f9022f4d..65add307933 100644 --- a/lms/djangoapps/support/tests/test_tasks.py +++ b/lms/djangoapps/support/tests/test_tasks.py @@ -4,6 +4,8 @@ from unittest.mock import patch, Mock, call +from django.conf import settings +from django.core import mail from xmodule.modulestore.tests.factories import BlockFactory from lms.djangoapps.courseware.tests.test_submitting_problems import TestSubmittingProblems @@ -15,6 +17,7 @@ from common.djangoapps.student.roles import SupportStaffRole from common.djangoapps.student.tests.factories import UserFactory from xmodule.video_block import VideoBlock +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers class ResetStudentCourse(TestSubmittingProblems): @@ -109,6 +112,20 @@ def basic_setup(self): self.refresh_course() + def assert_email_sent_successfully(self, expected): + """ + Verify that the course reset email has been sent to the user. + """ + from_email = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) + sent_message = mail.outbox[-1] + body = sent_message.body + + assert expected['subject'] in sent_message.subject + assert expected['body'] in body + assert sent_message.from_email == from_email + assert len(sent_message.to) == 1 + assert self.student_user.email in sent_message.to + def test_reset_student_course(self): """ Test that it resets student attempts """ with patch( @@ -157,6 +174,10 @@ def test_reset_student_course(self): course_reset_audit = CourseResetAudit.objects.get(course_enrollment=self.enrollment) self.assertIsNotNone(course_reset_audit.completed_at) self.assertEqual(course_reset_audit.status, CourseResetAudit.CourseResetStatus.COMPLETE) + self.assert_email_sent_successfully({ + 'subject': f'The course { self.course.display_name } has been reset !', + 'body': f'Your progress in course { self.course.display_name } has been reset on your behalf.' + }) def test_reset_student_course_student_module_not_found(self): diff --git a/lms/templates/support/edx_ace/wholecoursereset/email/body.html b/lms/templates/support/edx_ace/wholecoursereset/email/body.html new file mode 100644 index 00000000000..2ebf7da596d --- /dev/null +++ b/lms/templates/support/edx_ace/wholecoursereset/email/body.html @@ -0,0 +1,39 @@ +{% extends 'ace_common/edx_ace/common/base_body.html' %} + +{% load i18n %} +{% load static %} +{% block content %} + + + + +
+

+ {% trans "The course {{ course_title }} has been reset !" as tmsg %}{{ tmsg | force_escape }} +

+

+ {% filter force_escape %} + {% blocktrans %}Hello {{ full_name }},{% endblocktrans %} + {% endfilter %} +

+

+ {% filter force_escape %} + {% blocktrans %}Your progress in course {{course_title}} has been reset on your behalf.{% endblocktrans %} + {% endfilter %} + + {% filter force_escape %} + {% blocktrans %}You will be able to re-attempt this course and earn a verified certificate upon successful completion.{% endblocktrans %} + {% endfilter %} +
+

+ +

+ {% trans "Best," as tmsg %}{{ tmsg | force_escape }} +
+ {% filter force_escape %} + {% blocktrans %}The {{ platform_name }} Team {% endblocktrans %} + {% endfilter %} +

+ +
+{% endblock %} diff --git a/lms/templates/support/edx_ace/wholecoursereset/email/body.txt b/lms/templates/support/edx_ace/wholecoursereset/email/body.txt new file mode 100644 index 00000000000..2711b1dc877 --- /dev/null +++ b/lms/templates/support/edx_ace/wholecoursereset/email/body.txt @@ -0,0 +1,8 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}Hello {{full_name}}, {% endblocktrans %} +{% blocktrans %}Your progress in course {{course_title}} has been reset on your behalf.{% endblocktrans %} +{% blocktrans %}You will be able to re-attempt this course and earn a verified certificate upon successful completion.{% endblocktrans %} + +{% trans "Best," %} +{% blocktrans %}The {{ platform_name }} Team {% endblocktrans %} +{% endautoescape %} diff --git a/lms/templates/support/edx_ace/wholecoursereset/email/from_name.txt b/lms/templates/support/edx_ace/wholecoursereset/email/from_name.txt new file mode 100644 index 00000000000..dcbc23c0048 --- /dev/null +++ b/lms/templates/support/edx_ace/wholecoursereset/email/from_name.txt @@ -0,0 +1 @@ +{{ platform_name }} diff --git a/lms/templates/support/edx_ace/wholecoursereset/email/head.html b/lms/templates/support/edx_ace/wholecoursereset/email/head.html new file mode 100644 index 00000000000..366ada7ad92 --- /dev/null +++ b/lms/templates/support/edx_ace/wholecoursereset/email/head.html @@ -0,0 +1 @@ +{% extends 'ace_common/edx_ace/common/base_head.html' %} diff --git a/lms/templates/support/edx_ace/wholecoursereset/email/subject.txt b/lms/templates/support/edx_ace/wholecoursereset/email/subject.txt new file mode 100644 index 00000000000..3721a622faf --- /dev/null +++ b/lms/templates/support/edx_ace/wholecoursereset/email/subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans trimmed %}The course {{ course_title }} has been reset !{% endblocktrans %} +{% endautoescape %}