From 44ef9e0c6bc638e3ba6f9dd02856153daeb6a9e5 Mon Sep 17 00:00:00 2001 From: efortish Date: Wed, 30 Jul 2025 15:41:48 -0500 Subject: [PATCH 01/14] chore: review comments applied --- .../instructor_task/tasks_helper/grades.py | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 5358af370897..a8db4dd90dea 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -44,6 +44,7 @@ from openedx.core.lib.cache_utils import get_cache from openedx.core.lib.courses import get_course_by_id from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.partitions.partitions_service import PartitionService # lint-amnesty, pylint: disable=wrong-import-order from xmodule.split_test_block import get_split_user_partitions # lint-amnesty, pylint: disable=wrong-import-order @@ -823,6 +824,23 @@ def _build_block_base_path(block): path.append(block.display_name) return list(reversed(path)) + @staticmethod + def resolve_block_descendants(course_key, usage_key): + """ + Return every usage_key of type 'problem' under any block in the course tree. + """ + store = modulestore() + problem_keys = [] + stack = [usage_key] + while stack: + current_key = stack.pop() + block = store.get_item(current_key) + if getattr(block, 'category', '') == 'problem': + problem_keys.append(current_key) + elif hasattr(block, 'children'): + stack.extend(getattr(block, 'children', [])) + return problem_keys + @classmethod def _build_problem_list(cls, course_blocks, root, path=None): """ @@ -837,7 +855,16 @@ def _build_problem_list(cls, course_blocks, root, path=None): Tuple[str, List[str], UsageKey]: tuple of a block's display name, path, and usage key """ - name = course_blocks.get_xblock_field(root, 'display_name') or root.block_type + name = course_blocks.get_xblock_field(root, 'display_name') + if not name or name == 'problem': + # Fallback: CourseBlocks may not have display_name cached for all blocks, + # especially for dynamically generated content or library_content blocks. + # Loading the full block is necessary to get meaningful names for CSV reports + try: + block = modulestore().get_item(root) + name = getattr(block, 'display_name', None) or root.block_type + except ItemNotFoundError: + name = root.block_type if path is None: path = [name] @@ -871,6 +898,7 @@ def _build_student_data( 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 = [] @@ -978,11 +1006,20 @@ def generate(cls, _xblock_instance_args, _entry_id, course_id, task_input, actio if problem_types_filter: filter_types = problem_types_filter.split(',') + # Expand problem locations to include all descendant problems here + expanded_usage_keys = [] + for problem_location_str in problem_locations: + usage_key = UsageKey.from_string(problem_location_str).map_into_course(course_id) + expanded_usage_keys.extend(cls.resolve_block_descendants(course_id, usage_key)) + + # Convert back to strings for consistency with the existing interface + expanded_usage_key_strs = [str(key) for key in expanded_usage_keys] + # Compute result table and format it student_data, student_data_keys = cls._build_student_data( user_id=task_input.get('user_id'), course_key=course_id, - usage_key_str_list=problem_locations, + usage_key_str_list=expanded_usage_key_strs, filter_types=filter_types, ) From dde44378012b69a87eb018c59b3887426f513198 Mon Sep 17 00:00:00 2001 From: efortish Date: Wed, 30 Jul 2025 15:54:18 -0500 Subject: [PATCH 02/14] chore: docstring improved --- lms/djangoapps/instructor_task/tasks_helper/grades.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index a8db4dd90dea..06702ffa0d7c 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -827,7 +827,13 @@ def _build_block_base_path(block): @staticmethod def resolve_block_descendants(course_key, usage_key): """ - Return every usage_key of type 'problem' under any block in the course tree. + Return the display names of the blocks that lie above the supplied block in hierarchy. + + Arguments: + block: a single block + + Returns: + List[str]: a list of display names of blocks starting from the root block (Course) """ store = modulestore() problem_keys = [] From a3b5263510916294321dfc6665f98b810d17c877 Mon Sep 17 00:00:00 2001 From: efortish Date: Wed, 30 Jul 2025 17:28:40 -0500 Subject: [PATCH 03/14] chore: try block added to the expanded_usage_keys --- lms/djangoapps/instructor_task/tasks_helper/grades.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 06702ffa0d7c..0afcab7a2d41 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -827,13 +827,15 @@ def _build_block_base_path(block): @staticmethod def resolve_block_descendants(course_key, usage_key): """ - Return the display names of the blocks that lie above the supplied block in hierarchy. + Return every usage_key of type 'problem' under any block in the course tree. + Recursively traverses the course structure to find all descendant problem blocks. - Arguments: - block: a single block + Args: + course_key: The course identifier + usage_key: The starting block to search from Returns: - List[str]: a list of display names of blocks starting from the root block (Course) + List[UsageKey]: All problem block usage keys found under the root block """ store = modulestore() problem_keys = [] From fc1572deffc9e89f300351185e520151e281e8e0 Mon Sep 17 00:00:00 2001 From: efortish Date: Wed, 30 Jul 2025 18:10:48 -0500 Subject: [PATCH 04/14] chore: now try except added --- lms/djangoapps/instructor_task/tasks_helper/grades.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 0afcab7a2d41..32579e3757c0 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -16,6 +16,7 @@ from django.contrib.auth import get_user_model from lazy import lazy from opaque_keys.edx.keys import UsageKey +from opaque_keys import InvalidKeyError from pytz import UTC from six.moves import zip_longest @@ -1017,8 +1018,12 @@ def generate(cls, _xblock_instance_args, _entry_id, course_id, task_input, actio # Expand problem locations to include all descendant problems here expanded_usage_keys = [] for problem_location_str in problem_locations: - usage_key = UsageKey.from_string(problem_location_str).map_into_course(course_id) - expanded_usage_keys.extend(cls.resolve_block_descendants(course_id, usage_key)) + try: + usage_key = UsageKey.from_string(problem_location_str).map_into_course(course_id) + expanded_usage_keys.extend(cls.resolve_block_descendants(course_id, usage_key)) + except InvalidKeyError: + continue + # Convert back to strings for consistency with the existing interface expanded_usage_key_strs = [str(key) for key in expanded_usage_keys] From 2357237ebed2b1490bfeab0781a8d30ceff8dfd5 Mon Sep 17 00:00:00 2001 From: efortish Date: Wed, 30 Jul 2025 18:15:19 -0500 Subject: [PATCH 05/14] chore: blankspace errased --- lms/djangoapps/instructor_task/tasks_helper/grades.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 32579e3757c0..95d1e696db0b 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -1024,7 +1024,6 @@ def generate(cls, _xblock_instance_args, _entry_id, course_id, task_input, actio except InvalidKeyError: continue - # Convert back to strings for consistency with the existing interface expanded_usage_key_strs = [str(key) for key in expanded_usage_keys] From 26612e5e1a1c34fb15dd6a721e7aa21ba01319e2 Mon Sep 17 00:00:00 2001 From: efortish Date: Wed, 30 Jul 2025 18:22:32 -0500 Subject: [PATCH 06/14] chore: dots added --- lms/djangoapps/instructor_task/tasks_helper/grades.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 95d1e696db0b..af04c109591c 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -858,11 +858,11 @@ def _build_problem_list(cls, course_blocks, root, path=None): Arguments: course_blocks (BlockStructureBlockData): Block structure for a course. root (UsageKey): This block and its children will be used to generate - the problem list + the problem list. path (List[str]): The list of display names for the parent of root block Yields: Tuple[str, List[str], UsageKey]: tuple of a block's display name, path, and - usage key + usage key. """ name = course_blocks.get_xblock_field(root, 'display_name') if not name or name == 'problem': From 8f0799b7e89416d61720d55c0be991bfc38dc474 Mon Sep 17 00:00:00 2001 From: efortish Date: Thu, 31 Jul 2025 18:26:42 -0500 Subject: [PATCH 07/14] chore: look at the key instead of load the block --- .../instructor_task/tasks_helper/grades.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index af04c109591c..da89d17d668f 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -843,11 +843,15 @@ def resolve_block_descendants(course_key, usage_key): stack = [usage_key] while stack: current_key = stack.pop() - block = store.get_item(current_key) - if getattr(block, 'category', '') == 'problem': + if current_key.block_type == 'problem': problem_keys.append(current_key) - elif hasattr(block, 'children'): - stack.extend(getattr(block, 'children', [])) + else: + try: + block = store.get_item(current_key) + if hasattr(block, 'children'): + stack.extend(getattr(block, 'children', [])) + except ItemNotFoundError: + continue return problem_keys @classmethod From 9eb6cb1db7cb1c326d7976496269274606e8b93a Mon Sep 17 00:00:00 2001 From: efortish Date: Thu, 31 Jul 2025 22:58:22 -0500 Subject: [PATCH 08/14] chore: get_children incorporated --- .../instructor_task/tasks_helper/grades.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index da89d17d668f..041460d8a6e2 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -842,14 +842,22 @@ def resolve_block_descendants(course_key, usage_key): problem_keys = [] stack = [usage_key] while stack: - current_key = stack.pop() + current_item = stack.pop() + + if hasattr(current_item, 'location'): + current_key = current_item.location + elif hasattr(current_item, 'scope_ids') and hasattr(current_item.scope_ids, 'usage_id'): + current_key = current_item.scope_ids.usage_id + else: + current_key = current_item + if current_key.block_type == 'problem': problem_keys.append(current_key) else: try: block = store.get_item(current_key) - if hasattr(block, 'children'): - stack.extend(getattr(block, 'children', [])) + child_keys = block.get_children() + stack.extend(child_keys) except ItemNotFoundError: continue return problem_keys From 4f1a6437480a012e5472da4b3ea18c659f2ee520 Mon Sep 17 00:00:00 2001 From: Kevyn Suarez Date: Wed, 14 Jan 2026 18:47:17 -0500 Subject: [PATCH 09/14] chore: exclude ContentLibrary transformers for problem responses export --- .../instructor_task/tasks_helper/grades.py | 94 +++++++------------ 1 file changed, 35 insertions(+), 59 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index ae5de0fa5ff7..98ae7c960545 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -16,7 +16,6 @@ from django.contrib.auth import get_user_model from lazy import lazy from opaque_keys.edx.keys import UsageKey -from opaque_keys import InvalidKeyError from pytz import UTC from six.moves import zip_longest @@ -25,7 +24,8 @@ from common.djangoapps.student.roles import BulkRoleCache from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.api import get_certificates_for_course_and_users -from lms.djangoapps.course_blocks.api import get_course_blocks +from lms.djangoapps.course_blocks.api import get_course_block_access_transformers, get_course_blocks +from lms.djangoapps.course_blocks.transformers import library_content from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient from lms.djangoapps.grades.api import CourseGradeFactory from lms.djangoapps.grades.api import context as grades_context @@ -40,6 +40,7 @@ from lms.djangoapps.teams.models import CourseTeamMembership from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache +from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers from openedx.core.djangoapps.course_groups.cohorts import bulk_cache_cohorts, get_cohort, is_course_cohorted from openedx.core.djangoapps.user_api.course_tag.api import BulkCourseTags from openedx.core.lib.cache_utils import get_cache @@ -828,43 +829,6 @@ def _build_block_base_path(block): path.append(block.display_name) return list(reversed(path)) - @staticmethod - def resolve_block_descendants(course_key, usage_key): - """ - Return every usage_key of type 'problem' under any block in the course tree. - Recursively traverses the course structure to find all descendant problem blocks. - - Args: - course_key: The course identifier - usage_key: The starting block to search from - - Returns: - List[UsageKey]: All problem block usage keys found under the root block - """ - store = modulestore() - problem_keys = [] - stack = [usage_key] - while stack: - current_item = stack.pop() - - if hasattr(current_item, 'location'): - current_key = current_item.location - elif hasattr(current_item, 'scope_ids') and hasattr(current_item.scope_ids, 'usage_id'): - current_key = current_item.scope_ids.usage_id - else: - current_key = current_item - - if current_key.block_type == 'problem': - problem_keys.append(current_key) - else: - try: - block = store.get_item(current_key) - child_keys = block.get_children() - stack.extend(child_keys) - except ItemNotFoundError: - continue - return problem_keys - @classmethod def _build_problem_list(cls, course_blocks, root, path=None): """ @@ -883,7 +847,7 @@ def _build_problem_list(cls, course_blocks, root, path=None): if not name or name == 'problem': # Fallback: CourseBlocks may not have display_name cached for all blocks, # especially for dynamically generated content or library_content blocks. - # Loading the full block is necessary to get meaningful names for CSV reports + # Loading the full block is necessary to get meaningful names for CSV reports. try: block = modulestore().get_item(root) name = getattr(block, 'display_name', None) or root.block_type @@ -895,8 +859,15 @@ def _build_problem_list(cls, course_blocks, root, path=None): yield name, path, root for block in course_blocks.get_children(root): - name = course_blocks.get_xblock_field(block, 'display_name') or block.block_type - yield from cls._build_problem_list(course_blocks, block, path + [name]) + # Apply the same fallback logic for child blocks + child_name = course_blocks.get_xblock_field(block, 'display_name') + if not child_name or child_name == 'problem': + try: + child_block = modulestore().get_item(block) + child_name = getattr(child_block, 'display_name', None) or block.block_type + except ItemNotFoundError: + child_name = block.block_type + yield from cls._build_problem_list(course_blocks, block, path + [child_name]) @classmethod def _build_student_data( @@ -925,6 +896,21 @@ def _build_student_data( user = get_user_model().objects.get(pk=user_id) + # For reporting, we want the full set of descendant blocks including all children + # of library_content blocks (randomized content). The default transformer list includes + # ContentLibraryTransformer which filters children based on per-user selections. + # For staff-generated reports, we bypass those library transformers to see all problems. + report_transformers = BlockStructureTransformers([ + transformer for transformer in get_course_block_access_transformers(user) + if not isinstance( + transformer, + ( + library_content.ContentLibraryTransformer, + library_content.ContentLibraryOrderTransformer, + ) + ) + ]) + student_data = [] max_count = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') @@ -939,12 +925,14 @@ def _build_student_data( for usage_key in usage_keys: # lint-amnesty, pylint: disable=too-many-nested-blocks if max_count is not None and max_count <= 0: break - course_blocks = get_course_blocks(user, usage_key) + course_blocks = get_course_blocks(user, usage_key, transformers=report_transformers) 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'): + # Chapter, sequential, and library_content blocks are filtered out since + # they include state which isn't useful for this report. + # library_content state contains internal selection metadata (which problems + # were randomly assigned to each user), not actual student responses. + if block_key.block_type in ('sequential', 'chapter', 'library_content'): continue if filter_types is not None and block_key.block_type not in filter_types: @@ -1030,23 +1018,11 @@ def generate(cls, _xblock_instance_args, _entry_id, course_id, task_input, actio if problem_types_filter: filter_types = problem_types_filter.split(',') - # Expand problem locations to include all descendant problems here - expanded_usage_keys = [] - for problem_location_str in problem_locations: - try: - usage_key = UsageKey.from_string(problem_location_str).map_into_course(course_id) - expanded_usage_keys.extend(cls.resolve_block_descendants(course_id, usage_key)) - except InvalidKeyError: - continue - - # Convert back to strings for consistency with the existing interface - expanded_usage_key_strs = [str(key) for key in expanded_usage_keys] - # Compute result table and format it student_data, student_data_keys = cls._build_student_data( user_id=task_input.get('user_id'), course_key=course_id, - usage_key_str_list=expanded_usage_key_strs, + usage_key_str_list=problem_locations, filter_types=filter_types, ) From 4f9a318ad99fc7d4833992ec20161c9f9bbf0a57 Mon Sep 17 00:00:00 2001 From: Kevyn Suarez Date: Thu, 15 Jan 2026 15:27:07 -0500 Subject: [PATCH 10/14] fix: title column in report --- .../instructor_task/tasks_helper/grades.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 98ae7c960545..f59583e1b541 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -847,7 +847,7 @@ def _build_problem_list(cls, course_blocks, root, path=None): if not name or name == 'problem': # Fallback: CourseBlocks may not have display_name cached for all blocks, # especially for dynamically generated content or library_content blocks. - # Loading the full block is necessary to get meaningful names for CSV reports. + # Loading the full block is necessary to get meaningful names for CSV reports try: block = modulestore().get_item(root) name = getattr(block, 'display_name', None) or root.block_type @@ -859,15 +859,8 @@ def _build_problem_list(cls, course_blocks, root, path=None): yield name, path, root for block in course_blocks.get_children(root): - # Apply the same fallback logic for child blocks - child_name = course_blocks.get_xblock_field(block, 'display_name') - if not child_name or child_name == 'problem': - try: - child_block = modulestore().get_item(block) - child_name = getattr(child_block, 'display_name', None) or block.block_type - except ItemNotFoundError: - child_name = block.block_type - yield from cls._build_problem_list(course_blocks, block, path + [child_name]) + name = course_blocks.get_xblock_field(block, 'display_name') or block.block_type + yield from cls._build_problem_list(course_blocks, block, path + [name]) @classmethod def _build_student_data( From 3a460aff6371c47af47d8635ae1e2aca5c31c881 Mon Sep 17 00:00:00 2001 From: Kevyn Suarez Date: Fri, 16 Jan 2026 13:41:16 -0500 Subject: [PATCH 11/14] test: exclude transformers test --- .../tests/test_tasks_helper.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index e876fc4e9c9c..339b0134a0f1 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -27,6 +27,7 @@ import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api import openedx.core.djangoapps.content.block_structure.api as bs_api from xmodule.capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory # lint-amnesty, pylint: disable=wrong-import-order +from lms.djangoapps.course_blocks.transformers import library_content from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory @@ -524,6 +525,47 @@ def test_build_student_data_limit(self): assert len(student_data) == 4 + @patch('lms.djangoapps.instructor_task.tasks_helper.grades.list_problem_responses', return_value=[]) + def test_problem_responses_excludes_library_content_transformers(self, _mock_list_problem_responses): + """Ensure ProblemResponses bypasses per-user library_content transformers. + + The default course block access transformers include library_content transformers + that filter children based on the requesting user's selections. Reports must exclude + those transformers so output is not dependent on the instructor running the report. + """ + problem = self.define_option_problem('Problem1') + + captured = {} + + class _FakeCourseBlocks: + def get_xblock_field(self, _usage_key, field_name): + if field_name == 'display_name': + return 'Problem1' + return None + + def get_children(self, _usage_key): + return [] + + def _fake_get_course_blocks(_user, _usage_key, transformers=None, **_kwargs): + captured['transformers'] = transformers + return _FakeCourseBlocks() + + with patch( + 'lms.djangoapps.instructor_task.tasks_helper.grades.get_course_blocks', + side_effect=_fake_get_course_blocks, + ): + ProblemResponses._build_student_data( + user_id=self.instructor.id, + course_key=self.course.id, + usage_key_str_list=[str(problem.location)], + ) + + transformers = captured.get('transformers') + assert transformers is not None + all_transformers = transformers._transformers['supports_filter'] + transformers._transformers['no_filter'] + assert not any(isinstance(t, library_content.ContentLibraryTransformer) for t in all_transformers) + assert not any(isinstance(t, library_content.ContentLibraryOrderTransformer) for t in all_transformers) + @patch( 'lms.djangoapps.instructor_task.tasks_helper.grades.list_problem_responses', wraps=list_problem_responses From 071011cdb6c9ffda91b84b71d8741d4f8b2644d5 Mon Sep 17 00:00:00 2001 From: Kevyn Suarez Date: Fri, 16 Jan 2026 14:03:22 -0500 Subject: [PATCH 12/14] fix: pylint issues --- lms/djangoapps/instructor_task/tests/test_tasks_helper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 339b0134a0f1..d2427a4521e4 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -538,6 +538,7 @@ def test_problem_responses_excludes_library_content_transformers(self, _mock_lis captured = {} class _FakeCourseBlocks: + """Minimal fake CourseBlocks object for testing.""" def get_xblock_field(self, _usage_key, field_name): if field_name == 'display_name': return 'Problem1' From 0888917e82a1f3854ff8e1fbd8282c600b554530 Mon Sep 17 00:00:00 2001 From: Kevyn Suarez Date: Fri, 16 Jan 2026 14:12:55 -0500 Subject: [PATCH 13/14] chore: log added to fallback when the name is empty or generic --- lms/djangoapps/instructor_task/tasks_helper/grades.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index f59583e1b541..72f7379cdf21 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -848,6 +848,12 @@ def _build_problem_list(cls, course_blocks, root, path=None): # Fallback: CourseBlocks may not have display_name cached for all blocks, # especially for dynamically generated content or library_content blocks. # Loading the full block is necessary to get meaningful names for CSV reports + TASK_LOG.debug( + "ProblemResponses: display_name missing in CourseBlocks for %s, falling back to modulestore. " + "Occasional occurrences are expected (e.g., library_content children); " + "frequent occurrences may indicate a cache or transformer issue.", + root, + ) try: block = modulestore().get_item(root) name = getattr(block, 'display_name', None) or root.block_type From 16c1390df3a7ba94f8e71b6b264bb0bcc3480244 Mon Sep 17 00:00:00 2001 From: Kevyn Suarez Date: Thu, 22 Jan 2026 15:52:26 -0500 Subject: [PATCH 14/14] chore: itembank added to the build exclution --- .../instructor_task/tasks_helper/grades.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 72f7379cdf21..edbe6b4f2e80 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -849,8 +849,8 @@ def _build_problem_list(cls, course_blocks, root, path=None): # especially for dynamically generated content or library_content blocks. # Loading the full block is necessary to get meaningful names for CSV reports TASK_LOG.debug( - "ProblemResponses: display_name missing in CourseBlocks for %s, falling back to modulestore. " - "Occasional occurrences are expected (e.g., library_content children); " + "ProblemResponses: display_name missing in course_blocks for %s, falling back to modulestore. " + "Occasional occurrences of this message are expected (e.g., library_content children); " "frequent occurrences may indicate a cache or transformer issue.", root, ) @@ -927,11 +927,12 @@ def _build_student_data( course_blocks = get_course_blocks(user, usage_key, transformers=report_transformers) 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, sequential, and library_content blocks are filtered out since - # they include state which isn't useful for this report. - # library_content state contains internal selection metadata (which problems - # were randomly assigned to each user), not actual student responses. - if block_key.block_type in ('sequential', 'chapter', 'library_content'): + # Chapter, sequential, library_content, and itembank blocks are filtered out + # since they include state which isn't useful for this report. + # library_content (V1) and itembank (V2) state contains internal selection + # metadata (which problems were randomly assigned to each user), not actual + # student responses. + if block_key.block_type in ('sequential', 'chapter', 'library_content', 'itembank'): continue if filter_types is not None and block_key.block_type not in filter_types: