""" This module contains tasks for asynchronous execution of grade updates. """ from celery import task from django.conf import settings from django.contrib.auth.models import User from django.db.utils import IntegrityError from logging import getLogger from courseware.model_data import get_score from lms.djangoapps.course_blocks.api import get_course_blocks from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locator import CourseLocator from xmodule.modulestore.django import modulestore from .config.models import PersistentGradesEnabledFlag from .new.subsection_grade import SubsectionGradeFactory from .signals.signals import SUBSECTION_SCORE_CHANGED from .transformer import GradesTransformer log = getLogger(__name__) @task(default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY) def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, raw_earned, raw_possible): """ Updates a saved subsection grade. This method expects the following parameters: - user_id: serialized id of applicable User object - course_id: Unicode string identifying the course - usage_id: Unicode string identifying the course block - only_if_higher: boolean indicating whether grades should be updated only if the new raw_earned is higher than the previous value. - raw_earned: the raw points the learner earned on the problem that triggered the update - raw_possible: the max raw points the leaner could have earned on the problem """ course_key = CourseLocator.from_string(course_id) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return scored_block_usage_key = UsageKey.from_string(usage_id).replace(course_key=course_key) score = get_score(user_id, scored_block_usage_key) # If the score is None, it has not been saved at all yet # and we need to retry until it has been saved. if score is None: raise _retry_recalculate_subsection_grade( user_id, course_id, usage_id, only_if_higher, raw_earned, raw_possible, ) else: module_raw_earned, module_raw_possible = score # pylint: disable=unpacking-non-sequence # Validate that the retrieved scores match the scores when the task was created. # This race condition occurs if the transaction in the task creator's process hasn't # committed before the task initiates in the worker process. grades_match = module_raw_earned == raw_earned and module_raw_possible == raw_possible # We have to account for the situation where a student's state # has been deleted- in this case, get_score returns (None, None), but # the score change signal will contain 0 for raw_earned. state_deleted = module_raw_earned is None and module_raw_possible is None and raw_earned == 0 if not (state_deleted or grades_match): raise _retry_recalculate_subsection_grade( user_id, course_id, usage_id, only_if_higher, raw_earned, raw_possible, ) _update_subsection_grades( course_key, scored_block_usage_key, only_if_higher, course_id, user_id, usage_id, raw_earned, raw_possible, ) def _update_subsection_grades( course_key, scored_block_usage_key, only_if_higher, course_id, user_id, usage_id, raw_earned, raw_possible, ): """ A helper function to update subsection grades in the database for each subsection containing the given block, and to signal that those subsection grades were updated. """ student = User.objects.get(id=user_id) course_structure = get_course_blocks(student, modulestore().make_course_usage_key(course_key)) subsections_to_update = course_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set(), ) course = modulestore().get_course(course_key, depth=0) subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure) try: for subsection_usage_key in subsections_to_update: subsection_grade = subsection_grade_factory.update( course_structure[subsection_usage_key], only_if_higher, ) SUBSECTION_SCORE_CHANGED.send( sender=recalculate_subsection_grade, course=course, course_structure=course_structure, user=student, subsection_grade=subsection_grade, ) except IntegrityError as exc: raise _retry_recalculate_subsection_grade( user_id, course_id, usage_id, only_if_higher, raw_earned, raw_possible, exc, ) def _retry_recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, grade, max_grade, exc=None): """ Calls retry for the recalculate_subsection_grade task with the given inputs. """ recalculate_subsection_grade.retry( args=[ user_id, course_id, usage_id, only_if_higher, grade, max_grade, ], exc=exc )