Commit a6b94a24 by Tyler Hallada

Use signal instead of calling recalculate directly

parent f92c96ef
...@@ -9,6 +9,7 @@ from util.date_utils import to_timestamp ...@@ -9,6 +9,7 @@ from util.date_utils import to_timestamp
from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
from .constants import ScoreDatabaseTableEnum from .constants import ScoreDatabaseTableEnum
from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
def _get_key(key_or_id, key_cls): def _get_key(key_or_id, key_cls):
...@@ -70,8 +71,7 @@ class GradesService(object): ...@@ -70,8 +71,7 @@ class GradesService(object):
override earned_all or earned_graded value if they are None. Both default to None. override earned_all or earned_graded value if they are None. Both default to None.
""" """
# prevent circular imports: # prevent circular imports:
from .signals.handlers import SUBSECTION_RESCORE_EVENT_TYPE from .signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
from .tasks import recalculate_subsection_grade_v3
course_key = _get_key(course_key_or_id, CourseKey) course_key = _get_key(course_key_or_id, CourseKey)
usage_key = _get_key(usage_key_or_id, UsageKey) usage_key = _get_key(usage_key_or_id, UsageKey)
...@@ -89,22 +89,20 @@ class GradesService(object): ...@@ -89,22 +89,20 @@ class GradesService(object):
earned_graded_override=earned_graded earned_graded_override=earned_graded
) )
# Recalculation will call PersistentSubsectionGrade.update_or_create_grade which will use the above override # Cache a new event id and event type which the signal handler will use to emit a tracking log event.
# to update the grade before writing to the table. create_new_event_transaction_id()
event_transaction_id = create_new_event_transaction_id() set_event_transaction_type(SUBSECTION_OVERRIDE_EVENT_TYPE)
set_event_transaction_type(SUBSECTION_RESCORE_EVENT_TYPE)
recalculate_subsection_grade_v3.apply_async( # Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
kwargs=dict( # which will use the above override to update the grade before writing to the table.
user_id=user_id, SUBSECTION_OVERRIDE_CHANGED.send(
course_id=unicode(course_key), user_id=user_id,
usage_id=unicode(usage_key), course_id=unicode(course_key),
only_if_higher=False, usage_id=unicode(usage_key),
expected_modified_time=to_timestamp(override.modified), only_if_higher=False,
score_deleted=False, modified=override.modified,
event_transaction_id=unicode(event_transaction_id), score_deleted=False,
event_transaction_type=SUBSECTION_RESCORE_EVENT_TYPE, score_db_table=ScoreDatabaseTableEnum.overrides
score_db_table=ScoreDatabaseTableEnum.overrides
)
) )
def undo_override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id): def undo_override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id):
...@@ -115,8 +113,7 @@ class GradesService(object): ...@@ -115,8 +113,7 @@ class GradesService(object):
override does not exist, no error is raised, it just triggers the recalculation. override does not exist, no error is raised, it just triggers the recalculation.
""" """
# prevent circular imports: # prevent circular imports:
from .signals.handlers import SUBSECTION_RESCORE_EVENT_TYPE from .signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
from .tasks import recalculate_subsection_grade_v3
course_key = _get_key(course_key_or_id, CourseKey) course_key = _get_key(course_key_or_id, CourseKey)
usage_key = _get_key(usage_key_or_id, UsageKey) usage_key = _get_key(usage_key_or_id, UsageKey)
...@@ -126,23 +123,24 @@ class GradesService(object): ...@@ -126,23 +123,24 @@ class GradesService(object):
if override is not None: if override is not None:
override.delete() override.delete()
event_transaction_id = create_new_event_transaction_id() # Cache a new event id and event type which the signal handler will use to emit a tracking log event.
set_event_transaction_type(SUBSECTION_RESCORE_EVENT_TYPE) create_new_event_transaction_id()
recalculate_subsection_grade_v3.apply_async( set_event_transaction_type(SUBSECTION_OVERRIDE_EVENT_TYPE)
kwargs=dict(
user_id=user_id, # Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
course_id=unicode(course_key), # which will no longer use the above deleted override, and instead return the grade to the original score from
usage_id=unicode(usage_key), # the actual problem responses before writing to the table.
only_if_higher=False, SUBSECTION_OVERRIDE_CHANGED.send(
# Not used when score_deleted=True: user_id=user_id,
expected_modified_time=to_timestamp(datetime.now().replace(tzinfo=pytz.UTC)), course_id=unicode(course_key),
score_deleted=True, usage_id=unicode(usage_key),
event_transaction_id=unicode(event_transaction_id), only_if_higher=False,
event_transaction_type=SUBSECTION_RESCORE_EVENT_TYPE, modified=datetime.now().replace(tzinfo=pytz.UTC), # Not used when score_deleted=True
score_db_table=ScoreDatabaseTableEnum.overrides score_deleted=True,
) score_db_table=ScoreDatabaseTableEnum.overrides
) )
def should_override_grade_on_rejected_exam(self, course_key): def should_override_grade_on_rejected_exam(self, course_key_or_id):
"""Convienence function to return the state of the CourseWaffleFlag REJECTED_EXAM_OVERRIDES_GRADE""" """Convienence function to return the state of the CourseWaffleFlag REJECTED_EXAM_OVERRIDES_GRADE"""
course_key = _get_key(course_key_or_id, CourseKey)
return waffle_flags()[REJECTED_EXAM_OVERRIDES_GRADE].is_enabled(course_key) return waffle_flags()[REJECTED_EXAM_OVERRIDES_GRADE].is_enabled(course_key)
...@@ -30,7 +30,8 @@ from .signals import ( ...@@ -30,7 +30,8 @@ from .signals import (
PROBLEM_RAW_SCORE_CHANGED, PROBLEM_RAW_SCORE_CHANGED,
PROBLEM_WEIGHTED_SCORE_CHANGED, PROBLEM_WEIGHTED_SCORE_CHANGED,
SCORE_PUBLISHED, SCORE_PUBLISHED,
SUBSECTION_SCORE_CHANGED SUBSECTION_SCORE_CHANGED,
SUBSECTION_OVERRIDE_CHANGED
) )
log = getLogger(__name__) log = getLogger(__name__)
...@@ -38,7 +39,7 @@ log = getLogger(__name__) ...@@ -38,7 +39,7 @@ log = getLogger(__name__)
# define values to be used in grading events # define values to be used in grading events
GRADES_RESCORE_EVENT_TYPE = 'edx.grades.problem.rescored' GRADES_RESCORE_EVENT_TYPE = 'edx.grades.problem.rescored'
PROBLEM_SUBMITTED_EVENT_TYPE = 'edx.grades.problem.submitted' PROBLEM_SUBMITTED_EVENT_TYPE = 'edx.grades.problem.submitted'
SUBSECTION_RESCORE_EVENT_TYPE = 'edx.grades.subsection.rescored' SUBSECTION_OVERRIDE_EVENT_TYPE = 'edx.grades.subsection.score_overridden'
@receiver(score_set) @receiver(score_set)
...@@ -210,9 +211,10 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus ...@@ -210,9 +211,10 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus
@receiver(PROBLEM_WEIGHTED_SCORE_CHANGED) @receiver(PROBLEM_WEIGHTED_SCORE_CHANGED)
@receiver(SUBSECTION_OVERRIDE_CHANGED)
def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argument def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argument
""" """
Handles the PROBLEM_WEIGHTED_SCORE_CHANGED signal by Handles the PROBLEM_WEIGHTED_SCORE_CHANGED or SUBSECTION_OVERRIDE_CHANGED signals by
enqueueing a subsection update operation to occur asynchronously. enqueueing a subsection update operation to occur asynchronously.
""" """
_emit_event(kwargs) _emit_event(kwargs)
...@@ -294,9 +296,9 @@ def _emit_event(kwargs): ...@@ -294,9 +296,9 @@ def _emit_event(kwargs):
} }
) )
if root_type in [SUBSECTION_RESCORE_EVENT_TYPE]: if root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]:
tracker.emit( tracker.emit(
unicode(SUBSECTION_RESCORE_EVENT_TYPE), unicode(SUBSECTION_OVERRIDE_EVENT_TYPE),
{ {
'course_id': unicode(kwargs['course_id']), 'course_id': unicode(kwargs['course_id']),
'user_id': unicode(kwargs['user_id']), 'user_id': unicode(kwargs['user_id']),
......
...@@ -81,3 +81,23 @@ SUBSECTION_SCORE_CHANGED = Signal( ...@@ -81,3 +81,23 @@ SUBSECTION_SCORE_CHANGED = Signal(
'subsection_grade', # SubsectionGrade object 'subsection_grade', # SubsectionGrade object
] ]
) )
# Signal that indicates that a user's score for a subsection has been overridden.
# This signal is generated when an admin changes a user's exam attempt state to
# rejected or to verified from rejected. This signal may also be generated by any
# other client using the GradesService to override subsections in the future.
SUBSECTION_OVERRIDE_CHANGED = Signal(
providing_args=[
'user_id', # Integer User ID
'course_id', # Unicode string representing the course
'usage_id', # Unicode string indicating the courseware instance
'only_if_higher', # Boolean indicating whether updates should be
# made only if the new score is higher than previous.
'modified', # A datetime indicating when the database representation of
# this subsection override score was saved.
'score_deleted', # Boolean indicating whether the override score was
# deleted in this event.
'score_db_table', # The database table that houses the subsection override
# score that was created.
]
)
...@@ -4,11 +4,10 @@ from datetime import datetime ...@@ -4,11 +4,10 @@ from datetime import datetime
from freezegun import freeze_time from freezegun import freeze_time
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from lms.djangoapps.grades.services import GradesService, _get_key from lms.djangoapps.grades.services import GradesService, _get_key
from lms.djangoapps.grades.signals.handlers import SUBSECTION_RESCORE_EVENT_TYPE from lms.djangoapps.grades.signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
from mock import patch from mock import patch, call
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from util.date_utils import to_timestamp
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -46,8 +45,8 @@ class GradesServiceTests(ModuleStoreTestCase): ...@@ -46,8 +45,8 @@ class GradesServiceTests(ModuleStoreTestCase):
earned_graded=5.0, earned_graded=5.0,
possible_graded=5.0 possible_graded=5.0
) )
self.recalc_patcher = patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.apply_async') self.signal_patcher = patch('lms.djangoapps.grades.signals.signals.SUBSECTION_OVERRIDE_CHANGED.send')
self.mock_recalculate = self.recalc_patcher.start() self.mock_signal = self.signal_patcher.start()
self.id_patcher = patch('lms.djangoapps.grades.services.create_new_event_transaction_id') self.id_patcher = patch('lms.djangoapps.grades.services.create_new_event_transaction_id')
self.mock_create_id = self.id_patcher.start() self.mock_create_id = self.id_patcher.start()
self.mock_create_id.return_value = 1 self.mock_create_id.return_value = 1
...@@ -60,7 +59,7 @@ class GradesServiceTests(ModuleStoreTestCase): ...@@ -60,7 +59,7 @@ class GradesServiceTests(ModuleStoreTestCase):
} }
def tearDown(self): def tearDown(self):
self.recalc_patcher.stop() self.signal_patcher.stop()
self.id_patcher.stop() self.id_patcher.stop()
self.type_patcher.stop() self.type_patcher.stop()
self.flag_patcher.stop() self.flag_patcher.stop()
...@@ -177,17 +176,15 @@ class GradesServiceTests(ModuleStoreTestCase): ...@@ -177,17 +176,15 @@ class GradesServiceTests(ModuleStoreTestCase):
self.assertEqual(override_obj.earned_all_override, override['earned_all']) self.assertEqual(override_obj.earned_all_override, override['earned_all'])
self.assertEqual(override_obj.earned_graded_override, override['earned_graded']) self.assertEqual(override_obj.earned_graded_override, override['earned_graded'])
self.assertDictEqual( self.assertEqual(
self.mock_recalculate.call_args[1]['kwargs'], self.mock_signal.call_args,
dict( call(
user_id=self.user.id, user_id=self.user.id,
course_id=unicode(self.course.id), course_id=unicode(self.course.id),
usage_id=unicode(self.subsection.location), usage_id=unicode(self.subsection.location),
only_if_higher=False, only_if_higher=False,
expected_modified_time=to_timestamp(override_obj.modified), modified=override_obj.modified,
score_deleted=False, score_deleted=False,
event_transaction_id=unicode(self.mock_create_id.return_value),
event_transaction_type=SUBSECTION_RESCORE_EVENT_TYPE,
score_db_table=ScoreDatabaseTableEnum.overrides score_db_table=ScoreDatabaseTableEnum.overrides
) )
) )
...@@ -205,17 +202,15 @@ class GradesServiceTests(ModuleStoreTestCase): ...@@ -205,17 +202,15 @@ class GradesServiceTests(ModuleStoreTestCase):
override = self.service.get_subsection_grade_override(self.user.id, self.course.id, self.subsection.location) override = self.service.get_subsection_grade_override(self.user.id, self.course.id, self.subsection.location)
self.assertIsNone(override) self.assertIsNone(override)
self.assertDictEqual( self.assertEqual(
self.mock_recalculate.call_args[1]['kwargs'], self.mock_signal.call_args,
dict( call(
user_id=self.user.id, user_id=self.user.id,
course_id=unicode(self.course.id), course_id=unicode(self.course.id),
usage_id=unicode(self.subsection.location), usage_id=unicode(self.subsection.location),
only_if_higher=False, only_if_higher=False,
expected_modified_time=to_timestamp(datetime.now().replace(tzinfo=pytz.UTC)), modified=datetime.now().replace(tzinfo=pytz.UTC),
score_deleted=True, score_deleted=True,
event_transaction_id=unicode(self.mock_create_id.return_value),
event_transaction_type=SUBSECTION_RESCORE_EVENT_TYPE,
score_db_table=ScoreDatabaseTableEnum.overrides score_db_table=ScoreDatabaseTableEnum.overrides
) )
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment