diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 8327a34cdf..8a689bb41f 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -103,6 +103,7 @@ UNENROLLED_TO_ENROLLED = 'from unenrolled to enrolled' ALLOWEDTOENROLL_TO_UNENROLLED = 'from allowed to enroll to enrolled' UNENROLLED_TO_UNENROLLED = 'from unenrolled to unenrolled' DEFAULT_TRANSITION_STATE = 'N/A' +SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE = 30 TRANSITION_STATES = ( (UNENROLLED_TO_ALLOWEDTOENROLL, UNENROLLED_TO_ALLOWEDTOENROLL), @@ -1342,7 +1343,15 @@ class CourseEnrollment(models.Model): # Only emit mode change events when the user's enrollment # mode has changed from its previous setting self.emit_event(EVENT_NAME_ENROLLMENT_MODE_CHANGED) - ENROLLMENT_TRACK_UPDATED.send(sender=None, user=self.user, course_key=self.course_id) + # this signal is meant to trigger a score recalculation celery task, + # `countdown` is added to celery task as delay so that cohort is duly updated + # before starting score recalculation + ENROLLMENT_TRACK_UPDATED.send( + sender=None, + user=self.user, + course_key=self.course_id, + countdown=SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE + ) def send_signal(self, event, cost=None, currency=None): """ diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index da8f26be6d..12ccab7495 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -12,7 +12,13 @@ from nose.plugins.attrib import attr from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory from openedx.core.djangoapps.embargo.test_utils import restrict_course -from student.models import CourseEnrollment, CourseEnrollmentAllowed, CourseFullError, EnrollmentClosedError +from student.models import ( + CourseEnrollment, + CourseEnrollmentAllowed, + CourseFullError, + EnrollmentClosedError, + SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE, +) from student.roles import CourseInstructorRole, CourseStaffRole from student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory from util.testing import UrlResetMixin @@ -340,3 +346,34 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): # Still same cea.refresh_from_db() self.assertEqual(cea.user, user1) + + def test_score_recalculation_on_enrollment_update(self): + """ + Test that an update in enrollment cause score recalculation. + Note: + Score recalculation task must be called with a delay of SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE + """ + course_modes = ['verified', 'audit'] + + for mode_slug in course_modes: + CourseModeFactory.create( + course_id=self.course.id, + mode_slug=mode_slug, + mode_display_name=mode_slug, + ) + CourseEnrollment.enroll(self.user, self.course.id, mode="audit") + + local_task_args = dict( + user_id=self.user.id, + course_key=str(self.course.id) + ) + + with patch( + 'lms.djangoapps.grades.tasks.recalculate_course_and_subsection_grades_for_user.apply_async', + return_value=None + ) as mock_task_apply: + CourseEnrollment.enroll(self.user, self.course.id, mode="verified") + mock_task_apply.assert_called_once_with( + countdown=SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE, + kwargs=local_task_args + ) diff --git a/lms/djangoapps/grades/signals/handlers.py b/lms/djangoapps/grades/signals/handlers.py index b7bd359870..6c1f5decdd 100644 --- a/lms/djangoapps/grades/signals/handlers.py +++ b/lms/djangoapps/grades/signals/handlers.py @@ -241,12 +241,13 @@ def recalculate_course_grade_only(sender, course, course_structure, user, **kwar @receiver(ENROLLMENT_TRACK_UPDATED) @receiver(COHORT_MEMBERSHIP_UPDATED) -def recalculate_course_and_subsection_grades(sender, user, course_key, **kwargs): +def recalculate_course_and_subsection_grades(sender, user, course_key, countdown=None, **kwargs): # pylint: disable=unused-argument """ Updates a saved course grade, forcing the subsection grades from which it is calculated to update along the way. """ recalculate_course_and_subsection_grades_for_user.apply_async( + countdown=countdown, kwargs=dict( user_id=user.id, course_key=six.text_type(course_key)