Skip to content
Open
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions lms/djangoapps/instructor_task/tasks_helper/grades.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -44,6 +45,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

Expand Down Expand Up @@ -823,6 +825,31 @@ 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_key = stack.pop()
block = store.get_item(current_key)
if getattr(block, 'category', '') == 'problem':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you do not need to load the block to check its type; you can look at the key: if current_key.block_type == "problem": ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much @kdmccormick , I applied the change! 8f0799b

problem_keys.append(current_key)
elif hasattr(block, 'children'):
stack.extend(getattr(block, 'children', []))
return problem_keys
Copy link
Member

@mariajgrimaldi mariajgrimaldi Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think directly accessing the children attribute of a block is the right way to retrieve its children. There are two methods for this:

  • get_children -> returns all static children (this is what’s used when building the problem lists here).
  • get_child_block -> returns dynamic children (this is the method used here).

What I’d suggest is updating the method linked above to support retrieving dynamic children even when user_id is not passed (i.e., return all children if no user is specified). I don’t think this is a security concern, but I’m flagging it just in case. If the method can't support returning all children then we could try another way, but I think it's worth a shot.

Then, we could use this updated approach when building the problem list so that it works seamlessly:
grades.py#L829-L851

Let me know what you think!

Copy link
Contributor Author

@efortish efortish Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mariajgrimaldi I was testing, but was actually hard to change the get_child_block behavior, maybe if you have any idea to try it would be great!
Also, now I incorporated the usage of get_children as you said and it worked, it was necessary to normalize it because block.get_children could return blocks instead of usages_keys.

Everything seems work good: 9eb6cb1


@classmethod
def _build_problem_list(cls, course_blocks, root, path=None):
"""
Expand All @@ -831,13 +858,22 @@ 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
"""
name = course_blocks.get_xblock_field(root, 'display_name') or root.block_type
usage key.
"""
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]

Expand Down Expand Up @@ -871,6 +907,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 = []
Expand Down Expand Up @@ -978,11 +1015,23 @@ 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=problem_locations,
usage_key_str_list=expanded_usage_key_strs,
filter_types=filter_types,
)

Expand Down
Loading