Skip to content

Commit 85c14da

Browse files
authored
course_optimizer_provider tests (#36033)
1 parent 9507b12 commit 85c14da

File tree

4 files changed

+261
-15
lines changed

4 files changed

+261
-15
lines changed

cms/djangoapps/contentstore/core/__init__.py

Whitespace-only changes.

cms/djangoapps/contentstore/core/course_optimizer_provider.py

+33-15
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ def generate_broken_links_descriptor(json_content, request_user):
1212
"""
1313
Returns a Data Transfer Object for frontend given a list of broken links.
1414
15-
json_content contains a list of [block_id, link, is_locked]
16-
is_locked is true if the link is a studio link and returns 403 on request
15+
** Example json_content structure **
16+
Note: is_locked is true if the link is a studio link and returns 403
17+
[
18+
['block_id_1', 'link_1', is_locked],
19+
['block_id_1', 'link_2', is_locked],
20+
['block_id_2', 'link_3', is_locked],
21+
...
22+
]
1723
1824
** Example DTO structure **
1925
{
@@ -62,15 +68,15 @@ def generate_broken_links_descriptor(json_content, request_user):
6268

6369
usage_key = usage_key_with_run(block_id)
6470
block = get_xblock(usage_key, request_user)
65-
_update_node_tree_and_dictionary(
71+
xblock_node_tree, xblock_dictionary = _update_node_tree_and_dictionary(
6672
block=block,
6773
link=link,
6874
is_locked=is_locked_flag,
6975
node_tree=xblock_node_tree,
7076
dictionary=xblock_dictionary
7177
)
7278

73-
return _create_dto_from_node_tree_recursive(xblock_node_tree, xblock_dictionary)
79+
return _create_dto_recursive(xblock_node_tree, xblock_dictionary)
7480

7581

7682
def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictionary):
@@ -100,20 +106,29 @@ def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictiona
100106
** Example dictionary structure **
101107
{
102108
'xblock_id: {
103-
'display_name': 'xblock name'
104-
'category': 'html'
109+
'display_name': 'xblock name',
110+
'category': 'chapter'
105111
},
112+
'html_block_id': {
113+
'display_name': 'xblock name',
114+
'category': 'chapter',
115+
'url': 'url_1',
116+
'locked_links': [...],
117+
'broken_links': [...]
118+
}
106119
...,
107120
}
108121
"""
122+
updated_tree, updated_dictionary = node_tree, dictionary
123+
109124
path = _get_node_path(block)
110-
current_node = node_tree
125+
current_node = updated_tree
111126
xblock_id = ''
112127

113128
# Traverse the path and build the tree structure
114129
for xblock in path:
115130
xblock_id = xblock.location.block_id
116-
dictionary.setdefault(xblock_id,
131+
updated_dictionary.setdefault(xblock_id,
117132
{
118133
'display_name': xblock.display_name,
119134
'category': getattr(xblock, 'category', ''),
@@ -123,18 +138,20 @@ def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictiona
123138
current_node = current_node.setdefault(xblock_id, {})
124139

125140
# Add block-level details for the last xblock in the path (URL and broken/locked links)
126-
dictionary[xblock_id].setdefault('url',
141+
updated_dictionary[xblock_id].setdefault('url',
127142
f'/course/{block.course_id}/editor/{block.category}/{block.location}'
128143
)
129144
if is_locked:
130-
dictionary[xblock_id].setdefault('locked_links', []).append(link)
145+
updated_dictionary[xblock_id].setdefault('locked_links', []).append(link)
131146
else:
132-
dictionary[xblock_id].setdefault('broken_links', []).append(link)
147+
updated_dictionary[xblock_id].setdefault('broken_links', []).append(link)
148+
149+
return updated_tree, updated_dictionary
133150

134151

135152
def _get_node_path(block):
136153
"""
137-
Retrieves the path frmo the course root node to a specific block, excluding the root.
154+
Retrieves the path from the course root node to a specific block, excluding the root.
138155
139156
** Example Path structure **
140157
[chapter_node, sequential_node, vertical_node, html_node]
@@ -156,9 +173,10 @@ def _get_node_path(block):
156173
}
157174

158175

159-
def _create_dto_from_node_tree_recursive(xblock_node, xblock_dictionary):
176+
def _create_dto_recursive(xblock_node, xblock_dictionary):
160177
"""
161-
Recursively build the Data Transfer Object from the node tree and dictionary.
178+
Recursively build the Data Transfer Object by using
179+
the structure from the node tree and data from the dictionary.
162180
"""
163181
# Exit condition when there are no more child nodes (at block level)
164182
if not xblock_node:
@@ -168,7 +186,7 @@ def _create_dto_from_node_tree_recursive(xblock_node, xblock_dictionary):
168186
xblock_children = []
169187

170188
for xblock_id, node in xblock_node.items():
171-
child_blocks = _create_dto_from_node_tree_recursive(node, xblock_dictionary)
189+
child_blocks = _create_dto_recursive(node, xblock_dictionary)
172190
xblock_data = xblock_dictionary.get(xblock_id, {})
173191

174192
xblock_entry = {

cms/djangoapps/contentstore/core/tests/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""
2+
Tests for course optimizer
3+
"""
4+
5+
import unittest
6+
from unittest.mock import Mock, patch
7+
8+
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
9+
from cms.djangoapps.contentstore.core.course_optimizer_provider import (
10+
generate_broken_links_descriptor,
11+
_update_node_tree_and_dictionary,
12+
_get_node_path,
13+
_create_dto_recursive
14+
)
15+
16+
class TestLinkCheckProvider(CourseTestCase):
17+
"""
18+
Tests for functions that generate a json structure of locked and broken links
19+
to send to the frontend.
20+
"""
21+
def setUp(self):
22+
"""Setup course blocks for tests"""
23+
super().setUp()
24+
self.mock_course = Mock()
25+
self.mock_section = Mock(
26+
location=Mock(block_id='chapter_1'),
27+
display_name='Section Name',
28+
category='chapter'
29+
)
30+
self.mock_subsection = Mock(
31+
location=Mock(block_id='sequential_1'),
32+
display_name='Subsection Name',
33+
category='sequential'
34+
)
35+
self.mock_unit = Mock(
36+
location=Mock(block_id='vertical_1'),
37+
display_name='Unit Name',
38+
category='vertical'
39+
)
40+
self.mock_block = Mock(
41+
location=Mock(block_id='block_1'),
42+
display_name='Block Name',
43+
course_id=self.course.id,
44+
category='html'
45+
)
46+
self.mock_course.get_parent.return_value = None
47+
self.mock_section.get_parent.return_value = self.mock_course
48+
self.mock_subsection.get_parent.return_value = self.mock_section
49+
self.mock_unit.get_parent.return_value = self.mock_subsection
50+
self.mock_block.get_parent.return_value = self.mock_unit
51+
52+
53+
def test_update_node_tree_and_dictionary_returns_node_tree(self):
54+
"""
55+
Verify _update_node_tree_and_dictionary creates a node tree structure
56+
when passed a block level xblock.
57+
"""
58+
expected_tree = {
59+
'chapter_1': {
60+
'sequential_1': {
61+
'vertical_1': {
62+
'block_1': {}
63+
}
64+
}
65+
}
66+
}
67+
result_tree, result_dictionary = _update_node_tree_and_dictionary(
68+
self.mock_block, 'example_link', True, {}, {}
69+
)
70+
71+
self.assertEqual(expected_tree, result_tree)
72+
73+
74+
def test_update_node_tree_and_dictionary_returns_dictionary(self):
75+
"""
76+
Verify _update_node_tree_and_dictionary creates a dictionary of parent xblock entries
77+
when passed a block level xblock.
78+
"""
79+
expected_dictionary = {
80+
'chapter_1': {
81+
'display_name': 'Section Name',
82+
'category': 'chapter'
83+
},
84+
'sequential_1': {
85+
'display_name': 'Subsection Name',
86+
'category': 'sequential'
87+
},
88+
'vertical_1': {
89+
'display_name': 'Unit Name',
90+
'category': 'vertical'
91+
},
92+
'block_1': {
93+
'display_name': 'Block Name',
94+
'category': 'html',
95+
'url': f'/course/{self.course.id}/editor/html/{self.mock_block.location}',
96+
'locked_links': ['example_link']
97+
}
98+
}
99+
result_tree, result_dictionary = _update_node_tree_and_dictionary(
100+
self.mock_block, 'example_link', True, {}, {}
101+
)
102+
103+
self.assertEqual(expected_dictionary, result_dictionary)
104+
105+
106+
def test_create_dto_recursive_returns_for_empty_node(self):
107+
"""
108+
Test _create_dto_recursive behavior at the end of recursion.
109+
Function should return None when given empty node tree and empty dictionary.
110+
"""
111+
expected = _create_dto_recursive({}, {})
112+
self.assertEqual(None, expected)
113+
114+
115+
def test_create_dto_recursive_returns_for_leaf_node(self):
116+
"""
117+
Test _create_dto_recursive behavior at the step before the end of recursion.
118+
When evaluating a leaf node in the node tree, the function should return broken links
119+
and locked links data from the leaf node.
120+
"""
121+
expected_result = {
122+
'blocks': [
123+
{
124+
'id': 'block_1',
125+
'displayName': 'Block Name',
126+
'url': '/block/1',
127+
'brokenLinks': ['broken_link_1', 'broken_link_2'],
128+
'lockedLinks': ['locked_link']
129+
}
130+
]
131+
}
132+
133+
mock_node_tree = {
134+
'block_1': {}
135+
}
136+
mock_dictionary = {
137+
'chapter_1': {
138+
'display_name': 'Section Name',
139+
'category': 'chapter'
140+
},
141+
'sequential_1': {
142+
'display_name': 'Subsection Name',
143+
'category': 'sequential'
144+
},
145+
'vertical_1': {
146+
'display_name': 'Unit Name',
147+
'category': 'vertical'
148+
},
149+
'block_1': {
150+
'display_name': 'Block Name',
151+
'url': '/block/1',
152+
'broken_links': ['broken_link_1', 'broken_link_2'],
153+
'locked_links': ['locked_link']
154+
}
155+
}
156+
expected = _create_dto_recursive(mock_node_tree, mock_dictionary)
157+
self.assertEqual(expected_result, expected)
158+
159+
160+
def test_create_dto_recursive_returns_for_full_tree(self):
161+
"""
162+
Test _create_dto_recursive behavior when recursing many times.
163+
When evaluating a fully mocked node tree and dictionary, the function should return
164+
a full json DTO prepared for frontend.
165+
"""
166+
expected_result = {
167+
'sections': [
168+
{
169+
'id': 'chapter_1',
170+
'displayName': 'Section Name',
171+
'subsections': [
172+
{
173+
'id': 'sequential_1',
174+
'displayName': 'Subsection Name',
175+
'units': [
176+
{
177+
'id': 'vertical_1',
178+
'displayName': 'Unit Name',
179+
'blocks': [
180+
{
181+
'id': 'block_1',
182+
'displayName': 'Block Name',
183+
'url': '/block/1',
184+
'brokenLinks': ['broken_link_1', 'broken_link_2'],
185+
'lockedLinks': ['locked_link']
186+
}
187+
]
188+
}
189+
]
190+
}
191+
]
192+
}
193+
]
194+
}
195+
196+
mock_node_tree = {
197+
'chapter_1': {
198+
'sequential_1': {
199+
'vertical_1': {
200+
'block_1': {}
201+
}
202+
}
203+
}
204+
}
205+
mock_dictionary = {
206+
'chapter_1': {
207+
'display_name': 'Section Name',
208+
'category': 'chapter'
209+
},
210+
'sequential_1': {
211+
'display_name': 'Subsection Name',
212+
'category': 'sequential'
213+
},
214+
'vertical_1': {
215+
'display_name': 'Unit Name',
216+
'category': 'vertical'
217+
},
218+
'block_1': {
219+
'display_name': 'Block Name',
220+
'url': '/block/1',
221+
'broken_links': ['broken_link_1', 'broken_link_2'],
222+
'locked_links': ['locked_link']
223+
}
224+
}
225+
expected = _create_dto_recursive(mock_node_tree, mock_dictionary)
226+
227+
self.assertEqual(expected_result, expected)
228+

0 commit comments

Comments
 (0)