Skip to content

Commit

Permalink
course_optimizer_provider tests (#36033)
Browse files Browse the repository at this point in the history
  • Loading branch information
rayzhou-bit authored Jan 17, 2025
1 parent 9507b12 commit 85c14da
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 15 deletions.
Empty file.
48 changes: 33 additions & 15 deletions cms/djangoapps/contentstore/core/course_optimizer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ def generate_broken_links_descriptor(json_content, request_user):
"""
Returns a Data Transfer Object for frontend given a list of broken links.
json_content contains a list of [block_id, link, is_locked]
is_locked is true if the link is a studio link and returns 403 on request
** Example json_content structure **
Note: is_locked is true if the link is a studio link and returns 403
[
['block_id_1', 'link_1', is_locked],
['block_id_1', 'link_2', is_locked],
['block_id_2', 'link_3', is_locked],
...
]
** Example DTO structure **
{
Expand Down Expand Up @@ -62,15 +68,15 @@ def generate_broken_links_descriptor(json_content, request_user):

usage_key = usage_key_with_run(block_id)
block = get_xblock(usage_key, request_user)
_update_node_tree_and_dictionary(
xblock_node_tree, xblock_dictionary = _update_node_tree_and_dictionary(
block=block,
link=link,
is_locked=is_locked_flag,
node_tree=xblock_node_tree,
dictionary=xblock_dictionary
)

return _create_dto_from_node_tree_recursive(xblock_node_tree, xblock_dictionary)
return _create_dto_recursive(xblock_node_tree, xblock_dictionary)


def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictionary):
Expand Down Expand Up @@ -100,20 +106,29 @@ def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictiona
** Example dictionary structure **
{
'xblock_id: {
'display_name': 'xblock name'
'category': 'html'
'display_name': 'xblock name',
'category': 'chapter'
},
'html_block_id': {
'display_name': 'xblock name',
'category': 'chapter',
'url': 'url_1',
'locked_links': [...],
'broken_links': [...]
}
...,
}
"""
updated_tree, updated_dictionary = node_tree, dictionary

path = _get_node_path(block)
current_node = node_tree
current_node = updated_tree
xblock_id = ''

# Traverse the path and build the tree structure
for xblock in path:
xblock_id = xblock.location.block_id
dictionary.setdefault(xblock_id,
updated_dictionary.setdefault(xblock_id,
{
'display_name': xblock.display_name,
'category': getattr(xblock, 'category', ''),
Expand All @@ -123,18 +138,20 @@ def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictiona
current_node = current_node.setdefault(xblock_id, {})

# Add block-level details for the last xblock in the path (URL and broken/locked links)
dictionary[xblock_id].setdefault('url',
updated_dictionary[xblock_id].setdefault('url',
f'/course/{block.course_id}/editor/{block.category}/{block.location}'
)
if is_locked:
dictionary[xblock_id].setdefault('locked_links', []).append(link)
updated_dictionary[xblock_id].setdefault('locked_links', []).append(link)
else:
dictionary[xblock_id].setdefault('broken_links', []).append(link)
updated_dictionary[xblock_id].setdefault('broken_links', []).append(link)

return updated_tree, updated_dictionary


def _get_node_path(block):
"""
Retrieves the path frmo the course root node to a specific block, excluding the root.
Retrieves the path from the course root node to a specific block, excluding the root.
** Example Path structure **
[chapter_node, sequential_node, vertical_node, html_node]
Expand All @@ -156,9 +173,10 @@ def _get_node_path(block):
}


def _create_dto_from_node_tree_recursive(xblock_node, xblock_dictionary):
def _create_dto_recursive(xblock_node, xblock_dictionary):
"""
Recursively build the Data Transfer Object from the node tree and dictionary.
Recursively build the Data Transfer Object by using
the structure from the node tree and data from the dictionary.
"""
# Exit condition when there are no more child nodes (at block level)
if not xblock_node:
Expand All @@ -168,7 +186,7 @@ def _create_dto_from_node_tree_recursive(xblock_node, xblock_dictionary):
xblock_children = []

for xblock_id, node in xblock_node.items():
child_blocks = _create_dto_from_node_tree_recursive(node, xblock_dictionary)
child_blocks = _create_dto_recursive(node, xblock_dictionary)
xblock_data = xblock_dictionary.get(xblock_id, {})

xblock_entry = {
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"""
Tests for course optimizer
"""

import unittest
from unittest.mock import Mock, patch

from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.core.course_optimizer_provider import (
generate_broken_links_descriptor,
_update_node_tree_and_dictionary,
_get_node_path,
_create_dto_recursive
)

class TestLinkCheckProvider(CourseTestCase):
"""
Tests for functions that generate a json structure of locked and broken links
to send to the frontend.
"""
def setUp(self):
"""Setup course blocks for tests"""
super().setUp()
self.mock_course = Mock()
self.mock_section = Mock(
location=Mock(block_id='chapter_1'),
display_name='Section Name',
category='chapter'
)
self.mock_subsection = Mock(
location=Mock(block_id='sequential_1'),
display_name='Subsection Name',
category='sequential'
)
self.mock_unit = Mock(
location=Mock(block_id='vertical_1'),
display_name='Unit Name',
category='vertical'
)
self.mock_block = Mock(
location=Mock(block_id='block_1'),
display_name='Block Name',
course_id=self.course.id,
category='html'
)
self.mock_course.get_parent.return_value = None
self.mock_section.get_parent.return_value = self.mock_course
self.mock_subsection.get_parent.return_value = self.mock_section
self.mock_unit.get_parent.return_value = self.mock_subsection
self.mock_block.get_parent.return_value = self.mock_unit


def test_update_node_tree_and_dictionary_returns_node_tree(self):
"""
Verify _update_node_tree_and_dictionary creates a node tree structure
when passed a block level xblock.
"""
expected_tree = {
'chapter_1': {
'sequential_1': {
'vertical_1': {
'block_1': {}
}
}
}
}
result_tree, result_dictionary = _update_node_tree_and_dictionary(
self.mock_block, 'example_link', True, {}, {}
)

self.assertEqual(expected_tree, result_tree)


def test_update_node_tree_and_dictionary_returns_dictionary(self):
"""
Verify _update_node_tree_and_dictionary creates a dictionary of parent xblock entries
when passed a block level xblock.
"""
expected_dictionary = {
'chapter_1': {
'display_name': 'Section Name',
'category': 'chapter'
},
'sequential_1': {
'display_name': 'Subsection Name',
'category': 'sequential'
},
'vertical_1': {
'display_name': 'Unit Name',
'category': 'vertical'
},
'block_1': {
'display_name': 'Block Name',
'category': 'html',
'url': f'/course/{self.course.id}/editor/html/{self.mock_block.location}',
'locked_links': ['example_link']
}
}
result_tree, result_dictionary = _update_node_tree_and_dictionary(
self.mock_block, 'example_link', True, {}, {}
)

self.assertEqual(expected_dictionary, result_dictionary)


def test_create_dto_recursive_returns_for_empty_node(self):
"""
Test _create_dto_recursive behavior at the end of recursion.
Function should return None when given empty node tree and empty dictionary.
"""
expected = _create_dto_recursive({}, {})
self.assertEqual(None, expected)


def test_create_dto_recursive_returns_for_leaf_node(self):
"""
Test _create_dto_recursive behavior at the step before the end of recursion.
When evaluating a leaf node in the node tree, the function should return broken links
and locked links data from the leaf node.
"""
expected_result = {
'blocks': [
{
'id': 'block_1',
'displayName': 'Block Name',
'url': '/block/1',
'brokenLinks': ['broken_link_1', 'broken_link_2'],
'lockedLinks': ['locked_link']
}
]
}

mock_node_tree = {
'block_1': {}
}
mock_dictionary = {
'chapter_1': {
'display_name': 'Section Name',
'category': 'chapter'
},
'sequential_1': {
'display_name': 'Subsection Name',
'category': 'sequential'
},
'vertical_1': {
'display_name': 'Unit Name',
'category': 'vertical'
},
'block_1': {
'display_name': 'Block Name',
'url': '/block/1',
'broken_links': ['broken_link_1', 'broken_link_2'],
'locked_links': ['locked_link']
}
}
expected = _create_dto_recursive(mock_node_tree, mock_dictionary)
self.assertEqual(expected_result, expected)


def test_create_dto_recursive_returns_for_full_tree(self):
"""
Test _create_dto_recursive behavior when recursing many times.
When evaluating a fully mocked node tree and dictionary, the function should return
a full json DTO prepared for frontend.
"""
expected_result = {
'sections': [
{
'id': 'chapter_1',
'displayName': 'Section Name',
'subsections': [
{
'id': 'sequential_1',
'displayName': 'Subsection Name',
'units': [
{
'id': 'vertical_1',
'displayName': 'Unit Name',
'blocks': [
{
'id': 'block_1',
'displayName': 'Block Name',
'url': '/block/1',
'brokenLinks': ['broken_link_1', 'broken_link_2'],
'lockedLinks': ['locked_link']
}
]
}
]
}
]
}
]
}

mock_node_tree = {
'chapter_1': {
'sequential_1': {
'vertical_1': {
'block_1': {}
}
}
}
}
mock_dictionary = {
'chapter_1': {
'display_name': 'Section Name',
'category': 'chapter'
},
'sequential_1': {
'display_name': 'Subsection Name',
'category': 'sequential'
},
'vertical_1': {
'display_name': 'Unit Name',
'category': 'vertical'
},
'block_1': {
'display_name': 'Block Name',
'url': '/block/1',
'broken_links': ['broken_link_1', 'broken_link_2'],
'locked_links': ['locked_link']
}
}
expected = _create_dto_recursive(mock_node_tree, mock_dictionary)

self.assertEqual(expected_result, expected)

0 comments on commit 85c14da

Please sign in to comment.