Files
edx-platform/lms/djangoapps/grades/tests/integration/test_events.py
Feanil Patel f0f18f8405 refactor: Make the test easier to debug in the future.
This test had a redundant call to get the course data from the store
because that already happens at the end of the setup function.  And also
because expected call structure was being built inside the assert, it
made it harder to inspect when debugging.  Make the code a little bit
easire to debug in case we're back here in the future.
2024-11-05 10:44:17 -05:00

245 lines
11 KiB
Python

"""
Test grading events across apps.
"""
import ddt
from unittest.mock import call as mock_call
from unittest.mock import patch
from crum import set_current_request
import openedx.core.djangoapps.content.block_structure.api as bs_api
from xmodule.capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from lms.djangoapps.instructor.enrollment import reset_student_attempts
from lms.djangoapps.instructor_task.api import submit_rescore_problem_for_student
from openedx.core.djangolib.testing.utils import get_mock_request
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
from ... import events
@ddt.ddt
class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTestCase):
"""
Tests integration between the eventing in various layers
of the grading infrastructure.
"""
ENABLED_SIGNALS = ['course_published']
@classmethod
def reset_course(cls):
"""
Sets up the course anew.
"""
with cls.store.default_store(ModuleStoreEnum.Type.split):
cls.course = CourseFactory.create()
cls.chapter = BlockFactory.create(
parent=cls.course,
category="chapter",
display_name="Test Chapter"
)
cls.sequence = BlockFactory.create(
parent=cls.chapter,
category='sequential',
display_name="Test Sequential 1",
graded=True,
format="Homework"
)
cls.vertical = BlockFactory.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 = BlockFactory.create(
parent=cls.vertical,
category="problem",
display_name="p1",
data=problem_xml,
metadata={'weight': 2}
)
def setUp(self):
self.reset_course()
super().setUp()
self.addCleanup(set_current_request, None)
self.request = get_mock_request(UserFactory())
self.student = self.request.user
self.client.login(username=self.student.username, password=self.TEST_PASSWORD)
CourseEnrollment.enroll(self.student, self.course.id)
self.instructor = UserFactory.create(is_staff=True, username='test_instructor', password=self.TEST_PASSWORD)
self.refresh_course()
# Since this doesn't happen automatically and we don't want to run all the publish signal handlers
# Just make sure we have the latest version of the course in cache before we test the problem.
bs_api.update_course_in_cache(self.course.id)
@patch('lms.djangoapps.grades.events.tracker')
def test_submit_answer(self, events_tracker):
self.submit_question_answer('p1', {'2_1': 'choice_choice_2'})
event_transaction_id = events_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
expected_calls = [
mock_call(
events.PROBLEM_SUBMITTED_EVENT_TYPE,
{
'user_id': str(self.student.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.PROBLEM_SUBMITTED_EVENT_TYPE,
'course_id': str(self.course.id),
'problem_id': str(self.problem.location),
'weighted_earned': 2.0,
'weighted_possible': 2.0,
},
),
mock_call(
events.COURSE_GRADE_CALCULATED,
{
'course_version': str(self.course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': 'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': str(self.student.id),
'letter_grade': '',
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.PROBLEM_SUBMITTED_EVENT_TYPE,
'course_id': str(self.course.id),
'course_edited_timestamp': str(self.course.subtree_edited_on),
}
),
]
events_tracker.emit.assert_has_calls(expected_calls, any_order=True)
@ddt.data(True, False)
def test_delete_student_state(self, emit_signals):
self.submit_question_answer('p1', {'2_1': 'choice_choice_2'})
with patch('lms.djangoapps.instructor.enrollment.tracker') as enrollment_tracker:
with patch('lms.djangoapps.grades.events.tracker') as events_tracker:
reset_student_attempts(
self.course.id,
self.student,
self.problem.location,
self.instructor,
delete_module=True,
emit_signals_and_events=emit_signals
)
course = self.store.get_course(self.course.id, depth=0)
if not emit_signals:
enrollment_tracker.assert_not_called()
enrollment_tracker.emit.assert_not_called()
events_tracker.emit.assert_not_called()
else:
event_transaction_id = enrollment_tracker.method_calls[0][1][1]['event_transaction_id']
enrollment_tracker.emit.assert_called_with(
events.STATE_DELETED_EVENT_TYPE,
{
'user_id': str(self.student.id),
'course_id': str(self.course.id),
'problem_id': str(self.problem.location),
'instructor_id': str(self.instructor.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.STATE_DELETED_EVENT_TYPE,
}
)
events_tracker.emit.assert_has_calls(
[
mock_call(
events.COURSE_GRADE_CALCULATED,
{
'percent_grade': 0.0,
'grading_policy_hash': 'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': str(self.student.id),
'letter_grade': '',
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.STATE_DELETED_EVENT_TYPE,
'course_id': str(self.course.id),
'course_edited_timestamp': str(course.subtree_edited_on),
'course_version': str(course.course_version),
}
),
mock_call(
events.COURSE_GRADE_NOW_FAILED_EVENT_TYPE,
{
'user_id': str(self.student.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.STATE_DELETED_EVENT_TYPE,
'course_id': str(self.course.id),
}
),
],
any_order=True,
)
def test_rescoring_events(self):
self.submit_question_answer('p1', {'2_1': 'choice_choice_3'})
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']
)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
self.problem.data = new_problem_xml
self.store.update_item(self.problem, self.instructor.id)
self.store.publish(self.problem.location, self.instructor.id)
with patch('lms.djangoapps.grades.events.tracker') as events_tracker:
submit_rescore_problem_for_student(
request=get_mock_request(self.instructor),
usage_key=self.problem.location,
student=self.student,
only_if_higher=False
)
course = self.store.get_course(self.course.id, depth=0)
# make sure the tracker's context is updated with course info
for args in events_tracker.get_tracker().context.call_args_list:
assert args[0][1] == {
'course_id': str(self.course.id),
'enterprise_uuid': '',
'org_id': str(self.course.org)
}
event_transaction_id = events_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
events_tracker.emit.assert_has_calls(
[
mock_call(
events.GRADES_RESCORE_EVENT_TYPE,
{
'course_id': str(self.course.id),
'user_id': str(self.student.id),
'problem_id': str(self.problem.location),
'new_weighted_earned': 2,
'new_weighted_possible': 2,
'only_if_higher': False,
'instructor_id': str(self.instructor.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.GRADES_RESCORE_EVENT_TYPE,
},
),
mock_call(
events.COURSE_GRADE_CALCULATED,
{
'course_version': str(course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': 'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': str(self.student.id),
'letter_grade': '',
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.GRADES_RESCORE_EVENT_TYPE,
'course_id': str(self.course.id),
'course_edited_timestamp': str(course.subtree_edited_on),
},
),
],
any_order=True,
)