def generate(cls, _xmodule_instance_args, _entry_id, course_id, task_input, action_name): """ For a given `course_id`, generate a CSV file containing all student answers to a given problem, and store using a `ReportStore`. """ start_time = time() start_date = datetime.now(UTC) num_reports = 1 task_progress = TaskProgress(action_name, num_reports, start_time) current_step = {'step': 'Calculating students answers to problem'} task_progress.update_task_state(extra_meta=current_step) # Compute result table and format it problem_location = task_input.get('problem_location') student_data = list_problem_responses(course_id, problem_location) features = ['username', 'state'] header, rows = format_dictlist(student_data, features) task_progress.attempted = task_progress.succeeded = len(rows) task_progress.skipped = task_progress.total - task_progress.attempted rows.insert(0, header) current_step = {'step': 'Uploading CSV'} task_progress.update_task_state(extra_meta=current_step) # Perform the upload problem_location = re.sub(r'[:/]', '_', problem_location) csv_name = 'student_state_from_{}'.format(problem_location) upload_csv_to_report_store(rows, csv_name, course_id, start_date) return task_progress.update_task_state(extra_meta=current_step)
def test_list_problem_responses(self): def result_factory(result_id): """ Return a dummy StudentModule object that can be queried for relevant info (student.username and state). """ result = Mock(spec=['student', 'state']) result.student.username.return_value = u'user{}'.format(result_id) result.state.return_value = u'state{}'.format(result_id) return result # Ensure that UsageKey.from_string returns a problem key that list_problem_responses can work with # (even when called with a dummy location): mock_problem_key = Mock(return_value=u'') mock_problem_key.course_key = self.course_key with patch.object(UsageKey, 'from_string') as patched_from_string: patched_from_string.return_value = mock_problem_key # Ensure that StudentModule.objects.filter returns a result set that list_problem_responses can work with # (this keeps us from having to create fixtures for this test): mock_results = MagicMock( return_value=[result_factory(n) for n in range(5)]) with patch.object(StudentModule, 'objects') as patched_manager: patched_manager.filter.return_value = mock_results mock_problem_location = '' problem_responses = list_problem_responses( self.course_key, problem_location=mock_problem_location) # Check if list_problem_responses called UsageKey.from_string to look up problem key: patched_from_string.assert_called_once_with( mock_problem_location) # Check if list_problem_responses called StudentModule.objects.filter to obtain relevant records: patched_manager.filter.assert_called_once_with( course_id=self.course_key, module_state_key=mock_problem_key) # Check if list_problem_responses returned expected results: self.assertEqual(len(problem_responses), len(mock_results)) for mock_result in mock_results: self.assertIn( { 'username': mock_result.student.username, 'state': mock_result.state }, problem_responses)
def test_list_problem_responses(self): def result_factory(result_id): """ Return a dummy StudentModule object that can be queried for relevant info (student.username and state). """ result = Mock(spec=['student', 'state']) result.student.username.return_value = u'user{}'.format(result_id) result.state.return_value = u'state{}'.format(result_id) return result # Ensure that UsageKey.from_string returns a problem key that list_problem_responses can work with # (even when called with a dummy location): mock_problem_key = Mock(return_value=u'') mock_problem_key.course_key = self.course_key with patch.object(UsageKey, 'from_string') as patched_from_string: patched_from_string.return_value = mock_problem_key # Ensure that StudentModule.objects.filter returns a result set that list_problem_responses can work with # (this keeps us from having to create fixtures for this test): mock_results = MagicMock(return_value=[result_factory(n) for n in range(5)]) with patch.object(StudentModule, 'objects') as patched_manager: patched_manager.filter.return_value = mock_results mock_problem_location = '' problem_responses = list_problem_responses(self.course_key, problem_location=mock_problem_location) # Check if list_problem_responses called UsageKey.from_string to look up problem key: patched_from_string.assert_called_once_with(mock_problem_location) # Check if list_problem_responses called StudentModule.objects.filter to obtain relevant records: patched_manager.filter.assert_called_once_with( course_id=self.course_key, module_state_key=mock_problem_key ) # Check if list_problem_responses returned expected results: self.assertEqual(len(problem_responses), len(mock_results)) for mock_result in mock_results: self.assertIn( {'username': mock_result.student.username, 'state': mock_result.state}, problem_responses )
def _build_student_data(cls, user_id, course_key, usage_key_str): """ Generate a list of problem responses for all problem under the ``problem_location`` root. Arguments: user_id (int): The user id for the user generating the report course_key (CourseKey): The ``CourseKey`` for the course whose report is being generated usage_key_str (str): The generated report will include this block and it child blocks. Returns: Tuple[List[Dict], List[str]]: Returns a list of dictionaries containing the student data which will be included in the final csv, and the features/keys to include in that CSV. """ usage_key = UsageKey.from_string(usage_key_str).map_into_course(course_key) user = get_user_model().objects.get(pk=user_id) course_blocks = get_course_blocks(user, usage_key) student_data = [] max_count = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') store = modulestore() user_state_client = DjangoXBlockUserStateClient() student_data_keys = set() with store.bulk_operations(course_key): for title, path, block_key in cls._build_problem_list(course_blocks, usage_key): # Chapter and sequential blocks are filtered out since they include state # which isn't useful for this report. if block_key.block_type in ('sequential', 'chapter'): continue block = store.get_item(block_key) generated_report_data = {} # Blocks can implement the generate_report_data method to provide their own # human-readable formatting for user state. if hasattr(block, 'generate_report_data'): try: user_state_iterator = user_state_client.iter_all_for_block(block_key) generated_report_data = { username: state for username, state in block.generate_report_data(user_state_iterator, max_count) } except NotImplementedError: pass responses = list_problem_responses(course_key, block_key, max_count) student_data += responses for response in responses: response['title'] = title # A human-readable location for the current block response['location'] = ' > '.join(path) # A machine-friendly location for the current block response['block_key'] = str(block_key) user_data = generated_report_data.get(response['username'], {}) response.update(user_data) student_data_keys = student_data_keys.union(user_data.keys()) if max_count is not None: max_count -= len(responses) if max_count <= 0: break # Keep the keys in a useful order, starting with username, title and location, # then the columns returned by the xblock report generator in sorted order and # finally end with the more machine friendly block_key and state. student_data_keys_list = ( ['username', 'title', 'location'] + sorted(student_data_keys) + ['block_key', 'state'] ) return student_data, student_data_keys_list
def _build_student_data( cls, user_id, course_key, usage_key_str_list, filter_types=None, ): """ Generate a list of problem responses for all problem under the ``problem_location`` root. Arguments: user_id (int): The user id for the user generating the report course_key (CourseKey): The ``CourseKey`` for the course whose report is being generated usage_key_str_list (List[str]): The generated report will include these blocks and their child blocks. filter_types (List[str]): The report generator will only include data for block types in this list. Returns: Tuple[List[Dict], List[str]]: Returns a list of dictionaries containing the student data which will be included in the final csv, and the features/keys to include in that CSV. """ usage_keys = [ UsageKey.from_string(usage_key_str).map_into_course(course_key) for usage_key_str in usage_key_str_list ] user = get_user_model().objects.get(pk=user_id) student_data = [] max_count = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') store = modulestore() user_state_client = DjangoXBlockUserStateClient() student_data_keys = set() with store.bulk_operations(course_key): for usage_key in usage_keys: if max_count is not None and max_count <= 0: break course_blocks = get_course_blocks(user, usage_key) base_path = cls._build_block_base_path( store.get_item(usage_key)) for title, path, block_key in cls._build_problem_list( course_blocks, usage_key): # Chapter and sequential blocks are filtered out since they include state # which isn't useful for this report. if block_key.block_type in ('sequential', 'chapter'): continue if filter_types is not None and block_key.block_type not in filter_types: continue block = store.get_item(block_key) generated_report_data = defaultdict(list) # Blocks can implement the generate_report_data method to provide their own # human-readable formatting for user state. if hasattr(block, 'generate_report_data'): try: user_state_iterator = user_state_client.iter_all_for_block( block_key) for username, state in block.generate_report_data( user_state_iterator, max_count): generated_report_data[username].append(state) except NotImplementedError: pass responses = [] for response in list_problem_responses( course_key, block_key, max_count): response['title'] = title # A human-readable location for the current block response['location'] = ' > '.join(base_path + path) # A machine-friendly location for the current block response['block_key'] = str(block_key) # A block that has a single state per user can contain multiple responses # within the same state. user_states = generated_report_data.get( response['username'], []) if user_states: # For each response in the block, copy over the basic data like the # title, location, block_key and state, and add in the responses for user_state in user_states: user_response = response.copy() user_response.update(user_state) student_data_keys = student_data_keys.union( user_state.keys()) responses.append(user_response) else: responses.append(response) student_data += responses if max_count is not None: max_count -= len(responses) if max_count <= 0: break # Keep the keys in a useful order, starting with username, title and location, # then the columns returned by the xblock report generator in sorted order and # finally end with the more machine friendly block_key and state. student_data_keys_list = (['username', 'title', 'location'] + sorted(student_data_keys) + ['block_key', 'state']) return student_data, student_data_keys_list
def _build_student_data(cls, user_id, course_key, usage_key_str): """ Generate a list of problem responses for all problem under the ``problem_location`` root. Arguments: user_id (int): The user id for the user generating the report course_key (CourseKey): The ``CourseKey`` for the course whose report is being generated usage_key_str (str): The generated report will include this block and it child blocks. Returns: List[Dict]: Returns a list of dictionaries containing the student data which will be included in the final csv. """ usage_key = UsageKey.from_string(usage_key_str).map_into_course( course_key) user = get_user_model().objects.get(pk=user_id) course_blocks = get_course_blocks(user, usage_key) student_data = [] max_count = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') store = modulestore() user_state_client = DjangoXBlockUserStateClient() with store.bulk_operations(course_key): for title, path, block_key in cls._build_problem_list( course_blocks, usage_key): # Chapter and sequential blocks are filtered out since they include state # which isn't useful for this report. if block_key.block_type in ('sequential', 'chapter'): continue block = store.get_item(block_key) # Blocks can implement the generate_report_data method to provide their own # human-readable formatting for user state. if hasattr(block, 'generate_report_data'): try: user_state_iterator = user_state_client.iter_all_for_block( block_key) responses = [{ 'username': username, 'state': state } for username, state in block.generate_report_data( user_state_iterator, max_count)] except NotImplementedError: responses = list_problem_responses( course_key, block_key, max_count) else: responses = list_problem_responses(course_key, block_key, max_count) student_data += responses for response in responses: response['title'] = title # A human-readable location for the current block response['location'] = ' > '.join(path) # A machine-friendly location for the current block response['block_key'] = str(block_key) if max_count is not None: max_count -= len(responses) if max_count <= 0: break return student_data