def setUp(self): super().setUp() self.aside_usage_key = UsageKey.from_string( "aside-usage-v2:block-v1$:SGAU+SGA101+2017_SGA+type@problem+block" "@2582bbb68672426297e525b49a383eb8::rapid_response_xblock") self.scope_ids = make_scope_ids(self.runtime, self.aside_usage_key) self.aside_instance = RapidResponseAside(scope_ids=self.scope_ids, runtime=self.runtime)
def test_serialize_runs(self): """ serialize_runs should return a serialized representation of runs for a problem """ course_id = self.aside_instance.course_key problem_id = self.aside_instance.wrapped_block_usage_key run1 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=False, ) run2 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=True, ) assert RapidResponseAside.serialize_runs([run2, run1]) == [{ 'id': run.id, 'created': run.created.isoformat(), 'open': run.open, } for run in [run2, run1]]
def test_get_counts_for_problem(self): """ get_counts_for_problem should return histogram count data for a problem """ course_id = self.aside_instance.course_key problem_id = self.aside_instance.wrapped_block_usage_key run1 = RapidResponseRun.objects.create(course_key=course_id, problem_usage_key=problem_id, open=False) run2 = RapidResponseRun.objects.create(course_key=course_id, problem_usage_key=problem_id, open=True) choices = [ { 'answer_id': 'choice_0', 'answer_text': 'an incorrect answer' }, { 'answer_id': 'choice_1', 'answer_text': 'the correct answer' }, { 'answer_id': 'choice_2', 'answer_text': 'a different incorrect answer' }, ] choices_lookup = { choice['answer_id']: choice['answer_text'] for choice in choices } counts = list( zip( [choices[i]['answer_id'] for i in range(3)], list(range(2, 5)), [run1.id for _ in range(3)], )) + list( zip( [choices[i]['answer_id'] for i in range(3)], [3, 0, 7], [run2.id for _ in range(3)], )) counts_dict = defaultdict(dict) for answer_id, num_submissions, run_id in counts: counts_dict[answer_id][run_id] = num_submissions answer_text = choices_lookup[answer_id] for _ in range(num_submissions): user = UserFactory.create() RapidResponseSubmission.objects.create( # For some reason the modulestore looks for a deprecated course key run=RapidResponseRun.objects.get(id=run_id), user_id=user.id, answer_id=answer_id, answer_text=answer_text, event={}) run_ids = [run2.id, run1.id] assert RapidResponseAside.get_counts_for_problem( run_ids, choices) == counts_dict
class RapidResponseAsideTests(RuntimeEnabledTestCase): """Tests for RapidResponseAside logic""" def setUp(self): super().setUp() self.aside_usage_key = UsageKey.from_string( "aside-usage-v2:block-v1$:SGAU+SGA101+2017_SGA+type@problem+block" "@2582bbb68672426297e525b49a383eb8::rapid_response_xblock") self.scope_ids = make_scope_ids(self.runtime, self.aside_usage_key) self.aside_instance = RapidResponseAside(scope_ids=self.scope_ids, runtime=self.runtime) @data(*[ [True, True], [False, False], ]) @unpack def test_student_view(self, enabled_value, should_render_aside): """ Test that the aside student view returns a fragment if the block is rapid-response-enabled """ self.aside_instance.enabled = enabled_value fragment = self.aside_instance.student_view_aside(Mock()) # If the block is enabled for rapid response, it should return a fragment with # non-empty content and should specify a JS initialization function assert bool(fragment.content) is should_render_aside assert (fragment.js_init_fn == 'RapidResponseAsideInit') is should_render_aside @data(True, False) def test_student_view_context(self, is_open): """ Test that the aside student view has the proper context variables """ self.aside_instance.enabled = True with patch( 'rapid_response_xblock.block.RapidResponseAside.has_open_run', new_callable=PropertyMock, ) as has_open_run_mock: has_open_run_mock.return_value = is_open fragment = self.aside_instance.student_view_aside(Mock()) assert 'data-open="{}"'.format(is_open) in fragment.content @data(*[ [BLOCK_PROBLEM_CATEGORY, {MULTIPLE_CHOICE_TYPE}, None, True], [ BLOCK_PROBLEM_CATEGORY, None, Mock(problem_types={MULTIPLE_CHOICE_TYPE}), True ], [None, {MULTIPLE_CHOICE_TYPE}, None, False], ['invalid_category', {MULTIPLE_CHOICE_TYPE}, None, False], [BLOCK_PROBLEM_CATEGORY, {'invalid_problem_type'}, None, False], [ BLOCK_PROBLEM_CATEGORY, {MULTIPLE_CHOICE_TYPE, 'invalid_problem_type'}, None, False ], ]) @unpack def test_should_apply_to_block(self, block_category, block_problem_types, block_descriptor, should_apply): """ Test that should_apply_to_block only returns True for multiple choice problem blocks """ block = Mock(category=block_category, problem_types=block_problem_types, descriptor=block_descriptor) # `should_apply_to_block` uses `hasattr` to inspect the block object, and that # always returns True for Mock objects unless the attribute names are explicitly deleted. for block_attr in ['category', 'descriptor', 'problem_types']: if getattr(block, block_attr) is None: delattr(block, block_attr) assert self.aside_instance.should_apply_to_block(block) is should_apply @data(True, False) def test_studio_view(self, enabled_value): """ Test that the aside studio view returns a fragment """ self.aside_instance.enabled = enabled_value fragment = self.aside_instance.studio_view_aside(Mock()) assert 'data-enabled="{}"'.format(enabled_value) in fragment.content assert fragment.js_init_fn == 'RapidResponseAsideStudioInit' def test_toggle_block_open(self): """Test that toggle_block_open_status changes the status of a rapid response block""" usage_key = self.aside_instance.wrapped_block_usage_key course_key = self.aside_instance.course_key run = RapidResponseRun.objects.create( problem_usage_key=usage_key, course_key=course_key, ) assert run.open is False self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 2 assert RapidResponseRun.objects.filter(problem_usage_key=usage_key, course_key=course_key, open=True).exists() is True self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 2 assert RapidResponseRun.objects.filter(problem_usage_key=usage_key, course_key=course_key, open=True).exists() is False self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 3 assert RapidResponseRun.objects.filter( problem_usage_key=usage_key, course_key=course_key, open=True, ).exists() is True def test_toggle_block_open_duplicate(self): """Test that toggle_block_open_status only looks at the last run's open status""" usage_key = self.aside_instance.wrapped_block_usage_key course_key = self.aside_instance.course_key RapidResponseRun.objects.create( problem_usage_key=usage_key, course_key=course_key, open=True, ) RapidResponseRun.objects.create( problem_usage_key=usage_key, course_key=course_key, open=False, ) self.aside_instance.toggle_block_open_status(Mock()) assert RapidResponseRun.objects.count() == 3 assert RapidResponseRun.objects.filter( problem_usage_key=usage_key, course_key=course_key, ).order_by('-created').first().open is True def test_toggle_block_enabled(self): """ Test that toggle_block_enabled changes 'enabled' field value and returns an appropriate response """ # Test that the default value is False assert self.aside_instance.enabled is False for expected_enabled_value in [True, False]: resp = self.aside_instance.toggle_block_enabled(Mock()) assert self.aside_instance.enabled is expected_enabled_value assert resp.json['is_enabled'] == self.aside_instance.enabled @data(*[[True, 200], [False, 403]]) @unpack def test_toggle_block_open_staff_only(self, is_staff, expected_status): """Test that toggle_block_open_status is only enabled for staff""" with patch.object(self.aside_instance, 'is_staff', return_value=is_staff): resp = self.aside_instance.toggle_block_open_status() assert resp.status_code == expected_status @data(*[[True, 200], [False, 403]]) @unpack def test_responses_staff_only(self, is_staff, expected_status): """ Test that only staff users should access the API """ with patch.object(self.aside_instance, 'is_staff', return_value=is_staff), self.patch_modulestore(): resp = self.aside_instance.responses() assert resp.status_code == expected_status @data(True, False) def test_responses_open(self, is_open): """ Test that the responses API shows whether the problem is open """ RapidResponseRun.objects.create( problem_usage_key=self.aside_instance.wrapped_block_usage_key, course_key=self.aside_instance.course_key, open=is_open, ) with self.patch_modulestore(): resp = self.aside_instance.responses() assert resp.status_code == 200 assert resp.json['is_open'] == is_open @data(True, False) def test_responses(self, has_runs): """ Test that the responses API will show recorded events during the open period """ course_id = self.aside_instance.course_key problem_id = self.aside_instance.wrapped_block_usage_key choices = [{ 'answer_id': 'choice_0', 'answer_text': 'First answer', }, { 'answer_id': 'choice_1', 'answer_text': 'Second answer', }] if has_runs: run1 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=False) run2 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=True) counts = { choices[0]['answer_id']: { run1.id: 3, run2.id: 4, }, choices[1]['answer_id']: { run1.id: 5, run2.id: 6, } } expected_total_counts = { str(run1.id): 8, str(run2.id): 10, } else: counts = {} expected_total_counts = {} with patch( 'rapid_response_xblock.block.RapidResponseAside.get_counts_for_problem', return_value=counts, ) as get_counts_mock, patch( 'rapid_response_xblock.block.RapidResponseAside.choices', new_callable=PropertyMock) as get_choices_mock: get_choices_mock.return_value = choices resp = self.aside_instance.responses() run_queryset = RapidResponseRun.objects.all() assert resp.status_code == 200 assert resp.json['is_open'] is has_runs assert resp.json['choices'] == choices assert resp.json['runs'] == RapidResponseAside.serialize_runs( run_queryset) counts_with_str_keys = { answer_id: {str(run_id): count for run_id, count in list(runs.items())} for answer_id, runs in list(counts.items()) } assert resp.json['counts'] == counts_with_str_keys assert resp.json['total_counts'] == expected_total_counts now = datetime.now(tz=pytz.utc) minute = timedelta(minutes=1) assert (now - minute) < parse_datetime( resp.json['server_now']) < (now + minute) get_choices_mock.assert_called_once_with() get_counts_mock.assert_called_once_with( [run.id for run in run_queryset], choices) def test_choices(self): """ RapidResponseAside.choices should return a serialized representation of choices from a problem OLX """ with self.patch_modulestore(): assert self.aside_instance.choices == [ { 'answer_id': 'choice_0', 'answer_text': 'an incorrect answer' }, { 'answer_id': 'choice_1', 'answer_text': 'the correct answer' }, { 'answer_id': 'choice_2', 'answer_text': 'a different incorrect answer' }, ] def test_get_counts_for_problem(self): """ get_counts_for_problem should return histogram count data for a problem """ course_id = self.aside_instance.course_key problem_id = self.aside_instance.wrapped_block_usage_key run1 = RapidResponseRun.objects.create(course_key=course_id, problem_usage_key=problem_id, open=False) run2 = RapidResponseRun.objects.create(course_key=course_id, problem_usage_key=problem_id, open=True) choices = [ { 'answer_id': 'choice_0', 'answer_text': 'an incorrect answer' }, { 'answer_id': 'choice_1', 'answer_text': 'the correct answer' }, { 'answer_id': 'choice_2', 'answer_text': 'a different incorrect answer' }, ] choices_lookup = { choice['answer_id']: choice['answer_text'] for choice in choices } counts = list( zip( [choices[i]['answer_id'] for i in range(3)], list(range(2, 5)), [run1.id for _ in range(3)], )) + list( zip( [choices[i]['answer_id'] for i in range(3)], [3, 0, 7], [run2.id for _ in range(3)], )) counts_dict = defaultdict(dict) for answer_id, num_submissions, run_id in counts: counts_dict[answer_id][run_id] = num_submissions answer_text = choices_lookup[answer_id] for _ in range(num_submissions): user = UserFactory.create() RapidResponseSubmission.objects.create( # For some reason the modulestore looks for a deprecated course key run=RapidResponseRun.objects.get(id=run_id), user_id=user.id, answer_id=answer_id, answer_text=answer_text, event={}) run_ids = [run2.id, run1.id] assert RapidResponseAside.get_counts_for_problem( run_ids, choices) == counts_dict def test_serialize_runs(self): """ serialize_runs should return a serialized representation of runs for a problem """ course_id = self.aside_instance.course_key problem_id = self.aside_instance.wrapped_block_usage_key run1 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=False, ) run2 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=True, ) assert RapidResponseAside.serialize_runs([run2, run1]) == [{ 'id': run.id, 'created': run.created.isoformat(), 'open': run.open, } for run in [run2, run1]]
def test_responses(self, has_runs): """ Test that the responses API will show recorded events during the open period """ course_id = self.aside_instance.course_key problem_id = self.aside_instance.wrapped_block_usage_key choices = [{ 'answer_id': 'choice_0', 'answer_text': 'First answer', }, { 'answer_id': 'choice_1', 'answer_text': 'Second answer', }] if has_runs: run1 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=False) run2 = RapidResponseRun.objects.create( course_key=course_id, problem_usage_key=problem_id, open=True) counts = { choices[0]['answer_id']: { run1.id: 3, run2.id: 4, }, choices[1]['answer_id']: { run1.id: 5, run2.id: 6, } } expected_total_counts = { str(run1.id): 8, str(run2.id): 10, } else: counts = {} expected_total_counts = {} with patch( 'rapid_response_xblock.block.RapidResponseAside.get_counts_for_problem', return_value=counts, ) as get_counts_mock, patch( 'rapid_response_xblock.block.RapidResponseAside.choices', new_callable=PropertyMock) as get_choices_mock: get_choices_mock.return_value = choices resp = self.aside_instance.responses() run_queryset = RapidResponseRun.objects.all() assert resp.status_code == 200 assert resp.json['is_open'] is has_runs assert resp.json['choices'] == choices assert resp.json['runs'] == RapidResponseAside.serialize_runs( run_queryset) counts_with_str_keys = { answer_id: {str(run_id): count for run_id, count in list(runs.items())} for answer_id, runs in list(counts.items()) } assert resp.json['counts'] == counts_with_str_keys assert resp.json['total_counts'] == expected_total_counts now = datetime.now(tz=pytz.utc) minute = timedelta(minutes=1) assert (now - minute) < parse_datetime( resp.json['server_now']) < (now + minute) get_choices_mock.assert_called_once_with() get_counts_mock.assert_called_once_with( [run.id for run in run_queryset], choices)