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:
18
lms/djangoapps/support/message_types.py
Normal file
18
lms/djangoapps/support/message_types.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1 @@
|
||||
{{ platform_name }}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_head.html' %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans trimmed %}The course {{ course_title }} has been reset !{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
Reference in New Issue
Block a user