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
This commit is contained in:
Rodrigo Martin
2024-04-05 12:27:21 -03:00
committed by GitHub
parent a08a10c396
commit e768d6d9e5
8 changed files with 145 additions and 0 deletions

View File

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

View File

@@ -13,6 +13,15 @@ from lms.djangoapps.courseware.models import StudentModule
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)

View File

@@ -4,6 +4,8 @@ Unit tests for reset_student_course task
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.models.course_enrollment import CourseEnrollment
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 @@ class ResetStudentCourse(TestSubmittingProblems):
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 @@ class ResetStudentCourse(TestSubmittingProblems):
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):

View File

@@ -0,0 +1,39 @@
{% extends 'ace_common/edx_ace/common/base_body.html' %}
{% load i18n %}
{% load static %}
{% block content %}
<table width="100%" align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<h1>
{% trans "The course {{ course_title }} has been reset !" as tmsg %}{{ tmsg | force_escape }}
</h1>
<p style="color: rgba(0,0,0,.75);">
{% filter force_escape %}
{% blocktrans %}Hello {{ full_name }},{% endblocktrans %}
{% endfilter %}
</p>
<p style="color: rgba(0,0,0,.75);">
{% 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 %}
<br/>
</p>
<p>
{% trans "Best," as tmsg %}{{ tmsg | force_escape }}
<br/>
{% filter force_escape %}
{% blocktrans %}The {{ platform_name }} Team {% endblocktrans %}
{% endfilter %}
</p>
</td>
</tr>
</table>
{% endblock %}

View File

@@ -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 %}

View File

@@ -0,0 +1 @@
{{ platform_name }}

View File

@@ -0,0 +1 @@
{% extends 'ace_common/edx_ace/common/base_head.html' %}

View File

@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans trimmed %}The course {{ course_title }} has been reset !{% endblocktrans %}
{% endautoescape %}