diff --git a/cms/djangoapps/contentstore/core/course_optimizer_provider.py b/cms/djangoapps/contentstore/core/course_optimizer_provider.py index 66353980d5d2..8d40b958f08e 100644 --- a/cms/djangoapps/contentstore/core/course_optimizer_provider.py +++ b/cms/djangoapps/contentstore/core/course_optimizer_provider.py @@ -8,6 +8,8 @@ from cms.djangoapps.contentstore.tasks import CourseLinkCheckTask, LinkState from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import get_xblock from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import usage_key_with_run +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore # Restricts status in the REST API to only those which the requesting user has permission to view. @@ -285,3 +287,26 @@ def _create_dto_recursive(xblock_node, xblock_dictionary): xblock_children.append(xblock_entry) return {level: xblock_children} if level else None + + +def sort_course_sections(course_key, data): + """Retrieve and sort course sections based on the published course structure.""" + course_blocks = modulestore().get_items( + course_key, + qualifiers={'category': 'course'}, + revision=ModuleStoreEnum.RevisionOption.published_only + ) + + if not course_blocks or 'LinkCheckOutput' not in data or 'sections' not in data['LinkCheckOutput']: + return data # Return unchanged data if course_blocks or required keys are missing + + sorted_section_ids = [section.location.block_id for section in course_blocks[0].get_children()] + + sections_map = {section['id']: section for section in data['LinkCheckOutput']['sections']} + data['LinkCheckOutput']['sections'] = [ + sections_map[section_id] + for section_id in sorted_section_ids + if section_id in sections_map + ] + + return data diff --git a/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py b/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py index 72bd4120321e..ca0b73af71da 100644 --- a/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py +++ b/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py @@ -1,12 +1,14 @@ """ Tests for course optimizer """ +from unittest import mock from unittest.mock import Mock from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.core.course_optimizer_provider import ( _update_node_tree_and_dictionary, - _create_dto_recursive + _create_dto_recursive, + sort_course_sections ) from cms.djangoapps.contentstore.tasks import LinkState @@ -222,3 +224,74 @@ def test_create_dto_recursive_returns_for_full_tree(self): expected = _create_dto_recursive(mock_node_tree, mock_dictionary) self.assertEqual(expected_result, expected) + + @mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True) + def test_returns_unchanged_data_if_no_course_blocks(self, mock_modulestore): + """Test that the function returns unchanged data if no course blocks exist.""" + mock_modulestore_instance = Mock() + mock_modulestore.return_value = mock_modulestore_instance + mock_modulestore_instance.get_items.return_value = [] + + data = {} + result = sort_course_sections("course-v1:Test+Course", data) + assert result == data # Should return the original data + + @mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True) + def test_returns_unchanged_data_if_linkcheckoutput_missing(self, mock_modulestore): + """Test that the function returns unchanged data if 'LinkCheckOutput' is missing.""" + + mock_modulestore_instance = Mock() + mock_modulestore.return_value = mock_modulestore_instance + + data = {'LinkCheckStatus': 'Uninitiated'} # No 'LinkCheckOutput' + mock_modulestore_instance.get_items.return_value = data + + result = sort_course_sections("course-v1:Test+Course", data) + assert result == data + + @mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True) + def test_returns_unchanged_data_if_sections_missing(self, mock_modulestore): + """Test that the function returns unchanged data if 'sections' is missing.""" + + mock_modulestore_instance = Mock() + mock_modulestore.return_value = mock_modulestore_instance + + data = {'LinkCheckStatus': 'Success', 'LinkCheckOutput': {}} # No 'LinkCheckOutput' + mock_modulestore_instance.get_items.return_value = data + + result = sort_course_sections("course-v1:Test+Course", data) + assert result == data + + @mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True) + def test_sorts_sections_correctly(self, mock_modulestore): + """Test that the function correctly sorts sections based on published course structure.""" + + mock_course_block = Mock() + mock_course_block.get_children.return_value = [ + Mock(location=Mock(block_id="section2")), + Mock(location=Mock(block_id="section3")), + Mock(location=Mock(block_id="section1")), + ] + + mock_modulestore_instance = Mock() + mock_modulestore.return_value = mock_modulestore_instance + mock_modulestore_instance.get_items.return_value = [mock_course_block] + + data = { + "LinkCheckOutput": { + "sections": [ + {"id": "section1", "name": "Intro"}, + {"id": "section2", "name": "Advanced"}, + {"id": "section3", "name": "Bonus"}, # Not in course structure + ] + } + } + + result = sort_course_sections("course-v1:Test+Course", data) + expected_sections = [ + {"id": "section2", "name": "Advanced"}, + {"id": "section3", "name": "Bonus"}, + {"id": "section1", "name": "Intro"}, + ] + + assert result["LinkCheckOutput"]["sections"] == expected_sections diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py b/cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py index 9aa23838e6cf..24c8dd0d18f8 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from user_tasks.models import UserTaskStatus -from cms.djangoapps.contentstore.core.course_optimizer_provider import get_link_check_data +from cms.djangoapps.contentstore.core.course_optimizer_provider import get_link_check_data, sort_course_sections from cms.djangoapps.contentstore.rest_api.v0.serializers.course_optimizer import LinkCheckSerializer from cms.djangoapps.contentstore.tasks import check_broken_links from common.djangoapps.student.auth import has_course_author_access, has_studio_read_access @@ -139,6 +139,7 @@ def get(self, request: Request, course_id: str): self.permission_denied(request) data = get_link_check_data(request, course_id) - serializer = LinkCheckSerializer(data) + data = sort_course_sections(course_key, data) + serializer = LinkCheckSerializer(data) return Response(serializer.data)