Commit 6a473114 by Eric Fischer

Update Persisted Course Grade on Subsection Grade update

Makes use of the new SUBSECTION_SCORE_CHANGED signal to trigger a task that
updates persisted course grade values. We've also renamed SCORE_CHANGED to
PROBLEM_SCORE_CHANGED to head off any issues with unclear signal names.

TNL-5740
parent ba491a64
...@@ -1831,7 +1831,7 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems): ...@@ -1831,7 +1831,7 @@ 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.SCORE_CHANGED.send') @patch('lms.djangoapps.grades.signals.handlers.PROBLEM_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)
......
...@@ -153,7 +153,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl ...@@ -153,7 +153,7 @@ 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.SCORE_CHANGED.send') self.score_changed_signal_patch = patch('lms.djangoapps.grades.signals.handlers.PROBLEM_SCORE_CHANGED.send')
self.score_changed_signal_patch.start() self.score_changed_signal_patch.start()
def tearDown(self): def tearDown(self):
...@@ -162,7 +162,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl ...@@ -162,7 +162,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
def _stop_signal_patch(self): def _stop_signal_patch(self):
""" """
Stops the signal patch for the SCORE_CHANGED event. Stops the signal patch for the PROBLEM_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 SCORE_CHANGED, SUBSECTION_SCORE_CHANGED from lms.djangoapps.grades.signals.signals import PROBLEM_SCORE_CHANGED, SUBSECTION_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(SCORE_CHANGED) @receiver(PROBLEM_SCORE_CHANGED)
def handle_score_changed(**kwargs): def handle_score_changed(**kwargs):
""" """
Receives the SCORE_CHANGED signal sent by LMS when a student's score has changed Receives the PROBLEM_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.
......
...@@ -260,7 +260,14 @@ class CourseGradeFactory(object): ...@@ -260,7 +260,14 @@ class CourseGradeFactory(object):
self._compute_and_update_grade(course, course_structure, read_only) self._compute_and_update_grade(course, course_structure, read_only)
) )
def _compute_and_update_grade(self, course, course_structure, read_only): def update(self, course):
"""
Updates the CourseGrade for this Factory's student.
"""
course_structure = get_course_blocks(self.student, course.location)
self._compute_and_update_grade(course, course_structure)
def _compute_and_update_grade(self, course, course_structure, read_only=False):
""" """
Freshly computes and updates the grade for the student and course. Freshly computes and updates the grade for the student and course.
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Grades related signals. Grades related signals.
""" """
from celery import Task
from django.dispatch import receiver from django.dispatch import receiver
from logging import getLogger from logging import getLogger
...@@ -10,8 +11,8 @@ from openedx.core.lib.grade_utils import is_score_higher ...@@ -10,8 +11,8 @@ 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 SCORE_CHANGED, SCORE_PUBLISHED from .signals import PROBLEM_SCORE_CHANGED, SUBSECTION_SCORE_CHANGED, SCORE_PUBLISHED
from ..tasks import recalculate_subsection_grade from ..tasks import recalculate_subsection_grade, recalculate_course_grade
log = getLogger(__name__) log = getLogger(__name__)
...@@ -21,9 +22,9 @@ log = getLogger(__name__) ...@@ -21,9 +22,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 SCORE_CHANGED signal defined in this module. Converts the unicode keys to a PROBLEM_SCORE_CHANGED signal defined in this module. Converts the unicode keys
for user, course and item into the standard representation for the for user, course and item into the standard representation for the
SCORE_CHANGED signal. PROBLEM_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):
...@@ -41,7 +42,7 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a ...@@ -41,7 +42,7 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
if user is None: if user is None:
return return
SCORE_CHANGED.send( PROBLEM_SCORE_CHANGED.send(
sender=None, sender=None,
points_earned=points_earned, points_earned=points_earned,
points_possible=points_possible, points_possible=points_possible,
...@@ -55,9 +56,9 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a ...@@ -55,9 +56,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 SCORE_CHANGED signal indicating that the score has been set to 0/0. it to a PROBLEM_SCORE_CHANGED signal indicating that the score has been set to 0/0.
Converts the unicode keys for user, course and item into the standard Converts the unicode keys for user, course and item into the standard
representation for the SCORE_CHANGED signal. representation for the PROBLEM_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):
...@@ -71,7 +72,7 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused ...@@ -71,7 +72,7 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
if user is None: if user is None:
return return
SCORE_CHANGED.send( PROBLEM_SCORE_CHANGED.send(
sender=None, sender=None,
points_earned=0, points_earned=0,
points_possible=0, points_possible=0,
...@@ -106,7 +107,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_ ...@@ -106,7 +107,7 @@ 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)
SCORE_CHANGED.send( PROBLEM_SCORE_CHANGED.send(
sender=None, sender=None,
points_earned=raw_earned, points_earned=raw_earned,
points_possible=raw_possible, points_possible=raw_possible,
...@@ -118,10 +119,10 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_ ...@@ -118,10 +119,10 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
return update_score return update_score
@receiver(SCORE_CHANGED) @receiver(PROBLEM_SCORE_CHANGED)
def enqueue_grade_update(sender, **kwargs): # pylint: disable=unused-argument def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argument
""" """
Handles the SCORE_CHANGED signal by enqueueing an update operation to occur asynchronously. Handles the PROBLEM_SCORE_CHANGED signal by enqueueing a subsection update operation to occur asynchronously.
""" """
recalculate_subsection_grade.apply_async( recalculate_subsection_grade.apply_async(
args=( args=(
...@@ -131,3 +132,14 @@ def enqueue_grade_update(sender, **kwargs): # pylint: disable=unused-argument ...@@ -131,3 +132,14 @@ def enqueue_grade_update(sender, **kwargs): # pylint: disable=unused-argument
kwargs.get('only_if_higher'), kwargs.get('only_if_higher'),
) )
) )
@receiver(SUBSECTION_SCORE_CHANGED)
def enqueue_course_update(sender, **kwargs): # pylint: disable=unused-argument
"""
Handles the SUBSECTION_SCORE_CHANGED signal by enqueueing a course update operation to occur asynchronously.
"""
if isinstance(sender, Task): # We're already in a async worker, just do the task
recalculate_course_grade.apply(args=(kwargs['user'].id, unicode(kwargs['course'].id)))
else: # Otherwise, queue the work to be done asynchronously
recalculate_course_grade.apply_async(args=(kwargs['user'].id, unicode(kwargs['course'].id)))
...@@ -10,7 +10,7 @@ from django.dispatch import Signal ...@@ -10,7 +10,7 @@ from django.dispatch import Signal
# 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).
SCORE_CHANGED = Signal( PROBLEM_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
...@@ -25,7 +25,7 @@ SCORE_CHANGED = Signal( ...@@ -25,7 +25,7 @@ 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 SCORE_CHANGED signal instead, since that is signalled only after the # to the PROBLEM_SCORE_CHANGED signal instead, since that is signalled only after the
# problem's score is changed. # problem's score is changed.
SCORE_PUBLISHED = Signal( SCORE_PUBLISHED = Signal(
providing_args=[ providing_args=[
...@@ -40,7 +40,7 @@ SCORE_PUBLISHED = Signal( ...@@ -40,7 +40,7 @@ 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 SCORE_CHANGED sent for each affected containing # This is a downstream signal of PROBLEM_SCORE_CHANGED sent for each affected containing
# subsection. # subsection.
SUBSECTION_SCORE_CHANGED = Signal( SUBSECTION_SCORE_CHANGED = Signal(
providing_args=[ providing_args=[
......
...@@ -13,6 +13,7 @@ from opaque_keys.edx.locator import CourseLocator ...@@ -13,6 +13,7 @@ from opaque_keys.edx.locator import CourseLocator
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .new.course_grade import CourseGradeFactory
from .new.subsection_grade import SubsectionGradeFactory from .new.subsection_grade import SubsectionGradeFactory
from .signals.signals import SUBSECTION_SCORE_CHANGED from .signals.signals import SUBSECTION_SCORE_CHANGED
from .transformer import GradesTransformer from .transformer import GradesTransformer
...@@ -57,7 +58,7 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher): ...@@ -57,7 +58,7 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
only_if_higher, only_if_higher,
) )
SUBSECTION_SCORE_CHANGED.send( SUBSECTION_SCORE_CHANGED.send(
sender=None, sender=recalculate_subsection_grade,
course=course, course=course,
user=student, user=student,
subsection_grade=subsection_grade, subsection_grade=subsection_grade,
...@@ -65,3 +66,21 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher): ...@@ -65,3 +66,21 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher):
except IntegrityError as exc: except IntegrityError as exc:
raise recalculate_subsection_grade.retry(args=[user_id, course_id, usage_id], exc=exc) raise recalculate_subsection_grade.retry(args=[user_id, course_id, usage_id], exc=exc)
@task(default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY)
def recalculate_course_grade(user_id, course_id):
"""
Updates a saved course grade.
This method expects the following parameters:
- user_id: serialized id of applicable User object
- course_id: Unicode string representing the course
"""
student = User.objects.get(id=user_id)
course_key = CourseLocator.from_string(course_id)
course = modulestore().get_course(course_key, depth=0)
try:
CourseGradeFactory(student).update(course)
except IntegrityError as exc:
raise recalculate_course_grade.retry(args=[user_id, course_id], exc=exc)
...@@ -574,6 +574,8 @@ class TestCourseGradeLogging(SharedModuleStoreTestCase): ...@@ -574,6 +574,8 @@ class TestCourseGradeLogging(SharedModuleStoreTestCase):
enabled_for_course=True enabled_for_course=True
): ):
with patch('lms.djangoapps.grades.new.course_grade.log') as log_mock: with patch('lms.djangoapps.grades.new.course_grade.log') as log_mock:
# TODO: once merged with the "glue code" PR, update expected logging to include the relevant new info
# the course grade has not been created, so we expect each grade to be created # the course grade has not been created, so we expect each grade to be created
self._create_course_grade_and_check_logging( self._create_course_grade_and_check_logging(
grade_factory, grade_factory,
......
...@@ -42,7 +42,7 @@ class SubmissionSignalRelayTest(TestCase): ...@@ -42,7 +42,7 @@ class SubmissionSignalRelayTest(TestCase):
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(SubmissionSignalRelayTest, self).setUp()
self.signal_mock = self.setup_patch('lms.djangoapps.grades.signals.signals.SCORE_CHANGED.send', None) self.signal_mock = self.setup_patch('lms.djangoapps.grades.signals.signals.PROBLEM_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(
...@@ -68,7 +68,7 @@ class SubmissionSignalRelayTest(TestCase): ...@@ -68,7 +68,7 @@ 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 SCORE_CHANGED signal. the signal handler correctly converts it to a PROBLEM_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.
""" """
......
...@@ -7,17 +7,20 @@ import ddt ...@@ -7,17 +7,20 @@ import ddt
from django.conf import settings from django.conf import settings
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from mock import patch from mock import patch
from uuid import uuid4
from unittest import skip from unittest import skip
from opaque_keys.edx.locator import CourseLocator
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase 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 SCORE_CHANGED from lms.djangoapps.grades.signals.signals import PROBLEM_SCORE_CHANGED, SUBSECTION_SCORE_CHANGED
from lms.djangoapps.grades.tasks import recalculate_subsection_grade from lms.djangoapps.grades.tasks import recalculate_course_grade, recalculate_subsection_grade
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False}) @patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
...@@ -59,24 +62,91 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -59,24 +62,91 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
_ = anonymous_id_for_user(self.user, self.course.id) _ = anonymous_id_for_user(self.user, self.course.id)
# pylint: enable=attribute-defined-outside-init,no-member # pylint: enable=attribute-defined-outside-init,no-member
def test_score_changed_signal_queues_task(self): @ddt.data(
('lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async', PROBLEM_SCORE_CHANGED),
('lms.djangoapps.grades.tasks.recalculate_course_grade.apply_async', SUBSECTION_SCORE_CHANGED)
)
@ddt.unpack
def test_signal_queues_task(self, enqueue_op, test_signal):
""" """
Ensures that the SCORE_CHANGED signal enqueues a recalculate subsection grade task. Ensures that the PROBLEM_SCORE_CHANGED and SUBSECTION_SCORE_CHANGED signals enqueue the correct tasks.
""" """
self.set_up_course() self.set_up_course()
if test_signal == PROBLEM_SCORE_CHANGED:
send_args = self.score_changed_kwargs
expected_args = tuple(self.score_changed_kwargs.values())
else:
send_args = {'user': self.user, 'course': self.course}
expected_args = (self.score_changed_kwargs['user_id'], self.score_changed_kwargs['course_id'])
with patch( with patch(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async', enqueue_op,
return_value=None return_value=None
) as mock_task_apply: ) as mock_task_apply:
SCORE_CHANGED.send(sender=None, **self.score_changed_kwargs) test_signal.send(sender=None, **send_args)
mock_task_apply.assert_called_once_with(args=tuple(self.score_changed_kwargs.values())) mock_task_apply.assert_called_once_with(args=expected_args)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
def test_subsection_grade_updated(self, default_store): def test_subsection_update_triggers_course_update(self, mock_course_signal):
"""
Ensures that the subsection update operation also updates the course grade.
"""
self.set_up_course()
mock_return = uuid4()
course_key = CourseLocator.from_string(unicode(self.course.id))
course = modulestore().get_course(course_key, depth=0)
with patch(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update',
return_value=mock_return
):
recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values()))
mock_course_signal.assert_called_once_with(
sender=recalculate_subsection_grade,
course=course,
user=self.user,
subsection_grade=mock_return,
)
@ddt.data(True, False)
def test_course_update_enqueuing(self, should_be_async):
"""
Ensures that the course update operation is enqueued on an async queue (or not) as expected.
"""
base = 'lms.djangoapps.grades.tasks.recalculate_course_grade'
if should_be_async:
executed = base + '.apply_async'
other = base + '.apply'
sender = None
else:
executed = base + '.apply'
other = base + '.apply_async'
sender = recalculate_subsection_grade
self.set_up_course()
with patch(executed) as executed_task:
with patch(other) as other_task:
SUBSECTION_SCORE_CHANGED.send(
sender=sender,
course=self.course,
user=self.user,
)
other_task.assert_not_called()
executed_task.assert_called_once_with(
args=(
self.score_changed_kwargs['user_id'],
self.score_changed_kwargs['course_id'],
)
)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1),
(ModuleStoreEnum.Type.split, 0),
)
@ddt.unpack
def test_subsection_grade_updated(self, default_store, added_queries):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with check_mongo_calls(2) and self.assertNumQueries(13): with check_mongo_calls(2) and self.assertNumQueries(21 + added_queries):
recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values())) recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values()))
def test_single_call_to_create_block_structure(self): def test_single_call_to_create_block_structure(self):
...@@ -87,16 +157,20 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -87,16 +157,20 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
return_value=None, return_value=None,
) as mock_block_structure_create: ) as mock_block_structure_create:
recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values())) recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values()))
self.assertEquals(mock_block_structure_create.call_count, 1) self.assertEquals(mock_block_structure_create.call_count, 2)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(
def test_query_count_does_not_change_with_more_problems(self, default_store): (ModuleStoreEnum.Type.mongo, 1),
(ModuleStoreEnum.Type.split, 0),
)
@ddt.unpack
def test_query_count_does_not_change_with_more_problems(self, default_store, added_queries):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2')
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3')
with check_mongo_calls(2) and self.assertNumQueries(13): with check_mongo_calls(2) and self.assertNumQueries(21 + added_queries):
recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values())) recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values()))
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
...@@ -104,7 +178,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -104,7 +178,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course(enable_subsection_grades=False) self.set_up_course(enable_subsection_grades=False)
self.assertFalse(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertFalse(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with check_mongo_calls(2) and self.assertNumQueries(5): additional_queries = 1 if default_store == ModuleStoreEnum.Type.mongo else 0
with check_mongo_calls(2) and self.assertNumQueries(12 + additional_queries):
recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values())) recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values()))
@skip("Pending completion of TNL-5089") @skip("Pending completion of TNL-5089")
...@@ -124,7 +199,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -124,7 +199,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry') @patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update') @patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
def test_retry_on_integrity_error(self, mock_update, mock_retry): def test_retry_subsection_update_on_integrity_error(self, mock_update, mock_retry):
""" """
Ensures that tasks will be retried if IntegrityErrors are encountered. Ensures that tasks will be retried if IntegrityErrors are encountered.
""" """
...@@ -132,3 +207,19 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -132,3 +207,19 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
mock_update.side_effect = IntegrityError("WHAMMY") mock_update.side_effect = IntegrityError("WHAMMY")
recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values())) recalculate_subsection_grade.apply(args=tuple(self.score_changed_kwargs.values()))
self.assertTrue(mock_retry.called) self.assertTrue(mock_retry.called)
@patch('lms.djangoapps.grades.tasks.recalculate_course_grade.retry')
@patch('lms.djangoapps.grades.new.course_grade.CourseGradeFactory.update')
def test_retry_course_update_on_integrity_error(self, mock_update, mock_retry):
"""
Ensures that tasks will be retried if IntegrityErrors are encountered.
"""
self.set_up_course()
mock_update.side_effect = IntegrityError("WHAMMY")
recalculate_course_grade.apply(
args=(
self.score_changed_kwargs['user_id'],
self.score_changed_kwargs['course_id'],
)
)
self.assertTrue(mock_retry.called)
...@@ -19,7 +19,7 @@ from courseware.model_data import FieldDataCache ...@@ -19,7 +19,7 @@ from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor from courseware.module_render import get_module_for_descriptor
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.scores import weighted_score
from lms.djangoapps.grades.signals.signals import 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
...@@ -293,7 +293,7 @@ def _reset_module_attempts(studentmodule): ...@@ -293,7 +293,7 @@ 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 SCORE_CHANGED event for the given module. The earned points are Fires a PROBLEM_SCORE_CHANGED event for the given module. The earned points are
always zero. We must retrieve the possible points from the XModule, as always zero. We must retrieve the possible points from the XModule, as
noted below. noted below.
""" """
...@@ -322,7 +322,7 @@ def _fire_score_changed_for_block(course_id, student, block, module_state_key): ...@@ -322,7 +322,7 @@ def _fire_score_changed_for_block(course_id, student, block, module_state_key):
points_earned, points_possible = weighted_score(0, max_score, getattr(module, 'weight', None)) points_earned, points_possible = weighted_score(0, max_score, getattr(module, 'weight', None))
else: else:
points_earned, points_possible = 0, 0 points_earned, points_possible = 0, 0
SCORE_CHANGED.send( PROBLEM_SCORE_CHANGED.send(
sender=None, sender=None,
points_possible=points_possible, points_possible=points_possible,
points_earned=points_earned, points_earned=points_earned,
......
...@@ -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.SCORE_CHANGED.send') @patch('lms.djangoapps.grades.signals.handlers.PROBLEM_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()})
......
...@@ -378,7 +378,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): ...@@ -378,7 +378,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.SCORE_CHANGED.send') @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_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'})
...@@ -404,7 +404,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): ...@@ -404,7 +404,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.SCORE_CHANGED.send') @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_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.SCORE_CHANGED.send') @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_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 SCORE_CHANGED from lms.djangoapps.grades.signals.signals import PROBLEM_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,11 +19,11 @@ from xmodule.modulestore.django import modulestore ...@@ -19,11 +19,11 @@ from xmodule.modulestore.django import modulestore
log = logging.getLogger("edx.lti_provider") log = logging.getLogger("edx.lti_provider")
@receiver(SCORE_CHANGED) @receiver(PROBLEM_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
SCORE_CHANGED for a description of the signal. PROBLEM_SCORE_CHANGED for a description of the signal.
""" """
points_possible = kwargs.get('points_possible', None) points_possible = kwargs.get('points_possible', None)
points_earned = kwargs.get('points_earned', None) points_earned = kwargs.get('points_earned', 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