Merge pull request #34350 from openedx/hajorg/au-1848-course-reset-celery-task
feat: add celery task to reset course progress for learner
This commit is contained in:
67
lms/djangoapps/support/tasks.py
Normal file
67
lms/djangoapps/support/tasks.py
Normal file
@@ -0,0 +1,67 @@
|
||||
""" Celery Tasks for the Instructor App. """
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from celery import shared_task
|
||||
from edx_django_utils.monitoring import set_code_owner_attribute
|
||||
|
||||
from common.djangoapps.student.models.course_enrollment import CourseEnrollment
|
||||
from common.djangoapps.student.models.user import get_user_by_username_or_email
|
||||
from lms.djangoapps.courseware.courses import get_course
|
||||
from lms.djangoapps.courseware.models import StudentModule
|
||||
from lms.djangoapps.instructor.enrollment import reset_student_attempts
|
||||
from lms.djangoapps.support.models import CourseResetAudit
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def update_audit_status(audit_instance, status):
|
||||
audit_instance.status = status
|
||||
if status == CourseResetAudit.CourseResetStatus.COMPLETE:
|
||||
audit_instance.completed_at = datetime.now()
|
||||
audit_instance.save()
|
||||
|
||||
|
||||
def get_blocks(course):
|
||||
""" Get a list of problem xblock for the course."""
|
||||
blocks = []
|
||||
for section in course.get_children():
|
||||
for subsection in section.get_children():
|
||||
for vertical in subsection.get_children():
|
||||
for block in vertical.get_children():
|
||||
blocks.append(block)
|
||||
return blocks
|
||||
|
||||
|
||||
@shared_task
|
||||
@set_code_owner_attribute
|
||||
def reset_student_course(course_id, learner_email, reset_by_user_email):
|
||||
"""
|
||||
Resets a learner's course progress
|
||||
"""
|
||||
user = get_user_by_username_or_email(learner_email)
|
||||
reset_by_user = get_user_by_username_or_email(reset_by_user_email)
|
||||
enrollment = CourseEnrollment.objects.get(
|
||||
course=course_id,
|
||||
user=user,
|
||||
is_active=True,
|
||||
)
|
||||
course_overview = enrollment.course_overview
|
||||
course_reset_audit = CourseResetAudit.objects.get(
|
||||
course_enrollment=enrollment,
|
||||
status=CourseResetAudit.CourseResetStatus.ENQUEUED
|
||||
)
|
||||
update_audit_status(course_reset_audit, CourseResetAudit.CourseResetStatus.IN_PROGRESS)
|
||||
|
||||
try:
|
||||
course = get_course(course_overview.id, depth=4)
|
||||
blocks = get_blocks(course)
|
||||
for data in blocks:
|
||||
try:
|
||||
reset_student_attempts(course.id, user, data.scope_ids.usage_id, reset_by_user, True)
|
||||
except StudentModule.DoesNotExist:
|
||||
pass
|
||||
update_audit_status(course_reset_audit, CourseResetAudit.CourseResetStatus.COMPLETE)
|
||||
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)
|
||||
199
lms/djangoapps/support/tests/test_tasks.py
Normal file
199
lms/djangoapps/support/tests/test_tasks.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Unit tests for reset_student_course task
|
||||
"""
|
||||
|
||||
from unittest.mock import patch, Mock, call
|
||||
|
||||
from xmodule.modulestore.tests.factories import BlockFactory
|
||||
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import TestSubmittingProblems
|
||||
from lms.djangoapps.courseware.models import StudentModule
|
||||
from lms.djangoapps.support.tasks import reset_student_course
|
||||
from lms.djangoapps.support.tests.factories import CourseResetAuditFactory, CourseResetCourseOptInFactory
|
||||
from lms.djangoapps.support.models import CourseResetAudit
|
||||
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
|
||||
|
||||
|
||||
class ResetStudentCourse(TestSubmittingProblems):
|
||||
""" Test reset_student_course task """
|
||||
USERNAME = "support"
|
||||
EMAIL = "support@example.com"
|
||||
PASSWORD = "support"
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set permissions, create a course and learner, enroll learner and opt into course reset
|
||||
"""
|
||||
super().setUp()
|
||||
self.user = UserFactory(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
SupportStaffRole().add_users(self.user)
|
||||
self.course_id = str(self.course.id)
|
||||
self.enrollment = CourseEnrollment.objects.filter(user=self.student_user, course_id=self.course.id).first()
|
||||
self.opt_in = CourseResetCourseOptInFactory.create(course_id=self.course.id)
|
||||
self.audit = CourseResetAuditFactory.create(
|
||||
course=self.opt_in,
|
||||
course_enrollment=self.enrollment,
|
||||
reset_by=self.user,
|
||||
status=CourseResetAudit.CourseResetStatus.ENQUEUED
|
||||
)
|
||||
self.p1 = ''
|
||||
self.p2 = ''
|
||||
self.p3 = ''
|
||||
self.video = ''
|
||||
|
||||
def basic_setup(self):
|
||||
"""
|
||||
Set up a simple course for testing basic grading functionality.
|
||||
"""
|
||||
grading_policy = {
|
||||
"GRADER": [{
|
||||
"type": "Homework",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"short_label": "HW",
|
||||
"weight": 1.0
|
||||
}],
|
||||
"GRADE_CUTOFFS": {
|
||||
'A': .9,
|
||||
'B': .33
|
||||
}
|
||||
}
|
||||
self.add_grading_policy(grading_policy)
|
||||
|
||||
# set up a simple course with four problems
|
||||
homework = self.add_graded_section_to_course('homework')
|
||||
vertical = BlockFactory.create(
|
||||
parent_location=homework.location,
|
||||
category='vertical',
|
||||
display_name='Unit 1',
|
||||
)
|
||||
|
||||
self.p1 = self.add_dropdown_to_section(vertical.location, 'p1', 1)
|
||||
self.p2 = self.add_dropdown_to_section(vertical.location, 'p2', 1)
|
||||
self.p3 = self.add_dropdown_to_section(vertical.location, 'p3', 1)
|
||||
video_sample_xml = """
|
||||
<video display_name="Test Video"
|
||||
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
|
||||
show_captions="false"
|
||||
from="1.0"
|
||||
to="60.0">
|
||||
<source src="http://www.example.com/file.mp4"/>
|
||||
<track src="http://www.example.com/track"/>
|
||||
</video>
|
||||
"""
|
||||
video_data = VideoBlock.parse_video_xml(video_sample_xml)
|
||||
video_data.pop('source')
|
||||
self.video = BlockFactory.create(
|
||||
category='video',
|
||||
parent_location=vertical.location,
|
||||
**video_data
|
||||
)
|
||||
|
||||
self.refresh_course()
|
||||
|
||||
def test_reset_student_course(self):
|
||||
""" Test that it resets student attempts """
|
||||
with patch(
|
||||
'lms.djangoapps.support.tasks.reset_student_attempts',
|
||||
) as mock_reset_student_attempts:
|
||||
self.basic_setup()
|
||||
reset_student_course(self.course_id, self.student_user.email, self.user.email)
|
||||
|
||||
mock_reset_student_attempts.assert_has_calls([
|
||||
call(
|
||||
self.course.id,
|
||||
self.student_user,
|
||||
self.p1.location,
|
||||
self.user,
|
||||
True
|
||||
),
|
||||
call(
|
||||
self.course.id,
|
||||
self.student_user,
|
||||
self.p2.location,
|
||||
self.user,
|
||||
True
|
||||
),
|
||||
call(
|
||||
self.course.id,
|
||||
self.student_user,
|
||||
self.p3.location,
|
||||
self.user,
|
||||
True
|
||||
),
|
||||
call(
|
||||
self.course.id,
|
||||
self.student_user,
|
||||
self.video.location,
|
||||
self.user,
|
||||
True
|
||||
)
|
||||
])
|
||||
|
||||
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)
|
||||
|
||||
def test_reset_student_course_student_module_not_found(self):
|
||||
|
||||
with patch(
|
||||
'lms.djangoapps.support.tasks.reset_student_attempts',
|
||||
Mock(side_effect=StudentModule.DoesNotExist())
|
||||
) as mock_reset_student_attempts:
|
||||
self.basic_setup()
|
||||
reset_student_course(self.course_id, self.student_user.email, self.user.email)
|
||||
mock_reset_student_attempts.assert_has_calls([
|
||||
call(
|
||||
self.course.id,
|
||||
self.student_user,
|
||||
self.p1.location,
|
||||
self.user,
|
||||
True
|
||||
),
|
||||
call(
|
||||
self.course.id,
|
||||
self.student_user,
|
||||
self.p2.location,
|
||||
self.user,
|
||||
True
|
||||
),
|
||||
call(
|
||||
self.course.id,
|
||||
self.student_user,
|
||||
self.p3.location,
|
||||
self.user,
|
||||
True
|
||||
)
|
||||
])
|
||||
|
||||
course_reset_audit = CourseResetAudit.objects.get(course_enrollment=self.enrollment)
|
||||
self.assertRaises(StudentModule.DoesNotExist, mock_reset_student_attempts)
|
||||
self.assertIsNotNone(course_reset_audit.completed_at)
|
||||
self.assertEqual(course_reset_audit.status, CourseResetAudit.CourseResetStatus.COMPLETE)
|
||||
|
||||
@patch('lms.djangoapps.support.tasks.reset_student_attempts')
|
||||
def test_reset_student_course_fail(self, mock_reset_student_attempts):
|
||||
with patch(
|
||||
'lms.djangoapps.support.tasks.get_blocks',
|
||||
Mock(side_effect=Exception())
|
||||
):
|
||||
reset_student_course(self.course_id, self.student_user.email, self.user.email)
|
||||
mock_reset_student_attempts.assert_not_called()
|
||||
course_reset_audit = CourseResetAudit.objects.get(course_enrollment=self.enrollment)
|
||||
self.assertIsNone(course_reset_audit.completed_at)
|
||||
self.assertEqual(course_reset_audit.status, CourseResetAudit.CourseResetStatus.FAILED)
|
||||
|
||||
def test_reset_student_attempts_raise_exception(self):
|
||||
with patch(
|
||||
'lms.djangoapps.support.tasks.reset_student_attempts',
|
||||
Mock(side_effect=Exception())
|
||||
) as mock_reset_student_attempts:
|
||||
self.basic_setup()
|
||||
reset_student_course(self.course_id, self.student_user.email, self.user.email)
|
||||
mock_reset_student_attempts.assert_called_once()
|
||||
course_reset_audit = CourseResetAudit.objects.get(course_enrollment=self.enrollment)
|
||||
self.assertIsNone(course_reset_audit.completed_at)
|
||||
self.assertEqual(course_reset_audit.status, CourseResetAudit.CourseResetStatus.FAILED)
|
||||
@@ -2377,7 +2377,8 @@ class TestResetCourseViewPost(SupportViewTestCase):
|
||||
response = self.client.post(self._url(username='does_not_exist'), data={'course_id': 'course-v1:aa+bb+c'})
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_learner_course_reset(self):
|
||||
@patch('lms.djangoapps.support.views.course_reset.reset_student_course')
|
||||
def test_learner_course_reset(self, mock_reset_student_course):
|
||||
response = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data, {
|
||||
@@ -2386,12 +2387,16 @@ class TestResetCourseViewPost(SupportViewTestCase):
|
||||
'can_reset': False,
|
||||
'display_name': self.course.display_name
|
||||
})
|
||||
self.assertEqual(
|
||||
mock_reset_student_course.delay.call_count, 1
|
||||
)
|
||||
|
||||
def test_course_not_opt_in(self):
|
||||
response = self.client.post(self._url(username=self.user.username), data={'course_id': 'course-v1:aa+bb+c'})
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_course_reset_failed(self):
|
||||
@patch('lms.djangoapps.support.views.course_reset.reset_student_course')
|
||||
def test_course_reset_failed(self, mock_reset_student_course):
|
||||
course = CourseFactory.create(
|
||||
org='xx',
|
||||
course='yy',
|
||||
@@ -2416,6 +2421,9 @@ class TestResetCourseViewPost(SupportViewTestCase):
|
||||
status=CourseResetAudit.CourseResetStatus.FAILED
|
||||
)
|
||||
response = self.client.post(self._url(username=self.user.username), data={'course_id': course.id})
|
||||
self.assertEqual(
|
||||
mock_reset_student_course.delay.call_count, 1
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_course_reset_dupe(self):
|
||||
|
||||
@@ -14,6 +14,7 @@ from lms.djangoapps.support.models import (
|
||||
CourseResetCourseOptIn,
|
||||
CourseResetAudit
|
||||
)
|
||||
from ..tasks import reset_student_course
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -131,7 +132,8 @@ class CourseResetAPIView(APIView):
|
||||
):
|
||||
course_reset_audit.status = CourseResetAudit.CourseResetStatus.ENQUEUED
|
||||
course_reset_audit.save()
|
||||
# Call celery task
|
||||
reset_student_course.delay(course_id, user.email, request.user.email)
|
||||
|
||||
resp = {
|
||||
'course_id': course_id,
|
||||
'status': course_reset_audit.status_message(),
|
||||
@@ -159,7 +161,7 @@ class CourseResetAPIView(APIView):
|
||||
'display_name': course_overview.display_name
|
||||
}
|
||||
|
||||
# Call celery task
|
||||
reset_student_course.delay(course_id, user.email, request.user.email)
|
||||
return Response(resp, status=201)
|
||||
else:
|
||||
return Response(None, status=400)
|
||||
|
||||
Reference in New Issue
Block a user