diff --git a/cms/djangoapps/contentstore/rest_api/serializers/common.py b/cms/djangoapps/contentstore/rest_api/serializers/common.py index 362504ccb0..824054330f 100644 --- a/cms/djangoapps/contentstore/rest_api/serializers/common.py +++ b/cms/djangoapps/contentstore/rest_api/serializers/common.py @@ -50,3 +50,23 @@ class StrictSerializer(serializers.Serializer): ) return ret + + +class ProctoringErrorModelSerializer(serializers.Serializer): + """ + Serializer for proctoring error model item. + """ + deprecated = serializers.BooleanField() + display_name = serializers.CharField() + help = serializers.CharField() + hide_on_enabled_publisher = serializers.BooleanField() + value = serializers.CharField() + + +class ProctoringErrorListSerializer(serializers.Serializer): + """ + Serializer for proctoring error list. + """ + key = serializers.CharField() + message = serializers.CharField() + model = ProctoringErrorModelSerializer() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index f47c9911b2..12f797e8e6 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -4,6 +4,7 @@ Serializers for v1 contentstore API. from .course_details import CourseDetailsSerializer from .course_rerun import CourseRerunSerializer from .course_team import CourseTeamSerializer +from .course_index import CourseIndexSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer from .home import CourseHomeSerializer from .proctoring import ( diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py new file mode 100644 index 0000000000..d423f7e9da --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py @@ -0,0 +1,31 @@ +""" +API Serializers for course index +""" + +from rest_framework import serializers + +from cms.djangoapps.contentstore.rest_api.serializers.common import ProctoringErrorListSerializer + + +class InitialIndexStateSerializer(serializers.Serializer): + """Serializer for initial course index state""" + expanded_locators = serializers.ListSerializer(child=serializers.CharField()) + locator_to_show = serializers.CharField() + + +class CourseIndexSerializer(serializers.Serializer): + """Serializer for course index""" + course_release_date = serializers.CharField() + course_structure = serializers.DictField() + deprecated_blocks_info = serializers.DictField() + discussions_incontext_feedback_url = serializers.CharField() + discussions_incontext_learnmore_url = serializers.CharField() + initial_state = InitialIndexStateSerializer() + initial_user_clipboard = serializers.DictField() + language_code = serializers.CharField() + lms_link = serializers.CharField() + mfe_proctored_exam_settings_url = serializers.CharField() + notification_dismiss_url = serializers.CharField() + proctoring_errors = ProctoringErrorListSerializer(many=True) + reindex_link = serializers.CharField() + rerun_notification_id = serializers.IntegerField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py index df5b77f72f..8398bd8ad6 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py @@ -4,6 +4,7 @@ API Serializers for proctoring from rest_framework import serializers +from cms.djangoapps.contentstore.rest_api.serializers.common import ProctoringErrorListSerializer from xmodule.course_block import get_available_providers @@ -31,26 +32,6 @@ class ProctoredExamConfigurationSerializer(serializers.Serializer): course_start_date = serializers.DateTimeField() -class ProctoringErrorModelSerializer(serializers.Serializer): - """ - Serializer for proctoring error model item. - """ - deprecated = serializers.BooleanField() - display_name = serializers.CharField() - help = serializers.CharField() - hide_on_enabled_publisher = serializers.BooleanField() - value = serializers.CharField() - - -class ProctoringErrorListSerializer(serializers.Serializer): - """ - Serializer for proctoring error list. - """ - key = serializers.CharField() - message = serializers.CharField() - model = ProctoringErrorModelSerializer() - - class ProctoringErrorsSerializer(serializers.Serializer): """ Serializer for proctoring errors with url to proctored exam settings. diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 168fa9bcab..217957dfc0 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -7,6 +7,7 @@ from openedx.core.constants import COURSE_ID_PATTERN from .views import ( CourseDetailsView, CourseTeamView, + CourseIndexView, CourseGradingView, CourseRerunView, CourseSettingsView, @@ -59,6 +60,11 @@ urlpatterns = [ CourseSettingsView.as_view(), name="course_settings" ), + re_path( + fr'^course_index/{COURSE_ID_PATTERN}$', + CourseIndexView.as_view(), + name="course_index" + ), re_path( fr'^course_details/{COURSE_ID_PATTERN}$', CourseDetailsView.as_view(), diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 81666919a4..0800568ba2 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -2,6 +2,7 @@ Views for v1 contentstore API. """ from .course_details import CourseDetailsView +from .course_index import CourseIndexView from .course_team import CourseTeamView from .course_rerun import CourseRerunView from .grading import CourseGradingView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py new file mode 100644 index 0000000000..1ffac5ba69 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py @@ -0,0 +1,98 @@ +"""API Views for course index""" + +import edx_api_doc_tools as apidocs +from django.conf import settings +from opaque_keys.edx.keys import CourseKey +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseIndexSerializer +from cms.djangoapps.contentstore.utils import get_course_index_context +from common.djangoapps.student.auth import has_studio_read_access +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes + + +@view_auth_classes(is_authenticated=True) +class CourseIndexView(DeveloperErrorViewMixin, APIView): + """View for Course Index""" + + @apidocs.schema( + parameters=[ + apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"), + apidocs.string_parameter( + "show", + apidocs.ParameterLocation.QUERY, + description="Query param to set initial state which fully expanded to see the item", + )], + responses={ + 200: CourseIndexSerializer, + 401: "The requester is not authenticated.", + 403: "The requester cannot access the specified course.", + 404: "The requested course does not exist.", + }, + ) + @verify_course_exists() + def get(self, request: Request, course_id: str): + """ + Get an object containing course index for outline. + + **Example Request** + + GET /api/contentstore/v1/course_index/{course_id}?show=block-v1:edx+101+y+type@course+block@course + + **Response Values** + + If the request is successful, an HTTP 200 "OK" response is returned. + + The HTTP 200 response contains a single dict that contains keys that + are the course's outline. + + **Example Response** + + ```json + { + "course_release_date": "Set Date", + "course_structure": {}, + "deprecated_blocks_info": { + "deprecated_enabled_block_types": [], + "blocks": [], + "advance_settings_url": "/settings/advanced/course-v1:edx+101+y76" + }, + "discussions_incontext_feedback_url": "", + "discussions_incontext_learnmore_url": "", + "initial_state": { + "expanded_locators": [ + "block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6", + "block-v1:edx+101+y76+type@sequential+block@8a85e287e30a47e98d8c1f37f74a6a9d" + ], + "locator_to_show": "block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6" + }, + "initial_user_clipboard": { + "content": null, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "language_code": "en", + "lms_link": "//localhost:18000/courses/course-v1:edx+101+y76/jump_to/block-v1:edx+101+y76", + "mfe_proctored_exam_settings_url": "", + "notification_dismiss_url": "/course_notifications/course-v1:edx+101+y76/2", + "proctoring_errors": [], + "reindex_link": "/course/course-v1:edx+101+y76/search_reindex", + "rerun_notification_id": 2 + } + ``` + """ + + course_key = CourseKey.from_string(course_id) + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + course_index_context = get_course_index_context(request, course_key) + course_index_context.update({ + "discussions_incontext_learnmore_url": settings.DISCUSSIONS_INCONTEXT_LEARNMORE_URL, + "discussions_incontext_feedback_url": settings.DISCUSSIONS_INCONTEXT_FEEDBACK_URL, + }) + + serializer = CourseIndexSerializer(course_index_context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py new file mode 100644 index 0000000000..49b22a5d63 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py @@ -0,0 +1,125 @@ +""" +Unit tests for course index outline. +""" +from django.test import RequestFactory +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.rest_api.v1.mixins import PermissionAccessMixin +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.utils import get_lms_link_for_item +from cms.djangoapps.contentstore.views.course import _course_outline_json +from common.djangoapps.student.tests.factories import UserFactory +from xmodule.modulestore.tests.factories import BlockFactory + + +class CourseIndexViewTest(CourseTestCase, PermissionAccessMixin): + """ + Tests for CourseIndexView. + """ + + def setUp(self): + super().setUp() + with self.store.bulk_operations(self.course.id, emit_signals=False): + self.chapter = BlockFactory.create( + parent=self.course, display_name='Overview' + ) + self.section = BlockFactory.create( + parent=self.chapter, display_name='Welcome' + ) + self.unit = BlockFactory.create( + parent=self.section, display_name='New Unit' + ) + self.xblock = BlockFactory.create( + parent=self.unit, + category='problem', + display_name='Some problem' + ) + self.user = UserFactory() + self.factory = RequestFactory() + self.request = self.factory.get(f"/course/{self.course.id}") + self.request.user = self.user + self.reload_course() + self.url = reverse( + "cms.djangoapps.contentstore:v1:course_index", + kwargs={"course_id": self.course.id}, + ) + + def test_course_index_response(self): + """Check successful response content""" + response = self.client.get(self.url) + expected_response = { + "course_release_date": "Set Date", + "course_structure": _course_outline_json(self.request, self.course), + "deprecated_blocks_info": { + "deprecated_enabled_block_types": [], + "blocks": [], + "advance_settings_url": f"/settings/advanced/{self.course.id}" + }, + "discussions_incontext_feedback_url": "", + "discussions_incontext_learnmore_url": "", + "initial_state": None, + "initial_user_clipboard": { + "content": None, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "language_code": "en", + "lms_link": get_lms_link_for_item(self.course.location), + "mfe_proctored_exam_settings_url": "", + "notification_dismiss_url": None, + "proctoring_errors": [], + "reindex_link": f"/course/{self.course.id}/search_reindex", + "rerun_notification_id": None + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + + def test_course_index_response_with_show_locators(self): + """Check successful response content with show query param""" + response = self.client.get(self.url, {"show": str(self.unit.location)}) + expected_response = { + "course_release_date": "Set Date", + "course_structure": _course_outline_json(self.request, self.course), + "deprecated_blocks_info": { + "deprecated_enabled_block_types": [], + "blocks": [], + "advance_settings_url": f"/settings/advanced/{self.course.id}" + }, + "discussions_incontext_feedback_url": "", + "discussions_incontext_learnmore_url": "", + "initial_state": { + "expanded_locators": [ + str(self.unit.location), + str(self.xblock.location), + ], + "locator_to_show": str(self.unit.location), + }, + "initial_user_clipboard": { + "content": None, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "language_code": "en", + "lms_link": get_lms_link_for_item(self.course.location), + "mfe_proctored_exam_settings_url": "", + "notification_dismiss_url": None, + "proctoring_errors": [], + "reindex_link": f"/course/{self.course.id}/search_reindex", + "rerun_notification_id": None + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + + def test_course_index_response_with_invalid_course(self): + """Check error response for invalid course id""" + response = self.client.get(self.url + "1") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.data, { + "developer_message": f"Unknown course {self.course.id}1", + "error_code": "course_does_not_exist" + }) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 62951e4cd6..d1acd50f71 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -25,7 +25,8 @@ from pytz import UTC from xblock.fields import Scope from cms.djangoapps.contentstore.toggles import exam_setting_view_enabled -from common.djangoapps.course_action_state.models import CourseRerunUIStateManager +from common.djangoapps.course_action_state.models import CourseRerunUIStateManager, CourseRerunState +from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.edxmako.services import MakoService from common.djangoapps.student import auth @@ -45,6 +46,8 @@ from common.djangoapps.util.milestones_helpers import ( get_namespace_choices, generate_milestone_namespace ) +from common.djangoapps.util.date_utils import get_default_time_display +from common.djangoapps.xblock_django.api import deprecated_xblocks from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from openedx.core import toggles as core_toggles from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course @@ -80,7 +83,9 @@ from cms.djangoapps.contentstore.toggles import ( # use_xpert_translations_component, ) from cms.djangoapps.models.settings.course_grading import CourseGradingModel +from cms.djangoapps.models.settings.course_metadata import CourseMetadata from xmodule.library_tools import LibraryToolsService +from xmodule.course_block import DEFAULT_START_DATE # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order @@ -1672,6 +1677,89 @@ def get_course_videos_context(course_block, pagination_conf, course_key=None): return course_video_context +def get_course_index_context(request, course_key, course_block=None): + """ + Utils is used to get context of course index outline. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.course import ( + course_outline_initial_state, + _course_outline_json, + _deprecated_blocks_info, + ) + from openedx.core.djangoapps.content_staging import api as content_staging_api + + if not course_block: + with modulestore().bulk_operations(course_key): + course_block = modulestore().get_course(course_key) + + lms_link = get_lms_link_for_item(course_block.location) + reindex_link = None + if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False): + if GlobalStaff().has_user(request.user): + reindex_link = f"/course/{str(course_key)}/search_reindex" + sections = course_block.get_children() + course_structure = _course_outline_json(request, course_block) + locator_to_show = request.GET.get('show', None) + + course_release_date = ( + get_default_time_display(course_block.start) + if course_block.start != DEFAULT_START_DATE + else _("Set Date") + ) + + settings_url = reverse_course_url('settings_handler', course_key) + + try: + current_action = CourseRerunState.objects.find_first(course_key=course_key, should_display=True) + except (ItemNotFoundError, CourseActionStateItemNotFoundError): + current_action = None + + deprecated_block_names = [block.name for block in deprecated_xblocks()] + deprecated_blocks_info = _deprecated_blocks_info(course_block, deprecated_block_names) + + frontend_app_publisher_url = configuration_helpers.get_value_for_org( + course_block.location.org, + 'FRONTEND_APP_PUBLISHER_URL', + settings.FEATURES.get('FRONTEND_APP_PUBLISHER_URL', False) + ) + # gather any errors in the currently stored proctoring settings. + advanced_dict = CourseMetadata.fetch(course_block) + proctoring_errors = CourseMetadata.validate_proctoring_settings(course_block, advanced_dict, request.user) + + user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) + + course_index_context = { + 'language_code': request.LANGUAGE_CODE, + 'context_course': course_block, + 'lms_link': lms_link, + 'sections': sections, + 'course_structure': course_structure, + 'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None, # lint-amnesty, pylint: disable=line-too-long + 'initial_user_clipboard': user_clipboard, + 'rerun_notification_id': current_action.id if current_action else None, + 'course_release_date': course_release_date, + 'settings_url': settings_url, + 'reindex_link': reindex_link, + 'deprecated_blocks_info': deprecated_blocks_info, + 'notification_dismiss_url': reverse_course_url( + 'course_notifications_handler', + current_action.course_key, + kwargs={ + 'action_state_id': current_action.id, + }, + ) if current_action else None, + 'frontend_app_publisher_url': frontend_app_publisher_url, + 'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_block.id), + 'advance_settings_url': reverse_course_url('advanced_settings_handler', course_block.id), + 'proctoring_errors': proctoring_errors, + 'taxonomy_tags_widget_url': get_taxonomy_tags_widget_url(course_block.id), + } + + return course_index_context + + class StudioPermissionsService: """ Service that can provide information about a user's permissions. diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index b42c011c4b..6d832b68ae 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -54,12 +54,9 @@ from common.djangoapps.student.roles import ( UserBasedRole, OrgStaffRole ) -from common.djangoapps.util.date_utils import get_default_time_display from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json from common.djangoapps.util.string_utils import _has_non_ascii_characters -from common.djangoapps.xblock_django.api import deprecated_xblocks from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.content_staging import api as content_staging_api from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers @@ -69,11 +66,11 @@ from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME from organizations.models import Organization from xmodule.contentstore.content import StaticContent # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.course_block import CourseBlock, DEFAULT_START_DATE, CourseFields # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.course_block import CourseBlock, CourseFields # lint-amnesty, pylint: disable=wrong-import-order from xmodule.error_block import ErrorBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import EdxJSONEncoder # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.exceptions import DuplicateCourseError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.partitions.partitions import UserPartition # lint-amnesty, pylint: disable=wrong-import-order from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException # lint-amnesty, pylint: disable=wrong-import-order @@ -102,10 +99,10 @@ from ..utils import ( get_course_grading, get_home_context, get_library_context, + get_course_index_context, get_lms_link_for_item, get_proctored_exam_settings_url, get_course_outline_url, - get_taxonomy_tags_widget_url, get_studio_home_url, get_updates_url, get_advanced_settings_url, @@ -625,72 +622,13 @@ def course_index(request, course_key): # A unit may not have a draft version, but one of its components could, and hence the unit itself has changes. with modulestore().bulk_operations(course_key): course_block = get_course_and_check_access(course_key, request.user, depth=None) - if not course_block: - raise Http404 - if use_new_course_outline_page(course_key): - return redirect(get_course_outline_url(course_key)) - lms_link = get_lms_link_for_item(course_block.location) - reindex_link = None - if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False): - if GlobalStaff().has_user(request.user): - reindex_link = f"/course/{str(course_key)}/search_reindex" - sections = course_block.get_children() - course_structure = _course_outline_json(request, course_block) - locator_to_show = request.GET.get('show', None) + if not course_block: + raise Http404 + if use_new_course_outline_page(course_key): + return redirect(get_course_outline_url(course_key)) - course_release_date = ( - get_default_time_display(course_block.start) - if course_block.start != DEFAULT_START_DATE - else _("Set Date") - ) - - settings_url = reverse_course_url('settings_handler', course_key) - - try: - current_action = CourseRerunState.objects.find_first(course_key=course_key, should_display=True) - except (ItemNotFoundError, CourseActionStateItemNotFoundError): - current_action = None - - deprecated_block_names = [block.name for block in deprecated_xblocks()] - deprecated_blocks_info = _deprecated_blocks_info(course_block, deprecated_block_names) - - frontend_app_publisher_url = configuration_helpers.get_value_for_org( - course_block.location.org, - 'FRONTEND_APP_PUBLISHER_URL', - settings.FEATURES.get('FRONTEND_APP_PUBLISHER_URL', False) - ) - # gather any errors in the currently stored proctoring settings. - advanced_dict = CourseMetadata.fetch(course_block) - proctoring_errors = CourseMetadata.validate_proctoring_settings(course_block, advanced_dict, request.user) - - user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) - - return render_to_response('course_outline.html', { - 'language_code': request.LANGUAGE_CODE, - 'context_course': course_block, - 'lms_link': lms_link, - 'sections': sections, - 'course_structure': course_structure, - 'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None, # lint-amnesty, pylint: disable=line-too-long - 'initial_user_clipboard': user_clipboard, - 'rerun_notification_id': current_action.id if current_action else None, - 'course_release_date': course_release_date, - 'settings_url': settings_url, - 'reindex_link': reindex_link, - 'deprecated_blocks_info': deprecated_blocks_info, - 'notification_dismiss_url': reverse_course_url( - 'course_notifications_handler', - current_action.course_key, - kwargs={ - 'action_state_id': current_action.id, - }, - ) if current_action else None, - 'frontend_app_publisher_url': frontend_app_publisher_url, - 'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_block.id), - 'advance_settings_url': reverse_course_url('advanced_settings_handler', course_block.id), - 'proctoring_errors': proctoring_errors, - 'taxonomy_tags_widget_url': get_taxonomy_tags_widget_url(course_block.id), - }) + course_index_context = get_course_index_context(request, course_key, course_block) + return render_to_response('course_outline.html', course_index_context) @function_trace('get_courses_accessible_to_user')