Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

course_optimizer_provider tests #36033

Merged
merged 9 commits into from
Jan 17, 2025
Empty file.
39 changes: 28 additions & 11 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,7 +68,7 @@ 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,
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 Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"""
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_from_node_tree_recursive
)

class TestLinkCheck(CourseTestCase):
"""
Tests for the link check functionality
"""
def setUp(self):
"""Setup for tests"""
global MOCK_TREE
global MOCK_XBLOCK_DICTIONARY
global MOCK_COURSE
global MOCK_SECTION
global MOCK_SUBSECTION
global MOCK_UNIT
global MOCK_BLOCK

MOCK_TREE = {
'chapter_1': {
'sequential_1': {
'vertical_1': {
'block_1': {}
}
}
}
}

MOCK_XBLOCK_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']
}
}

MOCK_COURSE = Mock()
MOCK_SECTION = Mock(
location=Mock(block_id='chapter_1'),
display_name='Section Name',
category='chapter'
)
MOCK_SUBSECTION = Mock(
location=Mock(block_id='sequential_1'),
display_name='Subsection Name',
category='sequential'
)
MOCK_UNIT = Mock(
location=Mock(block_id='vertical_1'),
display_name='Unit Name',
category='vertical'
)
MOCK_BLOCK = Mock(
location=Mock(block_id='block_1'),
display_name='Block Name',
course_id='course-v1:test+course',
category='html'
)
# MOCK_BLOCK.location.__str__.return_value = 'block_location_str'
MOCK_COURSE.get_parent.return_value = None
MOCK_SECTION.get_parent.return_value = MOCK_COURSE
MOCK_SUBSECTION.get_parent.return_value = MOCK_SECTION
MOCK_UNIT.get_parent.return_value = MOCK_SUBSECTION
MOCK_BLOCK.get_parent.return_value = MOCK_UNIT


def test_update_node_tree_and_dictionary(self):
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Also the convention we use for pytest is that the function docstring describes quite clearly what the unit under test is supposed to do and what we are testing.

"""Test _update_node_tree_and_dictionary"""
expected_tree = MOCK_TREE
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/{MOCK_BLOCK.course_id}/editor/html/{MOCK_BLOCK.location}',
'locked_links': ['example_link']
}
}

result_tree, result_dictionary = _update_node_tree_and_dictionary(
MOCK_BLOCK, 'example_link', True, {}, {}
)

self.assertEqual(expected_tree, result_tree)
self.assertEqual(expected_dictionary, result_dictionary)


def test_get_node_path(self):
Copy link
Member

Choose a reason for hiding this comment

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

See naming comment above

"""Tests _get_node_path"""
expected_result = [MOCK_SECTION, MOCK_SUBSECTION, MOCK_UNIT, MOCK_BLOCK]
result = _get_node_path(MOCK_BLOCK)
self.assertEqual(expected_result, result)


def test_create_dto_recursive_empty(self):
"""Tests _create_dto_from_node_tree_recursive"""
expected = _create_dto_from_node_tree_recursive({}, {})
self.assertEqual(None, expected)


def test_create_dto_recursive_leaf_node(self):
"""Tests _create_dto_from_node_tree_recursive"""
expected_result = {
'blocks': [
{
'id': 'block_1',
'displayName': 'Block Name',
'url': '/block/1',
'brokenLinks': ['broken_link_1', 'broken_link_2'],
'lockedLinks': ['locked_link']
}
]
}
expected = _create_dto_from_node_tree_recursive(
MOCK_TREE['chapter_1']['sequential_1']['vertical_1'],
MOCK_XBLOCK_DICTIONARY
)
self.assertEqual(expected_result, expected)


def test_create_dto_recursive_full_tree(self):
"""Tests _create_dto_from_node_tree_recursive"""
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']
}
]
}
]
}
]
}
]
}

expected = _create_dto_from_node_tree_recursive(MOCK_TREE, MOCK_XBLOCK_DICTIONARY)
self.assertEqual(expected_result, expected)

7 changes: 3 additions & 4 deletions cms/djangoapps/contentstore/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,8 @@ def test_register_exams_failure(self, _mock_register_exams_proctoring, _mock_reg
course_publish.assert_called()


class CourseOptimizerTestCase(TestCase):


@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class CourseLinkCheckTestCase(CourseTestCase):
def test_user_does_not_exist_raises_exception(self):
raise NotImplementedError

Expand Down Expand Up @@ -265,4 +264,4 @@ def test_max_number_of_retries_is_respected(self):
raise NotImplementedError

def test_scan_generates_file_named_by_course_key(self):
raise NotImplementedErro
raise NotImplementedError
Loading