Commit 4eb5311e by Sanford Student

Separating problem raw score changed and problem weighted score changed signals

For TNL-5993
parent af504fc6
...@@ -1831,14 +1831,15 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems): ...@@ -1831,14 +1831,15 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
self.assertIsNone(student_module.grade) self.assertIsNone(student_module.grade)
self.assertIsNone(student_module.max_grade) self.assertIsNone(student_module.max_grade)
@patch('lms.djangoapps.grades.signals.handlers.PROBLEM_SCORE_CHANGED.send') @patch('lms.djangoapps.grades.signals.handlers.PROBLEM_RAW_SCORE_CHANGED.send')
def test_score_change_signal(self, send_mock): def test_score_change_signal(self, send_mock):
"""Test that a Django signal is generated when a score changes""" """Test that a Django signal is generated when a score changes"""
self.set_module_grade_using_publish(self.grade_dict) self.set_module_grade_using_publish(self.grade_dict)
expected_signal_kwargs = { expected_signal_kwargs = {
'sender': None, 'sender': None,
'points_possible': self.grade_dict['max_value'], 'raw_possible': self.grade_dict['max_value'],
'points_earned': self.grade_dict['value'], 'raw_earned': self.grade_dict['value'],
'weight': None,
'user_id': self.student_user.id, 'user_id': self.student_user.id,
'course_id': unicode(self.course.id), 'course_id': unicode(self.course.id),
'usage_id': unicode(self.problem.location), 'usage_id': unicode(self.problem.location),
......
...@@ -153,7 +153,9 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl ...@@ -153,7 +153,9 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
self.student_user = User.objects.get(email=self.student) self.student_user = User.objects.get(email=self.student)
self.factory = RequestFactory() self.factory = RequestFactory()
# Disable the score change signal to prevent other components from being pulled into tests. # Disable the score change signal to prevent other components from being pulled into tests.
self.score_changed_signal_patch = patch('lms.djangoapps.grades.signals.handlers.PROBLEM_SCORE_CHANGED.send') self.score_changed_signal_patch = patch(
'lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send'
)
self.score_changed_signal_patch.start() self.score_changed_signal_patch.start()
def tearDown(self): def tearDown(self):
...@@ -162,7 +164,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl ...@@ -162,7 +164,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
def _stop_signal_patch(self): def _stop_signal_patch(self):
""" """
Stops the signal patch for the PROBLEM_SCORE_CHANGED event. Stops the signal patch for the PROBLEM_WEIGHTED_SCORE_CHANGED event.
In case a test wants to test with the event actually In case a test wants to test with the event actually
firing. firing.
""" """
......
...@@ -4,15 +4,15 @@ Signal handlers for the gating djangoapp ...@@ -4,15 +4,15 @@ Signal handlers for the gating djangoapp
from django.dispatch import receiver from django.dispatch import receiver
from gating import api as gating_api from gating import api as gating_api
from lms.djangoapps.grades.signals.signals import PROBLEM_SCORE_CHANGED from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
@receiver(PROBLEM_SCORE_CHANGED) @receiver(PROBLEM_WEIGHTED_SCORE_CHANGED)
def handle_score_changed(**kwargs): def handle_score_changed(**kwargs):
""" """
Receives the PROBLEM_SCORE_CHANGED signal sent by LMS when a student's score has changed Receives the PROBLEM_WEIGHTED_SCORE_CHANGED signal sent by LMS when a student's score has changed
for a given component and triggers the evaluation of any milestone relationships for a given component and triggers the evaluation of any milestone relationships
which are attached to the updated content. which are attached to the updated content.
......
...@@ -10,8 +10,14 @@ from openedx.core.lib.grade_utils import is_score_higher ...@@ -10,8 +10,14 @@ from openedx.core.lib.grade_utils import is_score_higher
from student.models import user_by_anonymous_id from student.models import user_by_anonymous_id
from submissions.models import score_set, score_reset from submissions.models import score_set, score_reset
from .signals import PROBLEM_SCORE_CHANGED, SUBSECTION_SCORE_CHANGED, SCORE_PUBLISHED from .signals import (
PROBLEM_RAW_SCORE_CHANGED,
PROBLEM_WEIGHTED_SCORE_CHANGED,
SUBSECTION_SCORE_CHANGED,
SCORE_PUBLISHED,
)
from ..new.course_grade import CourseGradeFactory from ..new.course_grade import CourseGradeFactory
from ..scores import weighted_score
from ..tasks import recalculate_subsection_grade from ..tasks import recalculate_subsection_grade
...@@ -22,9 +28,9 @@ log = getLogger(__name__) ...@@ -22,9 +28,9 @@ log = getLogger(__name__)
def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-argument def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-argument
""" """
Consume the score_set signal defined in the Submissions API, and convert it Consume the score_set signal defined in the Submissions API, and convert it
to a PROBLEM_SCORE_CHANGED signal defined in this module. Converts the unicode keys to a PROBLEM_WEIGHTED_SCORE_CHANGED signal defined in this module. Converts the
for user, course and item into the standard representation for the unicode keys for user, course and item into the standard representation for the
PROBLEM_SCORE_CHANGED signal. PROBLEM_WEIGHTED_SCORE_CHANGED signal.
This method expects that the kwargs dictionary will contain the following This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_set): entries (See the definition of score_set):
...@@ -42,10 +48,10 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a ...@@ -42,10 +48,10 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
if user is None: if user is None:
return return
PROBLEM_SCORE_CHANGED.send( PROBLEM_WEIGHTED_SCORE_CHANGED.send(
sender=None, sender=None,
points_earned=points_earned, weighted_earned=points_earned,
points_possible=points_possible, weighted_possible=points_possible,
user_id=user.id, user_id=user.id,
course_id=course_id, course_id=course_id,
usage_id=usage_id usage_id=usage_id
...@@ -56,9 +62,9 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a ...@@ -56,9 +62,9 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused-argument def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused-argument
""" """
Consume the score_reset signal defined in the Submissions API, and convert Consume the score_reset signal defined in the Submissions API, and convert
it to a PROBLEM_SCORE_CHANGED signal indicating that the score has been set to 0/0. it to a PROBLEM_WEIGHTED_SCORE_CHANGED signal indicating that the score
Converts the unicode keys for user, course and item into the standard has been set to 0/0. Converts the unicode keys for user, course and item
representation for the PROBLEM_SCORE_CHANGED signal. into the standard representation for the PROBLEM_WEIGHTED_SCORE_CHANGED signal.
This method expects that the kwargs dictionary will contain the following This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_reset): entries (See the definition of score_reset):
...@@ -72,10 +78,10 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused ...@@ -72,10 +78,10 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
if user is None: if user is None:
return return
PROBLEM_SCORE_CHANGED.send( PROBLEM_WEIGHTED_SCORE_CHANGED.send(
sender=None, sender=None,
points_earned=0, weighted_earned=0,
points_possible=0, weighted_possible=0,
user_id=user.id, user_id=user.id,
course_id=course_id, course_id=course_id,
usage_id=usage_id usage_id=usage_id
...@@ -106,10 +112,11 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_ ...@@ -106,10 +112,11 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
if update_score: if update_score:
set_score(user.id, block.location, raw_earned, raw_possible) set_score(user.id, block.location, raw_earned, raw_possible)
PROBLEM_SCORE_CHANGED.send( PROBLEM_RAW_SCORE_CHANGED.send(
sender=None, sender=None,
points_earned=raw_earned, raw_earned=raw_earned,
points_possible=raw_possible, raw_possible=raw_possible,
weight=getattr(block, 'weight', None),
user_id=user.id, user_id=user.id,
course_id=unicode(block.location.course_key), course_id=unicode(block.location.course_key),
usage_id=unicode(block.location), usage_id=unicode(block.location),
...@@ -118,10 +125,38 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_ ...@@ -118,10 +125,38 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
return update_score return update_score
@receiver(PROBLEM_SCORE_CHANGED) @receiver(PROBLEM_RAW_SCORE_CHANGED)
def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument
"""
Handles the raw score changed signal, converting the score to a
weighted score and firing the PROBLEM_WEIGHTED_SCORE_CHANGED signal.
"""
if kwargs['raw_possible'] is not None:
weighted_earned, weighted_possible = weighted_score(
kwargs['raw_earned'],
kwargs['raw_possible'],
kwargs['weight'],
)
else: # TODO: remove as part of TNL-5982
weighted_earned, weighted_possible = kwargs['raw_earned'], kwargs['raw_possible']
PROBLEM_WEIGHTED_SCORE_CHANGED.send(
sender=None,
weighted_earned=weighted_earned,
weighted_possible=weighted_possible,
user_id=kwargs['user_id'],
course_id=kwargs['course_id'],
usage_id=kwargs['usage_id'],
only_if_higher=kwargs['only_if_higher'],
score_deleted=kwargs.get('score_deleted', False),
)
@receiver(PROBLEM_WEIGHTED_SCORE_CHANGED)
def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argument def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argument
""" """
Handles the PROBLEM_SCORE_CHANGED signal by enqueueing a subsection update operation to occur asynchronously. Handles the PROBLEM_WEIGHTED_SCORE_CHANGED signal by
enqueueing a subsection update operation to occur asynchronously.
""" """
result = recalculate_subsection_grade.apply_async( result = recalculate_subsection_grade.apply_async(
kwargs=dict( kwargs=dict(
...@@ -129,8 +164,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum ...@@ -129,8 +164,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
course_id=kwargs['course_id'], course_id=kwargs['course_id'],
usage_id=kwargs['usage_id'], usage_id=kwargs['usage_id'],
only_if_higher=kwargs.get('only_if_higher'), only_if_higher=kwargs.get('only_if_higher'),
raw_earned=kwargs.get('points_earned'), weighted_earned=kwargs.get('weighted_earned'),
raw_possible=kwargs.get('points_possible'), weighted_possible=kwargs.get('weighted_possible'),
score_deleted=kwargs.get('score_deleted', False), score_deleted=kwargs.get('score_deleted', False),
) )
) )
......
...@@ -4,19 +4,39 @@ Grades related signals. ...@@ -4,19 +4,39 @@ Grades related signals.
from django.dispatch import Signal from django.dispatch import Signal
# Signal that indicates that a user's score for a problem has been updated. # Signal that indicates that a user's raw score for a problem has been updated.
# This signal is generated when a scoring event occurs either within the core # This signal is generated when a scoring event occurs within the core
# platform or in the Submissions module. Note that this signal will be triggered # platform. Note that this signal will be triggered
# regardless of the new and previous values of the score (i.e. it may be the # regardless of the new and previous values of the score (i.e. it may be the
# case that this signal is generated when a user re-attempts a problem but # case that this signal is generated when a user re-attempts a problem but
# receives the same score). # receives the same score).
PROBLEM_SCORE_CHANGED = Signal( PROBLEM_RAW_SCORE_CHANGED = Signal(
providing_args=[ providing_args=[
'user_id', # Integer User ID 'user_id', # Integer User ID
'course_id', # Unicode string representing the course 'course_id', # Unicode string representing the course
'usage_id', # Unicode string indicating the courseware instance 'usage_id', # Unicode string indicating the courseware instance
'points_earned', # Score obtained by the user 'raw_earned', # Score obtained by the user
'points_possible', # Maximum score available for the exercise 'raw_possible', # Maximum score available for the exercise
'weight', # Weight of the problem
'only_if_higher', # Boolean indicating whether updates should be
# made only if the new score is higher than previous.
]
)
# Signal that indicates that a user's weighted score for a problem has been updated.
# This signal is generated when a scoring event occurs in the Submissions module
# or a PROBLEM_RAW_SCORE_CHANGED event is handled in the core platform.
# Note that this signal will be triggered
# regardless of the new and previous values of the score (i.e. it may be the
# case that this signal is generated when a user re-attempts a problem but
# receives the same score).
PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
providing_args=[
'user_id', # Integer User ID
'course_id', # Unicode string representing the course
'usage_id', # Unicode string indicating the courseware instance
'weighted_earned', # Score obtained by the user
'weighted_possible', # Maximum score available for the exercise
'only_if_higher', # Boolean indicating whether updates should be 'only_if_higher', # Boolean indicating whether updates should be
# made only if the new score is higher than previous. # made only if the new score is higher than previous.
] ]
...@@ -25,8 +45,8 @@ PROBLEM_SCORE_CHANGED = Signal( ...@@ -25,8 +45,8 @@ PROBLEM_SCORE_CHANGED = Signal(
# Signal that indicates that a user's score for a problem has been published # Signal that indicates that a user's score for a problem has been published
# for possible persistence and update. Typically, most clients should listen # for possible persistence and update. Typically, most clients should listen
# to the PROBLEM_SCORE_CHANGED signal instead, since that is signalled only after the # to the PROBLEM_WEIGHTED_SCORE_CHANGED signal instead, since that is signalled
# problem's score is changed. # only after the problem's score is changed.
SCORE_PUBLISHED = Signal( SCORE_PUBLISHED = Signal(
providing_args=[ providing_args=[
'block', # Course block object 'block', # Course block object
...@@ -40,8 +60,8 @@ SCORE_PUBLISHED = Signal( ...@@ -40,8 +60,8 @@ SCORE_PUBLISHED = Signal(
# Signal that indicates that a user's score for a subsection has been updated. # Signal that indicates that a user's score for a subsection has been updated.
# This is a downstream signal of PROBLEM_SCORE_CHANGED sent for each affected containing # This is a downstream signal of PROBLEM_WEIGHTED_SCORE_CHANGED sent for each
# subsection. # affected containing subsection.
SUBSECTION_SCORE_CHANGED = Signal( SUBSECTION_SCORE_CHANGED = Signal(
providing_args=[ providing_args=[
'course', # Course object 'course', # Course object
......
...@@ -23,7 +23,9 @@ log = getLogger(__name__) ...@@ -23,7 +23,9 @@ log = getLogger(__name__)
@task(default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY) @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, **kwargs): def recalculate_subsection_grade(
user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_possible, **kwargs
):
""" """
Updates a saved subsection grade. Updates a saved subsection grade.
...@@ -34,9 +36,9 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r ...@@ -34,9 +36,9 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
only_if_higher (boolean): indicating whether grades should only_if_higher (boolean): indicating whether grades should
be updated only if the new raw_earned is higher than the be updated only if the new raw_earned is higher than the
previous value. previous value.
raw_earned (float): the raw points the learner earned on the weighted_earned (float): the weighted points the learner earned on the
problem that triggered the update. problem that triggered the update.
raw_possible (float): the max raw points the leaner could have weighted_possible (float): the max weighted points the leaner could have
earned on the problem. earned on the problem.
score_deleted (boolean): indicating whether the grade change is score_deleted (boolean): indicating whether the grade change is
a result of the problem's score being deleted. a result of the problem's score being deleted.
...@@ -53,10 +55,10 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r ...@@ -53,10 +55,10 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
# creator's process hasn't committed before the task initiates in the worker # creator's process hasn't committed before the task initiates in the worker
# process. # process.
if not _has_database_updated_with_new_score( if not _has_database_updated_with_new_score(
user_id, scored_block_usage_key, raw_earned, raw_possible, score_deleted, user_id, scored_block_usage_key, weighted_earned, weighted_possible, score_deleted,
): ):
raise _retry_recalculate_subsection_grade( raise _retry_recalculate_subsection_grade(
user_id, course_id, usage_id, only_if_higher, raw_earned, raw_possible, score_deleted, user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_possible, score_deleted,
) )
_update_subsection_grades( _update_subsection_grades(
...@@ -66,8 +68,8 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r ...@@ -66,8 +68,8 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
course_id, course_id,
user_id, user_id,
usage_id, usage_id,
raw_earned, weighted_earned,
raw_possible, weighted_possible,
score_deleted, score_deleted,
) )
...@@ -100,8 +102,8 @@ def _update_subsection_grades( ...@@ -100,8 +102,8 @@ def _update_subsection_grades(
course_id, course_id,
user_id, user_id,
usage_id, usage_id,
raw_earned, weighted_earned,
raw_possible, weighted_possible,
score_deleted, score_deleted,
): ):
""" """
...@@ -138,12 +140,12 @@ def _update_subsection_grades( ...@@ -138,12 +140,12 @@ def _update_subsection_grades(
except DatabaseError as exc: except DatabaseError as exc:
raise _retry_recalculate_subsection_grade( raise _retry_recalculate_subsection_grade(
user_id, course_id, usage_id, only_if_higher, raw_earned, raw_possible, score_deleted, exc, user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_possible, score_deleted, exc,
) )
def _retry_recalculate_subsection_grade( def _retry_recalculate_subsection_grade(
user_id, course_id, usage_id, only_if_higher, raw_earned, raw_possible, score_deleted, exc=None, user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_possible, score_deleted, exc=None,
): ):
""" """
Calls retry for the recalculate_subsection_grade task with the Calls retry for the recalculate_subsection_grade task with the
...@@ -155,8 +157,8 @@ def _retry_recalculate_subsection_grade( ...@@ -155,8 +157,8 @@ def _retry_recalculate_subsection_grade(
course_id=course_id, course_id=course_id,
usage_id=usage_id, usage_id=usage_id,
only_if_higher=only_if_higher, only_if_higher=only_if_higher,
raw_earned=raw_earned, weighted_earned=weighted_earned,
raw_possible=raw_possible, weighted_possible=weighted_possible,
score_deleted=score_deleted, score_deleted=score_deleted,
), ),
exc=exc, exc=exc,
......
...@@ -12,6 +12,7 @@ from ..signals.handlers import ( ...@@ -12,6 +12,7 @@ from ..signals.handlers import (
enqueue_subsection_update, enqueue_subsection_update,
submissions_score_set_handler, submissions_score_set_handler,
submissions_score_reset_handler, submissions_score_reset_handler,
problem_raw_score_changed_handler,
) )
UUID_REGEX = re.compile(ur'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{12}' % {'hex': u'[0-9a-f]'}) UUID_REGEX = re.compile(ur'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{12}' % {'hex': u'[0-9a-f]'})
...@@ -31,22 +32,39 @@ SUBMISSION_RESET_KWARGS = { ...@@ -31,22 +32,39 @@ SUBMISSION_RESET_KWARGS = {
'item_id': 'i4x://org/course/usage/123456' 'item_id': 'i4x://org/course/usage/123456'
} }
PROBLEM_RAW_SCORE_CHANGED_KWARGS = {
'raw_earned': 1.0,
'raw_possible': 2.0,
'weight': 4,
'user_id': 'UserID',
'course_id': 'CourseID',
'usage_id': 'i4x://org/course/usage/123456',
'only_if_higher': False,
'score_deleted': True,
}
@ddt.ddt @ddt.ddt
class SubmissionSignalRelayTest(TestCase): class ScoreChangedSignalRelayTest(TestCase):
""" """
Tests to ensure that the courseware module correctly catches score_set and Tests to ensure that the courseware module correctly catches
score_reset signals from the Submissions API and recasts them as LMS (a) score_set and score_reset signals from the Submissions API
signals. This ensures that listeners in the LMS only have to handle one type (b) LMS PROBLEM_RAW_SCORE_CHANGED signals
of signal for all scoring events. and recasts them as LMS PROBLEM_WEIGHTED_SCORE_CHANGED signals.
This ensures that listeners in the LMS only have to handle one type
of signal for all scoring events regardless of their origin.
""" """
def setUp(self): def setUp(self):
""" """
Configure mocks for all the dependencies of the render method Configure mocks for all the dependencies of the render method
""" """
super(SubmissionSignalRelayTest, self).setUp() super(ScoreChangedSignalRelayTest, self).setUp()
self.signal_mock = self.setup_patch('lms.djangoapps.grades.signals.signals.PROBLEM_SCORE_CHANGED.send', None) self.signal_mock = self.setup_patch(
'lms.djangoapps.grades.signals.signals.PROBLEM_WEIGHTED_SCORE_CHANGED.send',
None,
)
self.user_mock = MagicMock() self.user_mock = MagicMock()
self.user_mock.id = 42 self.user_mock.id = 42
self.get_user_mock = self.setup_patch( self.get_user_mock = self.setup_patch(
...@@ -72,15 +90,16 @@ class SubmissionSignalRelayTest(TestCase): ...@@ -72,15 +90,16 @@ class SubmissionSignalRelayTest(TestCase):
def test_score_set_signal_handler(self, handler, kwargs, earned, possible): def test_score_set_signal_handler(self, handler, kwargs, earned, possible):
""" """
Ensure that on receipt of a score_(re)set signal from the Submissions API, Ensure that on receipt of a score_(re)set signal from the Submissions API,
the signal handler correctly converts it to a PROBLEM_SCORE_CHANGED signal. the signal handler correctly converts it to a PROBLEM_WEIGHTED_SCORE_CHANGED
signal.
Also ensures that the handler calls user_by_anonymous_id correctly. Also ensures that the handler calls user_by_anonymous_id correctly.
""" """
handler(None, **kwargs) handler(None, **kwargs)
expected_set_kwargs = { expected_set_kwargs = {
'sender': None, 'sender': None,
'points_possible': possible, 'weighted_possible': possible,
'points_earned': earned, 'weighted_earned': earned,
'user_id': self.user_mock.id, 'user_id': self.user_mock.id,
'course_id': 'CourseID', 'course_id': 'CourseID',
'usage_id': 'i4x://org/course/usage/123456' 'usage_id': 'i4x://org/course/usage/123456'
...@@ -122,6 +141,36 @@ class SubmissionSignalRelayTest(TestCase): ...@@ -122,6 +141,36 @@ class SubmissionSignalRelayTest(TestCase):
handler(None, **kwargs) handler(None, **kwargs)
self.signal_mock.assert_not_called() self.signal_mock.assert_not_called()
def test_raw_score_changed_signal_handler(self):
problem_raw_score_changed_handler(None, **PROBLEM_RAW_SCORE_CHANGED_KWARGS)
expected_set_kwargs = {
'sender': None,
'weighted_earned': 2.0,
'weighted_possible': 4.0,
'user_id': 'UserID',
'course_id': 'CourseID',
'usage_id': 'i4x://org/course/usage/123456',
'only_if_higher': False,
'score_deleted': True,
}
self.signal_mock.assert_called_with(**expected_set_kwargs)
def test_raw_score_changed_score_deleted_optional(self):
local_kwargs = PROBLEM_RAW_SCORE_CHANGED_KWARGS.copy()
del local_kwargs['score_deleted']
problem_raw_score_changed_handler(None, **local_kwargs)
expected_set_kwargs = {
'sender': None,
'weighted_earned': 2.0,
'weighted_possible': 4.0,
'user_id': 'UserID',
'course_id': 'CourseID',
'usage_id': 'i4x://org/course/usage/123456',
'only_if_higher': False,
'score_deleted': False,
}
self.signal_mock.assert_called_with(**expected_set_kwargs)
@patch('lms.djangoapps.grades.signals.handlers.log.info') @patch('lms.djangoapps.grades.signals.handlers.log.info')
def test_problem_score_changed_logging(self, mocklog): def test_problem_score_changed_logging(self, mocklog):
enqueue_subsection_update( enqueue_subsection_update(
......
...@@ -18,7 +18,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -18,7 +18,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
from lms.djangoapps.grades.signals.signals import PROBLEM_SCORE_CHANGED from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from lms.djangoapps.grades.tasks import recalculate_subsection_grade from lms.djangoapps.grades.tasks import recalculate_subsection_grade
...@@ -50,9 +50,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -50,9 +50,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequential1") self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequential1")
self.problem = ItemFactory.create(parent=self.sequential, category='problem', display_name='Problem') self.problem = ItemFactory.create(parent=self.sequential, category='problem', display_name='Problem')
self.problem_score_changed_kwargs = OrderedDict([ self.problem_weighted_score_changed_kwargs = OrderedDict([
('points_earned', 1.0), ('weighted_earned', 1.0),
('points_possible', 2.0), ('weighted_possible', 2.0),
('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.problem.location)), ('usage_id', unicode(self.problem.location)),
...@@ -64,8 +64,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -64,8 +64,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
('course_id', unicode(self.course.id)), ('course_id', unicode(self.course.id)),
('usage_id', unicode(self.problem.location)), ('usage_id', unicode(self.problem.location)),
('only_if_higher', None), ('only_if_higher', None),
('raw_earned', 1.0), ('weighted_earned', 1.0),
('raw_possible', 2.0), ('weighted_possible', 2.0),
('score_deleted', False), ('score_deleted', False),
]) ])
...@@ -82,17 +82,17 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -82,17 +82,17 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with patch("lms.djangoapps.grades.tasks.get_score", return_value=score): with patch("lms.djangoapps.grades.tasks.get_score", return_value=score):
yield yield
def test_problem_score_changed_queues_task(self): def test_problem_weighted_score_changed_queues_task(self):
""" """
Ensures that the PROBLEM_SCORE_CHANGED signal enqueues the correct task. Ensures that the PROBLEM_WEIGHTED_SCORE_CHANGED signal enqueues the correct task.
""" """
self.set_up_course() self.set_up_course()
send_args = self.problem_score_changed_kwargs send_args = self.problem_weighted_score_changed_kwargs
with self.mock_get_score() and patch( with self.mock_get_score() and patch(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async', 'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async',
return_value=None return_value=None
) as mock_task_apply: ) as mock_task_apply:
PROBLEM_SCORE_CHANGED.send(sender=None, **send_args) PROBLEM_WEIGHTED_SCORE_CHANGED.send(sender=None, **send_args)
mock_task_apply.assert_called_once_with(kwargs=self.recalculate_subsection_grade_kwargs) mock_task_apply.assert_called_once_with(kwargs=self.recalculate_subsection_grade_kwargs)
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send') @patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
......
...@@ -15,8 +15,7 @@ from django.utils.translation import override as override_language ...@@ -15,8 +15,7 @@ from django.utils.translation import override as override_language
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.models import StudentModule from courseware.models import StudentModule
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from lms.djangoapps.grades.scores import weighted_score from lms.djangoapps.grades.signals.signals import PROBLEM_RAW_SCORE_CHANGED
from lms.djangoapps.grades.signals.signals import PROBLEM_SCORE_CHANGED
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.models import UserPreference from openedx.core.djangoapps.user_api.models import UserPreference
...@@ -290,28 +289,22 @@ def _reset_module_attempts(studentmodule): ...@@ -290,28 +289,22 @@ def _reset_module_attempts(studentmodule):
def _fire_score_changed_for_block(course_id, student, block, module_state_key): def _fire_score_changed_for_block(course_id, student, block, module_state_key):
""" """
Fires a PROBLEM_SCORE_CHANGED event for the given module. The earned points are Fires a PROBLEM_RAW_SCORE_CHANGED event for the given module.
always zero. We must retrieve the possible points from the XModule, as The earned points are always zero. We must retrieve the possible points
noted below. from the XModule, as noted below.
""" """
if block and block.has_score: if block and block.has_score and block.max_score() is not None:
max_score = block.max_score() PROBLEM_RAW_SCORE_CHANGED.send(
if max_score is None: sender=None,
return raw_possible=0,
else: raw_earned=block.max_score(),
points_earned, points_possible = weighted_score(0, max_score, getattr(block, 'weight', None)) weight=getattr(block, 'weight', None),
else: user_id=student.id,
points_earned, points_possible = 0, 0 course_id=unicode(course_id),
usage_id=unicode(module_state_key),
PROBLEM_SCORE_CHANGED.send( score_deleted=True,
sender=None, only_if_higher=False,
points_possible=points_possible, )
points_earned=points_earned,
user_id=student.id,
course_id=unicode(course_id),
usage_id=unicode(module_state_key),
score_deleted=True,
)
def get_email_params(course, auto_enroll, secure=True, course_key=None, display_name=None): def get_email_params(course, auto_enroll, secure=True, course_key=None, display_name=None):
......
...@@ -3190,7 +3190,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes ...@@ -3190,7 +3190,7 @@ class TestInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginEnrollmentTes
}) })
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
@patch('lms.djangoapps.grades.signals.handlers.PROBLEM_SCORE_CHANGED.send') @patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
def test_reset_student_attempts_delete(self, _mock_signal): def test_reset_student_attempts_delete(self, _mock_signal):
""" Test delete single student state. """ """ Test delete single student state. """
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()})
......
...@@ -374,7 +374,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): ...@@ -374,7 +374,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user) reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user)
self.assertEqual(json.loads(module().state)['attempts'], 0) self.assertEqual(json.loads(module().state)['attempts'], 0)
@mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_SCORE_CHANGED.send') @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
def test_delete_student_attempts(self, _mock_signal): def test_delete_student_attempts(self, _mock_signal):
msk = self.course_key.make_usage_key('dummy', 'module') msk = self.course_key.make_usage_key('dummy', 'module')
original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'})
...@@ -400,7 +400,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): ...@@ -400,7 +400,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
# Disable the score change signal to prevent other components from being # Disable the score change signal to prevent other components from being
# pulled into tests. # pulled into tests.
@mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_SCORE_CHANGED.send') @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
def test_delete_submission_scores(self, _mock_signal): def test_delete_submission_scores(self, _mock_signal):
user = UserFactory() user = UserFactory()
problem_location = self.course_key.make_usage_key('dummy', 'module') problem_location = self.course_key.make_usage_key('dummy', 'module')
......
...@@ -49,7 +49,7 @@ class InstructorServiceTests(SharedModuleStoreTestCase): ...@@ -49,7 +49,7 @@ class InstructorServiceTests(SharedModuleStoreTestCase):
state=json.dumps({'attempts': 2}), state=json.dumps({'attempts': 2}),
) )
@mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_SCORE_CHANGED.send') @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
def test_reset_student_attempts_delete(self, _mock_signal): def test_reset_student_attempts_delete(self, _mock_signal):
""" """
Test delete student state. Test delete student state.
......
...@@ -8,7 +8,7 @@ from django.dispatch import receiver ...@@ -8,7 +8,7 @@ from django.dispatch import receiver
import logging import logging
from lms.djangoapps.grades import progress from lms.djangoapps.grades import progress
from lms.djangoapps.grades.signals.signals import PROBLEM_SCORE_CHANGED from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from lms import CELERY_APP from lms import CELERY_APP
from lti_provider.models import GradedAssignment from lti_provider.models import GradedAssignment
import lti_provider.outcomes as outcomes import lti_provider.outcomes as outcomes
...@@ -19,14 +19,14 @@ from xmodule.modulestore.django import modulestore ...@@ -19,14 +19,14 @@ from xmodule.modulestore.django import modulestore
log = logging.getLogger("edx.lti_provider") log = logging.getLogger("edx.lti_provider")
@receiver(PROBLEM_SCORE_CHANGED) @receiver(PROBLEM_WEIGHTED_SCORE_CHANGED)
def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument
""" """
Consume signals that indicate score changes. See the definition of Consume signals that indicate score changes. See the definition of
PROBLEM_SCORE_CHANGED for a description of the signal. PROBLEM_WEIGHTED_SCORE_CHANGED for a description of the signal.
""" """
points_possible = kwargs.get('points_possible', None) points_possible = kwargs.get('weighted_possible', None)
points_earned = kwargs.get('points_earned', None) points_earned = kwargs.get('weighted_earned', None)
user_id = kwargs.get('user_id', None) user_id = kwargs.get('user_id', None)
course_id = kwargs.get('course_id', None) course_id = kwargs.get('course_id', None)
usage_id = kwargs.get('usage_id', None) usage_id = kwargs.get('usage_id', None)
......
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