diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index f47c9911b2..ee3e520b59 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -5,7 +5,7 @@ from .course_details import CourseDetailsSerializer from .course_rerun import CourseRerunSerializer from .course_team import CourseTeamSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer -from .home import CourseHomeSerializer +from .home import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index 12816a8cbd..80296b9a76 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -31,6 +31,16 @@ class LibraryViewSerializer(serializers.Serializer): can_edit = serializers.BooleanField() +class CourseTabSerializer(serializers.Serializer): + archived_courses = CourseCommonSerializer(required=False, many=True) + courses = CourseCommonSerializer(required=False, many=True) + in_process_course_actions = UnsucceededCourseSerializer(many=True, required=False, allow_null=True) + + +class LibraryTabSerializer(serializers.Serializer): + libraries = LibraryViewSerializer(many=True, required=False, allow_null=True) + + class CourseHomeSerializer(serializers.Serializer): """Serializer for course home""" allow_course_reruns = serializers.BooleanField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 168fa9bcab..625c6ad168 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -12,6 +12,8 @@ from .views import ( CourseSettingsView, CourseVideosView, HomePageView, + HomePageCoursesView, + HomePageLibrariesView, ProctoredExamSettingsView, ProctoringErrorsView, HelpUrlsView, @@ -29,6 +31,14 @@ urlpatterns = [ HomePageView.as_view(), name="home" ), + path( + 'home/courses', + HomePageCoursesView.as_view(), + name="courses"), + path( + 'home/libraries', + HomePageLibrariesView.as_view(), + name="libraries"), re_path( fr'^videos/{COURSE_ID_PATTERN}$', CourseVideosView.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..8cacb92c47 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -6,7 +6,7 @@ from .course_team import CourseTeamView from .course_rerun import CourseRerunView from .grading import CourseGradingView from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView -from .home import HomePageView +from .home import HomePageView, HomePageCoursesView, HomePageLibrariesView from .settings import CourseSettingsView from .videos import ( CourseVideosView, diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index ea0724e8e2..f8ee907d2e 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -7,8 +7,8 @@ from rest_framework.response import Response from rest_framework.views import APIView from openedx.core.lib.api.view_utils import view_auth_classes -from ....utils import get_home_context -from ..serializers import CourseHomeSerializer +from ....utils import get_home_context, get_course_context, get_library_context +from ..serializers import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer @view_auth_classes(is_authenticated=True) @@ -51,43 +51,12 @@ class HomePageView(APIView): "allow_to_create_new_org": true, "allow_unicode_course_id": false, "allowed_organizations": [], - "archived_courses": [ - { - "course_key": "course-v1:edX+P315+2T2023", - "display_name": "Quantum Entanglement", - "lms_link": "//localhost:18000/courses/course-v1:edX+P315+2T2023", - "number": "P315", - "org": "edX", - "rerun_link": "/course_rerun/course-v1:edX+P315+2T2023", - "run": "2T2023" - "url": "/course/course-v1:edX+P315+2T2023" - }, - ], + "archived_courses": [], "can_create_organizations": true, "course_creator_status": "granted", - "courses": [ - { - "course_key": "course-v1:edX+E2E-101+course", - "display_name": "E2E Test Course", - "lms_link": "//localhost:18000/courses/course-v1:edX+E2E-101+course", - "number": "E2E-101", - "org": "edX", - "rerun_link": "/course_rerun/course-v1:edX+E2E-101+course", - "run": "course", - "url": "/course/course-v1:edX+E2E-101+course" - }, - ], + "courses": [], "in_process_course_actions": [], - "libraries": [ - { - "display_name": "My First Library", - "library_key": "library-v1:new+CPSPR", - "url": "/library/library-v1:new+CPSPR", - "org": "new", - "number": "CPSPR", - "can_edit": true - } - ], + "libraries": [], "libraries_enabled": true, "library_authoring_mfe_url": "//localhost:3001/course/course-v1:edX+P315+2T2023", "optimization_enabled": true, @@ -106,7 +75,7 @@ class HomePageView(APIView): ``` """ - home_context = get_home_context(request) + home_context = get_home_context(request, True) home_context.update({ 'allow_to_create_new_org': settings.FEATURES.get('ENABLE_CREATOR_GROUP', True) and request.user.is_staff, 'studio_name': settings.STUDIO_NAME, @@ -118,3 +87,132 @@ class HomePageView(APIView): }) serializer = CourseHomeSerializer(home_context) return Response(serializer.data) + + +@view_auth_classes(is_authenticated=True) +class HomePageCoursesView(APIView): + """ + View for getting all courses and libraries available to the logged in user. + """ + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "org", + apidocs.ParameterLocation.QUERY, + description="Query param to filter by course org", + )], + responses={ + 200: CourseTabSerializer, + 401: "The requester is not authenticated.", + }, + ) + def get(self, request: Request): + """ + Get an object containing all courses. + + **Example Request** + + GET /api/contentstore/v1/home/courses + + **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 home. + + **Example Response** + + ```json + { + "archived_courses": [ + { + "course_key": "course-v1:edX+P315+2T2023", + "display_name": "Quantum Entanglement", + "lms_link": "//localhost:18000/courses/course-v1:edX+P315+2T2023", + "number": "P315", + "org": "edX", + "rerun_link": "/course_rerun/course-v1:edX+P315+2T2023", + "run": "2T2023" + "url": "/course/course-v1:edX+P315+2T2023" + }, + ], + "courses": [ + { + "course_key": "course-v1:edX+E2E-101+course", + "display_name": "E2E Test Course", + "lms_link": "//localhost:18000/courses/course-v1:edX+E2E-101+course", + "number": "E2E-101", + "org": "edX", + "rerun_link": "/course_rerun/course-v1:edX+E2E-101+course", + "run": "course", + "url": "/course/course-v1:edX+E2E-101+course" + }, + ], + "in_process_course_actions": [], + } + ``` + """ + + active_courses, archived_courses, in_process_course_actions = get_course_context(request) + courses_context = { + "courses": active_courses, + "archived_courses": archived_courses, + "in_process_course_actions": in_process_course_actions, + } + serializer = CourseTabSerializer(courses_context) + return Response(serializer.data) + + +@view_auth_classes(is_authenticated=True) +class HomePageLibrariesView(APIView): + """ + View for getting all courses and libraries available to the logged in user. + """ + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "org", + apidocs.ParameterLocation.QUERY, + description="Query param to filter by course org", + )], + responses={ + 200: LibraryTabSerializer, + 401: "The requester is not authenticated.", + }, + ) + def get(self, request: Request): + """ + Get an object containing all libraries on home page. + + **Example Request** + + GET /api/contentstore/v1/home/libraries + + **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 home. + + **Example Response** + + ```json + { + "libraries": [ + { + "display_name": "My First Library", + "library_key": "library-v1:new+CPSPR", + "url": "/library/library-v1:new+CPSPR", + "org": "new", + "number": "CPSPR", + "can_edit": true + } + ], } + ``` + """ + + library_context = get_library_context(request) + serializer = LibraryTabSerializer(library_context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 0d5a20b341..5279af0b12 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -11,6 +11,7 @@ from edx_toggles.toggles.testutils import ( from rest_framework import status from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase from cms.djangoapps.contentstore.views.course import ENABLE_GLOBAL_STAFF_OPTIMIZATION from cms.djangoapps.contentstore.toggles import ENABLE_TAGGING_TAXONOMY_LIST_PAGE from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory @@ -20,17 +21,16 @@ from xmodule.modulestore.tests.factories import CourseFactory @ddt.ddt class HomePageViewTest(CourseTestCase): """ - Tests for HomePageView. + Tests for HomePageCoursesView. """ def setUp(self): super().setUp() self.url = reverse("cms.djangoapps.contentstore:v1:home") - def test_home_page_response(self): + def test_home_page_courses_response(self): """Check successful response content""" response = self.client.get(self.url) - course_id = str(self.course.id) expected_response = { "allow_course_reruns": True, @@ -40,16 +40,7 @@ class HomePageViewTest(CourseTestCase): "archived_courses": [], "can_create_organizations": True, "course_creator_status": "granted", - "courses": [{ - "course_key": course_id, - "display_name": self.course.display_name, - "lms_link": f'//{settings.LMS_BASE}/courses/{course_id}/jump_to/{self.course.location}', - "number": self.course.number, - "org": self.course.org, - "rerun_link": f'/course_rerun/{course_id}', - "run": self.course.id.run, - "url": f'/course/{course_id}', - }], + "courses": [], "in_process_course_actions": [], "libraries": [], "libraries_enabled": True, @@ -73,6 +64,50 @@ class HomePageViewTest(CourseTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.data) + @override_waffle_flag(ENABLE_TAGGING_TAXONOMY_LIST_PAGE, True) + def test_taxonomy_list_link(self): + response = self.client.get(self.url) + self.assertTrue(response.data['taxonomies_enabled']) + self.assertEqual( + response.data['taxonomy_list_mfe_url'], + f'{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/taxonomies' + ) + + +@ddt.ddt +class HomePageCoursesViewTest(CourseTestCase): + """ + Tests for HomePageView. + """ + + def setUp(self): + super().setUp() + self.url = reverse("cms.djangoapps.contentstore:v1:courses") + + def test_home_page_response(self): + """Check successful response content""" + response = self.client.get(self.url) + course_id = str(self.course.id) + + expected_response = { + "archived_courses": [], + "courses": [{ + "course_key": course_id, + "display_name": self.course.display_name, + "lms_link": f'//{settings.LMS_BASE}/courses/{course_id}/jump_to/{self.course.location}', + "number": self.course.number, + "org": self.course.org, + "rerun_link": f'/course_rerun/{course_id}', + "run": self.course.id.run, + "url": f'/course/{course_id}', + }], + "in_process_course_actions": [], + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + print(response.data) + self.assertDictEqual(expected_response, response.data) + @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) def test_org_query_if_passed(self): """Test home page when org filter passed as a query param""" @@ -94,11 +129,32 @@ class HomePageViewTest(CourseTestCase): self.assertEqual(len(response.data['courses']), 0) self.assertEqual(response.status_code, status.HTTP_200_OK) - @override_waffle_flag(ENABLE_TAGGING_TAXONOMY_LIST_PAGE, True) - def test_taxonomy_list_link(self): + +@ddt.ddt +class HomePageLibrariesViewTest(LibraryTestCase): + """ + Tests for HomePageLibrariesView. + """ + + def setUp(self): + super().setUp() + self.url = reverse("cms.djangoapps.contentstore:v1:libraries") + + def test_home_page_libraries_response(self): + """Check successful response content""" response = self.client.get(self.url) - self.assertTrue(response.data['taxonomies_enabled']) - self.assertEqual( - response.data['taxonomy_list_mfe_url'], - f'{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/taxonomies' - ) + + expected_response = { + "libraries": [{ + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib', + 'url': '/library/library-v1:org+lib', + 'org': 'org', + 'number': 'lib', + 'can_edit': True + }], + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + print(response.data) + self.assertDictEqual(expected_response, response.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 62951e4cd6..76dae28a6e 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1472,44 +1472,17 @@ def get_library_context(request, request_is_json=False): return data -def get_home_context(request): +def get_course_context(request): """ - Utils is used to get context of course home. + Utils is used to get context of course home library tab. It is used for both DRF and django views. """ from cms.djangoapps.contentstore.views.course import ( - get_allowed_organizations, - get_allowed_organizations_for_libraries, get_courses_accessible_to_user, - user_can_create_organizations, - _accessible_libraries_iter, - _get_course_creator_status, - _format_library_for_view, _process_courses_list, ENABLE_GLOBAL_STAFF_OPTIMIZATION, ) - from cms.djangoapps.contentstore.views.library import ( - LIBRARY_AUTHORING_MICROFRONTEND_URL, - LIBRARIES_ENABLED, - should_redirect_to_library_authoring_mfe, - user_can_create_library, - ) - - optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() - - org = request.GET.get('org', '') if optimization_enabled else None - courses_iter, in_process_course_actions = get_courses_accessible_to_user(request, org) - user = request.user - libraries = [] - response_format = get_response_format(request) - - if not split_library_view_on_dashboard() and LIBRARIES_ENABLED: - accessible_libraries = _accessible_libraries_iter(user) - libraries = [_format_library_for_view(lib, request) for lib in accessible_libraries] - - if split_library_view_on_dashboard() and request_response_format_is_json(request, response_format): - libraries = get_library_context(request, True)['libraries'] def format_in_process_course_view(uca): """ @@ -1532,9 +1505,52 @@ def get_home_context(request): ) if uca.state == CourseRerunUIStateManager.State.FAILED else '' } + optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() + + org = request.GET.get('org', '') if optimization_enabled else None + courses_iter, in_process_course_actions = get_courses_accessible_to_user(request, org) split_archived = settings.FEATURES.get('ENABLE_SEPARATE_ARCHIVED_COURSES', False) active_courses, archived_courses = _process_courses_list(courses_iter, in_process_course_actions, split_archived) in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions] + return active_courses, archived_courses, in_process_course_actions + + +def get_home_context(request, no_course=False): + """ + Utils is used to get context of course home. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.course import ( + get_allowed_organizations, + get_allowed_organizations_for_libraries, + user_can_create_organizations, + _accessible_libraries_iter, + _get_course_creator_status, + _format_library_for_view, + ENABLE_GLOBAL_STAFF_OPTIMIZATION, + ) + from cms.djangoapps.contentstore.views.library import ( + LIBRARY_AUTHORING_MICROFRONTEND_URL, + LIBRARIES_ENABLED, + should_redirect_to_library_authoring_mfe, + user_can_create_library, + ) + + active_courses = [] + archived_courses = [] + in_process_course_actions = [] + + optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() + + user = request.user + libraries = [] + + if not no_course: + active_courses, archived_courses, in_process_course_actions = get_course_context(request) + + if not split_library_view_on_dashboard() and LIBRARIES_ENABLED and not no_course: + libraries = get_library_context(request, True)['libraries'] home_context = { 'courses': active_courses,