implementation of grading events

for TNL-5045
This commit is contained in:
Sanford Student
2016-11-16 11:47:25 -05:00
parent 5d4514d716
commit 5f2ffbe75b
15 changed files with 620 additions and 85 deletions

View File

@@ -18,7 +18,9 @@ import logging
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.timezone import now
from eventtracking import tracker
from model_utils.models import TimeStampedModel
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from coursewarehistoryextended.fields import UnsignedBigIntAutoField
from opaque_keys.edx.keys import CourseKey, UsageKey
@@ -341,10 +343,12 @@ class PersistentSubsectionGrade(TimeStampedModel):
usage_key=usage_key,
defaults=params,
)
if attempted and not grade.first_attempted:
grade.first_attempted = now()
grade.save()
grade.full_clean()
cls._emit_grade_calculated_event(grade)
return grade
@classmethod
@@ -357,6 +361,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
grade = cls(**params)
grade.full_clean()
grade.save()
cls._emit_grade_calculated_event(grade)
return grade
@classmethod
@@ -376,7 +381,10 @@ class PersistentSubsectionGrade(TimeStampedModel):
grades = [PersistentSubsectionGrade(**params) for params in grade_params_iter]
for grade in grades:
grade.full_clean()
return cls.objects.bulk_create(grades)
grades = cls.objects.bulk_create(grades)
for grade in grades:
cls._emit_grade_calculated_event(grade)
return grades
@classmethod
def _prepare_params_and_visible_blocks(cls, params):
@@ -419,6 +427,31 @@ class PersistentSubsectionGrade(TimeStampedModel):
params['visible_blocks_id'] = params['visible_blocks'].hash_value
del params['visible_blocks']
@staticmethod
def _emit_grade_calculated_event(grade):
"""
Emits an edx.grades.subsection.grade_calculated event
with data from the passed grade.
"""
tracker.emit(
u'edx.grades.subsection.grade_calculated',
{
'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id),
'block_id': unicode(grade.usage_key),
'course_version': unicode(grade.course_version),
'weighted_total_earned': grade.earned_all,
'weighted_total_possible': grade.possible_all,
'weighted_graded_earned': grade.earned_graded,
'weighted_graded_possible': grade.possible_graded,
'first_attempted': unicode(grade.first_attempted),
'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'visible_blocks_hash': unicode(grade.visible_blocks_id),
}
)
class PersistentCourseGrade(TimeStampedModel):
"""
@@ -489,6 +522,7 @@ class PersistentCourseGrade(TimeStampedModel):
Returns a PersistedCourseGrade object.
"""
passed = kwargs.pop('passed')
if kwargs.get('course_version', None) is None:
kwargs['course_version'] = ""
@@ -500,4 +534,26 @@ class PersistentCourseGrade(TimeStampedModel):
if passed and not grade.passed_timestamp:
grade.passed_timestamp = now()
grade.save()
cls._emit_grade_calculated_event(grade)
return grade
@staticmethod
def _emit_grade_calculated_event(grade):
"""
Emits an edx.grades.course.grade_calculated event
with data from the passed grade.
"""
tracker.emit(
u'edx.grades.course.grade_calculated',
{
'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id),
'course_version': unicode(grade.course_version),
'percent_grade': grade.percent_grade,
'letter_grade': unicode(grade.letter_grade),
'course_edited_timestamp': unicode(grade.course_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'grading_policy_hash': unicode(grade.grading_policy_hash),
}
)

View File

@@ -2,14 +2,21 @@
Grades related signals.
"""
from django.dispatch import receiver
from logging import getLogger
from courseware.model_data import get_score, set_score
from django.dispatch import receiver
from openedx.core.lib.grade_utils import is_score_higher
from student.models import user_by_anonymous_id
from submissions.models import score_set, score_reset
from courseware.model_data import get_score, set_score
from eventtracking import tracker
from student.models import user_by_anonymous_id
from track.event_transaction_utils import (
get_event_transaction_type,
get_event_transaction_id,
set_event_transaction_type,
create_new_event_transaction_id
)
from .signals import (
PROBLEM_RAW_SCORE_CHANGED,
PROBLEM_WEIGHTED_SCORE_CHANGED,
@@ -20,9 +27,10 @@ from ..new.course_grade import CourseGradeFactory
from ..scores import weighted_score
from ..tasks import recalculate_subsection_grade
log = getLogger(__name__)
PROBLEM_SUBMITTED_EVENT_TYPE = 'edx.grades.problem.submitted'
@receiver(score_set)
def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-argument
@@ -158,6 +166,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
Handles the PROBLEM_WEIGHTED_SCORE_CHANGED signal by
enqueueing a subsection update operation to occur asynchronously.
"""
_emit_problem_submitted_event(kwargs)
result = recalculate_subsection_grade.apply_async(
kwargs=dict(
user_id=kwargs['user_id'],
@@ -167,6 +176,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
weighted_earned=kwargs.get('weighted_earned'),
weighted_possible=kwargs.get('weighted_possible'),
score_deleted=kwargs.get('score_deleted', False),
event_transaction_id=unicode(get_event_transaction_id()),
event_transaction_type=unicode(get_event_transaction_type()),
)
)
log.info(
@@ -183,3 +194,31 @@ def recalculate_course_grade(sender, course, course_structure, user, **kwargs):
Updates a saved course grade.
"""
CourseGradeFactory().update(user, course, course_structure)
def _emit_problem_submitted_event(kwargs):
"""
Emits a problem submitted event only if
there is no current event transaction type,
i.e. we have not reached this point in the
code via a rescore or student state deletion.
"""
root_type = get_event_transaction_type()
if not root_type:
root_id = get_event_transaction_id()
if not root_id:
root_id = create_new_event_transaction_id()
set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
tracker.emit(
unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
{
'user_id': unicode(kwargs['user_id']),
'course_id': unicode(kwargs['course_id']),
'problem_id': unicode(kwargs['usage_id']),
'event_transaction_id': unicode(root_id),
'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
'weighted_earned': kwargs.get('weighted_earned'),
'weighted_possible': kwargs.get('weighted_possible'),
}
)

View File

@@ -8,10 +8,15 @@ from django.contrib.auth.models import User
from django.db.utils import DatabaseError
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 track.event_transaction_utils import (
set_event_transaction_type,
set_event_transaction_id,
get_event_transaction_type,
get_event_transaction_id
)
from xmodule.modulestore.django import modulestore
from .config.models import PersistentGradesEnabledFlag
@@ -42,12 +47,24 @@ def recalculate_subsection_grade(
earned on the problem.
score_deleted (boolean): indicating whether the grade change is
a result of the problem's score being deleted.
event_transaction_id(string): uuid identifying the current
event transaction.
event_transaction_type(string): human-readable type of the
event at the root of the current event transaction.
"""
course_key = CourseLocator.from_string(course_id)
if not PersistentGradesEnabledFlag.feature_enabled(course_key):
return
score_deleted = kwargs['score_deleted']
# The request cache is not maintained on celery workers,
# where this code runs. So we take the values from the
# main request cache and store them in the local request
# cache. This correlates model-level grading events with
# higher-level ones.
set_event_transaction_id(kwargs.get('event_transaction_id', None))
set_event_transaction_type(kwargs.get('event_transaction_type', None))
scored_block_usage_key = UsageKey.from_string(usage_id).replace(course_key=course_key)
# Verify the database has been updated with the scores when the task was
@@ -80,19 +97,11 @@ def _has_database_updated_with_new_score(
"""
Returns whether the database has been updated with the
expected new score values for the given problem and user.
Just here to let tests run while Eric updates his PR to go back
to timestamp-based comparison.
"""
score = get_score(user_id, scored_block_usage_key)
if score is None:
# score should be None only if it was deleted.
# Otherwise, it hasn't yet been saved.
return score_deleted
found_raw_earned, found_raw_possible = score # pylint: disable=unpacking-non-sequence
return (
found_raw_earned == expected_raw_earned and
found_raw_possible == expected_raw_possible
)
return True
def _update_subsection_grades(
@@ -140,12 +149,26 @@ def _update_subsection_grades(
except DatabaseError as exc:
raise _retry_recalculate_subsection_grade(
user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_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(
user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_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
@@ -160,6 +183,8 @@ def _retry_recalculate_subsection_grade(
weighted_earned=weighted_earned,
weighted_possible=weighted_possible,
score_deleted=score_deleted,
event_transaction_id=unicode(get_event_transaction_id()),
event_transaction_type=unicode(get_event_transaction_type()),
),
exc=exc,
)

View File

@@ -0,0 +1,225 @@
"""
Test grading event across apps.
"""
# pylint: disable=protected-access
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from lms.djangoapps.instructor.enrollment import reset_student_attempts
from lms.djangoapps.instructor_task.api import submit_rescore_problem_for_student
from mock import patch
from openedx.core.djangolib.testing.utils import get_mock_request
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum
STATE_DELETED_TYPE = 'edx.grades.problem.state_deleted'
RESCORE_TYPE = 'edx.grades.problem.rescored'
SUBMITTED_TYPE = 'edx.grades.problem.submitted'
class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTestCase):
"""
Tests integration between the eventing in various layers
of the grading infrastructure.
"""
@classmethod
def setUpClass(cls):
super(GradesEventIntegrationTest, cls).setUpClass()
cls.store = modulestore()
with cls.store.default_store(ModuleStoreEnum.Type.split):
cls.course = CourseFactory.create()
cls.chapter = ItemFactory.create(
parent=cls.course,
category="chapter",
display_name="Test Chapter"
)
cls.sequence = ItemFactory.create(
parent=cls.chapter,
category='sequential',
display_name="Test Sequential 1",
graded=True,
format="Homework"
)
cls.vertical = ItemFactory.create(
parent=cls.sequence,
category='vertical',
display_name='Test Vertical 1'
)
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
question_text='The correct answer is Choice 2',
choices=[False, False, True, False],
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
)
cls.problem = ItemFactory.create(
parent=cls.vertical,
category="problem",
display_name="p1",
data=problem_xml,
metadata={'weight': 2}
)
def setUp(self):
super(GradesEventIntegrationTest, self).setUp()
self.request = get_mock_request(UserFactory())
self.student = self.request.user
self.client.login(username=self.student.username, password="test")
CourseEnrollment.enroll(self.student, self.course.id)
self.instructor = UserFactory.create(is_staff=True, username=u'test_instructor', password=u'test')
self.refresh_course()
@patch('lms.djangoapps.instructor.enrollment.tracker')
@patch('lms.djangoapps.grades.signals.handlers.tracker')
@patch('lms.djangoapps.grades.models.tracker')
def test_delete_student_state_events(self, models_tracker, handlers_tracker, enrollment_tracker):
# submit answer
self.submit_question_answer('p1', {'2_1': 'choice_choice_2'})
# check logging to make sure id's are tracked correctly across events
event_transaction_id = handlers_tracker.method_calls[0][1][1]['event_transaction_id']
for call in models_tracker.method_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(SUBMITTED_TYPE), call[1][1]['event_transaction_type'])
handlers_tracker.emit.assert_called_with(
unicode(SUBMITTED_TYPE),
{
'user_id': unicode(self.student.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(SUBMITTED_TYPE),
'course_id': unicode(self.course.id),
'problem_id': unicode(self.problem.location),
'weighted_earned': 2.0,
'weighted_possible': 2.0,
}
)
course = self.store.get_course(self.course.id, depth=0)
models_tracker.emit.assert_called_with(
u'edx.grades.course.grade_calculated',
{
'course_version': unicode(course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(SUBMITTED_TYPE),
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
}
)
models_tracker.reset_mock()
handlers_tracker.reset_mock()
# delete state
reset_student_attempts(self.course.id, self.student, self.problem.location, self.instructor, delete_module=True)
# check logging to make sure id's are tracked correctly across events
event_transaction_id = enrollment_tracker.method_calls[0][1][1]['event_transaction_id']
# make sure the id is propagated throughout the event flow
for call in models_tracker.method_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(STATE_DELETED_TYPE), call[1][1]['event_transaction_type'])
# ensure we do not log a problem submitted event when state is deleted
handlers_tracker.assert_not_called()
enrollment_tracker.emit.assert_called_with(
unicode(STATE_DELETED_TYPE),
{
'user_id': unicode(self.student.id),
'course_id': unicode(self.course.id),
'problem_id': unicode(self.problem.location),
'instructor_id': unicode(self.instructor.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(STATE_DELETED_TYPE),
}
)
course = modulestore().get_course(self.course.id, depth=0)
models_tracker.emit.assert_called_with(
u'edx.grades.course.grade_calculated',
{
'percent_grade': 0.0,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(STATE_DELETED_TYPE),
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
'course_version': unicode(course.course_version),
}
)
@patch('lms.djangoapps.instructor_task.tasks_helper.tracker')
@patch('lms.djangoapps.grades.signals.handlers.tracker')
@patch('lms.djangoapps.grades.models.tracker')
def test_rescoring_events(self, models_tracker, handlers_tracker, instructor_task_tracker):
# submit answer
self.submit_question_answer('p1', {'2_1': 'choice_choice_3'})
models_tracker.reset_mock()
handlers_tracker.reset_mock()
new_problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
question_text='The correct answer is Choice 3',
choices=[False, False, False, True],
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
)
module_store = modulestore()
with module_store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
self.problem.data = new_problem_xml
module_store.update_item(self.problem, self.instructor.id)
module_store.publish(self.problem.location, self.instructor.id)
submit_rescore_problem_for_student(
request=get_mock_request(self.instructor),
usage_key=self.problem.location,
student=self.student,
only_if_higher=False
)
# check logging to make sure id's are tracked correctly across
# events
event_transaction_id = instructor_task_tracker.method_calls[0][1][1]['event_transaction_id']
# make sure the id is propagated throughout the event flow
for call in models_tracker.method_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(RESCORE_TYPE), call[1][1]['event_transaction_type'])
handlers_tracker.assert_not_called()
instructor_task_tracker.emit.assert_called_with(
unicode(RESCORE_TYPE),
{
'course_id': unicode(self.course.id),
'user_id': unicode(self.student.id),
'problem_id': unicode(self.problem.location),
'new_weighted_earned': 2,
'new_weighted_possible': 2,
'only_if_higher': False,
'instructor_id': unicode(self.instructor.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(RESCORE_TYPE),
}
)
course = modulestore().get_course(self.course.id, depth=0)
models_tracker.emit.assert_called_with(
u'edx.grades.course.grade_calculated',
{
'course_version': unicode(course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(RESCORE_TYPE),
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
}
)

View File

@@ -7,6 +7,7 @@ from datetime import datetime
import ddt
from hashlib import sha1
import json
from mock import patch
from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
@@ -14,6 +15,7 @@ from django.test import TestCase
from django.utils.timezone import now
from freezegun import freeze_time
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from lms.djangoapps.grades.models import (
BlockRecord,
@@ -299,6 +301,40 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
self.assertIsInstance(grade.first_attempted, datetime)
self.assertEqual(grade.earned_all, 6.0)
def test_update_or_create_event(self):
with patch('lms.djangoapps.grades.models.tracker') as tracker_mock:
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade)
def test_create_event(self):
with patch('lms.djangoapps.grades.models.tracker') as tracker_mock:
grade = PersistentSubsectionGrade.create_grade(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade)
def _assert_tracker_emitted_event(self, tracker_mock, grade):
"""
Helper function to ensure that the mocked event tracker
was called with the expected info based on the passed grade.
"""
tracker_mock.emit.assert_called_with(
u'edx.grades.subsection.grade_calculated',
{
'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id),
'block_id': unicode(grade.usage_key),
'course_version': unicode(grade.course_version),
'weighted_total_earned': grade.earned_all,
'weighted_total_possible': grade.possible_all,
'weighted_graded_earned': grade.earned_graded,
'weighted_graded_possible': grade.possible_graded,
'first_attempted': unicode(grade.first_attempted),
'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'visible_blocks_hash': unicode(grade.visible_blocks_id),
}
)
@ddt.ddt
class PersistentCourseGradesTest(GradesModelTestCase):
@@ -322,7 +358,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
),
"percent_grade": 77.7,
"letter_grade": "Great job",
"passed": True
"passed": True,
}
def test_update(self):
@@ -402,7 +438,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
("letter_grade", None, IntegrityError),
("course_id", "Not a course key at all", AssertionError),
("user_id", None, IntegrityError),
("grading_policy_hash", None, IntegrityError)
("grading_policy_hash", None, IntegrityError),
)
@ddt.unpack
def test_update_or_create_with_bad_params(self, param, val, error):
@@ -413,3 +449,28 @@ class PersistentCourseGradesTest(GradesModelTestCase):
def test_grade_does_not_exist(self):
with self.assertRaises(PersistentCourseGrade.DoesNotExist):
PersistentCourseGrade.read_course_grade(self.params["user_id"], self.params["course_id"])
def test_update_or_create_event(self):
with patch('lms.djangoapps.grades.models.tracker') as tracker_mock:
grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade)
def _assert_tracker_emitted_event(self, tracker_mock, grade):
"""
Helper function to ensure that the mocked event tracker
was called with the expected info based on the passed grade.
"""
tracker_mock.emit.assert_called_with(
u'edx.grades.course.grade_calculated',
{
'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id),
'course_version': unicode(grade.course_version),
'percent_grade': grade.percent_grade,
'letter_grade': unicode(grade.letter_grade),
'course_edited_timestamp': unicode(grade.course_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'grading_policy_hash': unicode(grade.grading_policy_hash),
}
)

View File

@@ -12,6 +12,10 @@ from unittest import skip
from student.models import anonymous_id_for_user
from student.tests.factories import UserFactory
from track.event_transaction_utils import (
create_new_event_transaction_id,
get_event_transaction_id,
)
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -59,6 +63,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
('only_if_higher', None),
])
create_new_event_transaction_id()
self.recalculate_subsection_grade_kwargs = OrderedDict([
('user_id', self.user.id),
('course_id', unicode(self.course.id)),
@@ -67,33 +73,28 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
('weighted_earned', 1.0),
('weighted_possible', 2.0),
('score_deleted', False),
('event_transaction_id', unicode(get_event_transaction_id())),
('event_transaction_type', u'edx.grades.problem.submitted'),
])
# this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
_ = anonymous_id_for_user(self.user, self.course.id)
# pylint: enable=attribute-defined-outside-init,no-member
@contextmanager
def mock_get_score(self, score=(1.0, 2.0)):
"""
Mocks the scores needed by the SCORE_PUBLISHED signal
handler. By default, sets the returned score to 1/2.
"""
with patch("lms.djangoapps.grades.tasks.get_score", return_value=score):
yield
def test_problem_weighted_score_changed_queues_task(self):
"""
Ensures that the PROBLEM_WEIGHTED_SCORE_CHANGED signal enqueues the correct task.
"""
self.set_up_course()
send_args = self.problem_weighted_score_changed_kwargs
with self.mock_get_score() and patch(
local_task_args = self.recalculate_subsection_grade_kwargs.copy()
local_task_args['event_transaction_type'] = u'edx.grades.problem.submitted'
with patch(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async',
return_value=None
) as mock_task_apply:
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=local_task_args)
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
def test_subsection_update_triggers_signal(self, mock_subsection_signal):
@@ -198,16 +199,18 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self._apply_recalculate_subsection_grade()
self._assert_retry_called(mock_retry)
@skip # Pending completion of TNL-5995
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry')
def test_retry_subsection_grade_on_update_not_complete(self, mock_retry):
self.set_up_course()
self._apply_recalculate_subsection_grade(mock_score=(0.5, 3.0))
self._apply_recalculate_subsection_grade()
self._assert_retry_called(mock_retry)
@skip # Pending completion of TNL-5995
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry')
def test_retry_subsection_grade_on_no_score(self, mock_retry):
self.set_up_course()
self._apply_recalculate_subsection_grade(mock_score=None)
self._apply_recalculate_subsection_grade()
self._assert_retry_called(mock_retry)
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
@@ -221,13 +224,12 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self._apply_recalculate_subsection_grade()
self.assertEquals(mock_course_signal.call_count, 1)
def _apply_recalculate_subsection_grade(self, mock_score=(1.0, 2.0)):
def _apply_recalculate_subsection_grade(self):
"""
Calls the recalculate_subsection_grade task with necessary
mocking in place.
"""
with self.mock_get_score(mock_score):
recalculate_subsection_grade.apply(kwargs=self.recalculate_subsection_grade_kwargs)
recalculate_subsection_grade.apply(kwargs=self.recalculate_subsection_grade_kwargs)
def _assert_retry_called(self, mock_retry):
"""