From e094571b592f4cf95d79a16b077dec7744b46e44 Mon Sep 17 00:00:00 2001 From: ruzniaievdm Date: Fri, 8 Sep 2023 15:39:31 +0300 Subject: [PATCH 1/3] feat: create course home api DRF (#33173) (#33204) --- .../rest_api/v1/serializers/__init__.py | 4 +- .../rest_api/v1/serializers/common.py | 19 +++ .../rest_api/v1/serializers/course_rerun.py | 15 +++ .../rest_api/v1/serializers/home.py | 62 +++++++++ .../rest_api/v1/serializers/settings.py | 18 +-- .../rest_api/v1/tests/test_course_rerun.py | 36 ++++++ .../rest_api/v1/tests/test_home.py | 89 +++++++++++++ .../contentstore/rest_api/v1/urls.py | 14 +- .../rest_api/v1/views/__init__.py | 2 + .../rest_api/v1/views/course_rerun.py | 76 +++++++++++ .../contentstore/rest_api/v1/views/home.py | 120 ++++++++++++++++++ cms/djangoapps/contentstore/utils.py | 109 +++++++++++++++- cms/djangoapps/contentstore/views/course.py | 76 +---------- 13 files changed, 552 insertions(+), 88 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/v1/serializers/common.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/serializers/course_rerun.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/serializers/home.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/tests/test_course_rerun.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/course_rerun.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/home.py diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 39d9d9127a85..53d4f551ff74 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -1,10 +1,12 @@ """ Serializers for v1 contentstore API. """ -from .settings import CourseSettingsSerializer +from .home import CourseHomeSerializer from .course_details import CourseDetailsSerializer +from .course_rerun import CourseRerunSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, ProctoredExamSettingsSerializer, ) +from .settings import CourseSettingsSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/common.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/common.py new file mode 100644 index 000000000000..bc2f8d2da6a2 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/common.py @@ -0,0 +1,19 @@ +""" +Common API Serializers +""" + +from rest_framework import serializers + +from openedx.core.lib.api.serializers import CourseKeyField + + +class CourseCommonSerializer(serializers.Serializer): + """Serializer for course renders""" + course_key = CourseKeyField() + display_name = serializers.CharField() + lms_link = serializers.CharField() + number = serializers.CharField() + org = serializers.CharField() + rerun_link = serializers.CharField() + run = serializers.CharField() + url = serializers.CharField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_rerun.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_rerun.py new file mode 100644 index 000000000000..317468a87a6b --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_rerun.py @@ -0,0 +1,15 @@ +""" +API Serializers for course rerun +""" + +from rest_framework import serializers + + +class CourseRerunSerializer(serializers.Serializer): + """ Serializer for course rerun """ + allow_unicode_course_id = serializers.BooleanField() + course_creator_status = serializers.CharField() + display_name = serializers.CharField() + number = serializers.CharField() + org = serializers.CharField() + run = serializers.CharField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py new file mode 100644 index 000000000000..5abcda673aa7 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -0,0 +1,62 @@ +""" +API Serializers for course home +""" + +from rest_framework import serializers + +from openedx.core.lib.api.serializers import CourseKeyField + +from .common import CourseCommonSerializer + + +class UnsucceededCourseSerializer(serializers.Serializer): + """Serializer for unsucceeded course""" + display_name = serializers.CharField() + course_key = CourseKeyField() + org = serializers.CharField() + number = serializers.CharField() + run = serializers.CharField() + is_failed = serializers.BooleanField() + is_in_progress = serializers.BooleanField() + dismiss_link = serializers.CharField() + + +class LibraryViewSerializer(serializers.Serializer): + """Serializer for library view""" + display_name = serializers.CharField() + library_key = serializers.CharField() + url = serializers.CharField() + org = serializers.CharField() + number = serializers.CharField() + can_edit = serializers.BooleanField() + + +class CourseHomeSerializer(serializers.Serializer): + """Serializer for course home""" + allow_course_reruns = serializers.BooleanField() + allow_to_create_new_org = serializers.BooleanField() + allow_unicode_course_id = serializers.BooleanField() + allowed_organizations = serializers.ListSerializer( + child=serializers.CharField(), + allow_empty=True + ) + archived_courses = CourseCommonSerializer(required=False, many=True) + can_create_organizations = serializers.BooleanField() + course_creator_status = serializers.CharField() + courses = CourseCommonSerializer(required=False, many=True) + in_process_course_actions = UnsucceededCourseSerializer(many=True, required=False, allow_null=True) + libraries = LibraryViewSerializer(many=True, required=False, allow_null=True) + libraries_enabled = serializers.BooleanField() + library_authoring_mfe_url = serializers.CharField() + optimization_enabled = serializers.BooleanField() + redirect_to_library_authoring_mfe = serializers.BooleanField() + request_course_creator_url = serializers.CharField() + rerun_creator_status = serializers.BooleanField() + show_new_library_button = serializers.BooleanField() + split_studio_home = serializers.BooleanField() + studio_name = serializers.CharField() + studio_short_name = serializers.CharField() + studio_request_email = serializers.CharField() + tech_support_email = serializers.CharField() + platform_name = serializers.CharField() + user_is_active = serializers.BooleanField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py index 0b65389596ae..7ecdc70a59e7 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py @@ -4,23 +4,11 @@ from rest_framework import serializers -from openedx.core.lib.api.serializers import CourseKeyField - - -class PossiblePreRequisiteCourseSerializer(serializers.Serializer): - """ Serializer for possible pre requisite course """ - course_key = CourseKeyField() - display_name = serializers.CharField() - lms_link = serializers.CharField() - number = serializers.CharField() - org = serializers.CharField() - rerun_link = serializers.CharField() - run = serializers.CharField() - url = serializers.CharField() +from .common import CourseCommonSerializer class CourseSettingsSerializer(serializers.Serializer): - """ Serializer for course settings """ + """Serializer for course settings""" about_page_editable = serializers.BooleanField() can_show_certificate_available_date_field = serializers.BooleanField() course_display_name = serializers.CharField() @@ -36,7 +24,7 @@ class CourseSettingsSerializer(serializers.Serializer): lms_link_for_about_page = serializers.URLField() marketing_enabled = serializers.BooleanField() mfe_proctored_exam_settings_url = serializers.CharField(required=False, allow_null=True, allow_blank=True) - possible_pre_requisite_courses = PossiblePreRequisiteCourseSerializer(required=False, many=True) + possible_pre_requisite_courses = CourseCommonSerializer(required=False, many=True) short_description_editable = serializers.BooleanField() show_min_grade_warning = serializers.BooleanField() sidebar_html_enabled = serializers.BooleanField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/tests/test_course_rerun.py b/cms/djangoapps/contentstore/rest_api/v1/tests/test_course_rerun.py new file mode 100644 index 000000000000..e25904ad465f --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/tests/test_course_rerun.py @@ -0,0 +1,36 @@ +""" +Unit tests for course rerun. +""" +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.rest_api.v1.mixins import PermissionAccessMixin + + +class CourseRerunViewTest(CourseTestCase, PermissionAccessMixin): + """ + Tests for CourseRerunView. + """ + + def setUp(self): + super().setUp() + self.url = reverse( + "cms.djangoapps.contentstore:v1:course_rerun", + kwargs={"course_id": self.course.id}, + ) + + def test_course_rerun_response(self): + """Check successful response content""" + response = self.client.get(self.url) + expected_response = { + "allow_unicode_course_id": False, + "course_creator_status": "granted", + "display_name": self.course.display_name, + "number": self.course.id.course, + "org": self.course.id.org, + "run": self.course.id.run, + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py new file mode 100644 index 000000000000..110ee24ba150 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py @@ -0,0 +1,89 @@ +""" +Unit tests for home page view. +""" +import ddt +from django.conf import settings +from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_switch +from rest_framework import status + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.views.course import ENABLE_GLOBAL_STAFF_OPTIMIZATION +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from xmodule.modulestore.tests.factories import CourseFactory + + +@ddt.ddt +class HomePageViewTest(CourseTestCase): + """ + Tests for HomePageView. + """ + + def setUp(self): + super().setUp() + self.url = reverse("cms.djangoapps.contentstore:v1:home") + + def test_home_page_response(self): + """Check successful response content""" + response = self.client.get(self.url) + course_id = str(self.course.id) + + expected_response = { + "allow_course_reruns": True, + "allow_to_create_new_org": False, + "allow_unicode_course_id": False, + "allowed_organizations": [], + "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}', + }], + "in_process_course_actions": [], + "libraries": [], + "libraries_enabled": True, + "library_authoring_mfe_url": settings.LIBRARY_AUTHORING_MICROFRONTEND_URL, + "optimization_enabled": False, + "redirect_to_library_authoring_mfe": False, + "request_course_creator_url": "/request_course_creator", + "rerun_creator_status": True, + "show_new_library_button": True, + "split_studio_home": False, + "studio_name": settings.STUDIO_NAME, + "studio_short_name": settings.STUDIO_SHORT_NAME, + "studio_request_email": "", + "tech_support_email": "technical@example.com", + "platform_name": settings.PLATFORM_NAME, + "user_is_active": True, + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + 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""" + foo_course = self.store.make_course_key('foo-org', 'bar-number', 'baz-run') + test_course = CourseFactory.create( + org=foo_course.org, + number=foo_course.course, + run=foo_course.run + ) + CourseOverviewFactory.create(id=test_course.id, org='foo-org') + response = self.client.get(self.url, {"org": "foo-org"}) + self.assertEqual(len(response.data['courses']), 1) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) + def test_org_query_if_empty(self): + """Test home page with an empty org query param""" + response = self.client.get(self.url) + self.assertEqual(len(response.data['courses']), 0) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 63b2b2d731e5..967b675cbbaf 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -1,18 +1,25 @@ """ Contenstore API v1 URLs. """ -from django.urls import re_path +from django.urls import re_path, path from openedx.core.constants import COURSE_ID_PATTERN from .views import ( CourseDetailsView, + CourseRerunView, CourseSettingsView, + HomePageView, ProctoredExamSettingsView, ) app_name = 'v1' urlpatterns = [ + path( + 'home', + HomePageView.as_view(), + name="home" + ), re_path( fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$', ProctoredExamSettingsView.as_view(), @@ -28,4 +35,9 @@ CourseDetailsView.as_view(), name="course_details" ), + re_path( + fr'^course_rerun/{COURSE_ID_PATTERN}$', + CourseRerunView.as_view(), + name="course_rerun" + ), ] diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index fd653e2038df..994700e6ae28 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -2,5 +2,7 @@ Views for v1 contentstore API. """ from .course_details import CourseDetailsView +from .course_rerun import CourseRerunView +from .home import HomePageView from .settings import CourseSettingsView from .proctoring import ProctoredExamSettingsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_rerun.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_rerun.py new file mode 100644 index 000000000000..fe39858c5380 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_rerun.py @@ -0,0 +1,76 @@ +""" API Views for course rerun """ + +import edx_api_doc_tools as apidocs +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.utils import get_course_rerun_context +from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseRerunSerializer +from common.djangoapps.student.roles import GlobalStaff +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes +from xmodule.modulestore.django import modulestore + + +@view_auth_classes(is_authenticated=True) +class CourseRerunView(DeveloperErrorViewMixin, APIView): + """ + View for course rerun. + """ + + @apidocs.schema( + parameters=[ + apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"), + ], + responses={ + 200: CourseRerunSerializer, + 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 rerun. + + **Example Request** + + GET /api/contentstore/v1/course_rerun/{course_id} + + **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 rerun. + + **Example Response** + + ```json + { + "allow_unicode_course_id": False, + "course_creator_status": "granted", + "number": "101", + "display_name": "new edx course", + "org": "edx", + "run": "2023", + } + ``` + """ + + if not GlobalStaff().has_user(request.user): + self.permission_denied(request) + + course_key = CourseKey.from_string(course_id) + with modulestore().bulk_operations(course_key): + course_block = modulestore().get_course(course_key) + course_rerun_context = get_course_rerun_context(course_key, course_block, request.user) + course_rerun_context.update({ + 'org': course_key.org, + 'number': course_key.course, + 'run': course_key.run, + }) + serializer = CourseRerunSerializer(course_rerun_context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py new file mode 100644 index 000000000000..ea0724e8e2df --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -0,0 +1,120 @@ +""" API Views for course home """ + +import edx_api_doc_tools as apidocs +from django.conf import settings +from rest_framework.request import Request +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 + + +@view_auth_classes(is_authenticated=True) +class HomePageView(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: CourseHomeSerializer, + 401: "The requester is not authenticated.", + }, + ) + def get(self, request: Request): + """ + Get an object containing all courses and libraries on home page. + + **Example Request** + + GET /api/contentstore/v1/home + + **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 + { + "allow_course_reruns": true, + "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" + }, + ], + "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" + }, + ], + "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_enabled": true, + "library_authoring_mfe_url": "//localhost:3001/course/course-v1:edX+P315+2T2023", + "optimization_enabled": true, + "redirect_to_library_authoring_mfe": false, + "request_course_creator_url": "/request_course_creator", + "rerun_creator_status": true, + "show_new_library_button": true, + "split_studio_home": false, + "studio_name": "Studio", + "studio_short_name": "Studio", + "studio_request_email": "", + "tech_support_email": "technical@example.com", + "platform_name": "Your Platform Name Here" + "user_is_active": true, + } + ``` + """ + + home_context = get_home_context(request) + 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, + 'studio_short_name': settings.STUDIO_SHORT_NAME, + 'studio_request_email': settings.FEATURES.get('STUDIO_REQUEST_EMAIL', ''), + 'tech_support_email': settings.TECH_SUPPORT_EMAIL, + 'platform_name': settings.PLATFORM_NAME, + 'user_is_active': request.user.is_active, + }) + serializer = CourseHomeSerializer(home_context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index f111e7ec6dbf..befafcc865f3 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -17,6 +17,7 @@ from pytz import UTC from cms.djangoapps.contentstore.toggles import exam_setting_view_enabled +from common.djangoapps.course_action_state.models import CourseRerunUIStateManager from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student import auth from common.djangoapps.student.models import CourseEnrollment @@ -48,8 +49,7 @@ from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML from cms.djangoapps.contentstore.toggles import ( - use_new_text_editor, - use_new_video_editor, + split_library_view_on_dashboard, use_new_advanced_settings_page, use_new_course_outline_page, use_new_export_page, @@ -58,8 +58,10 @@ use_new_home_page, use_new_import_page, use_new_schedule_details_page, + use_new_text_editor, use_new_unit_page, use_new_updates_page, + use_new_video_editor, use_new_video_uploads_page, ) from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order @@ -1062,3 +1064,106 @@ def get_course_settings(request, course_key, course_block): ) return settings_context + + +def get_home_context(request): + """ + Utils is used to get context of course grading. + 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 = [] + if not split_library_view_on_dashboard() and LIBRARIES_ENABLED: + libraries = _accessible_libraries_iter(request.user) + + def format_in_process_course_view(uca): + """ + Return a dict of the data which the view requires for each unsucceeded course + """ + return { + 'display_name': uca.display_name, + 'course_key': str(uca.course_key), + 'org': uca.course_key.org, + 'number': uca.course_key.course, + 'run': uca.course_key.run, + 'is_failed': uca.state == CourseRerunUIStateManager.State.FAILED, + 'is_in_progress': uca.state == CourseRerunUIStateManager.State.IN_PROGRESS, + 'dismiss_link': reverse_course_url( + 'course_notifications_handler', + uca.course_key, + kwargs={ + 'action_state_id': uca.id, + }, + ) if uca.state == CourseRerunUIStateManager.State.FAILED else '' + } + + 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] + + home_context = { + 'courses': active_courses, + 'split_studio_home': split_library_view_on_dashboard(), + 'archived_courses': archived_courses, + 'in_process_course_actions': in_process_course_actions, + 'libraries_enabled': LIBRARIES_ENABLED, + 'redirect_to_library_authoring_mfe': should_redirect_to_library_authoring_mfe(), + 'library_authoring_mfe_url': LIBRARY_AUTHORING_MICROFRONTEND_URL, + 'libraries': [_format_library_for_view(lib, request) for lib in libraries], + 'show_new_library_button': user_can_create_library(user) and not should_redirect_to_library_authoring_mfe(), + 'user': user, + 'request_course_creator_url': reverse('request_course_creator'), + 'course_creator_status': _get_course_creator_status(user), + 'rerun_creator_status': GlobalStaff().has_user(user), + 'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False), + 'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True), + 'optimization_enabled': optimization_enabled, + 'active_tab': 'courses', + 'allowed_organizations': get_allowed_organizations(user), + 'allowed_organizations_for_libraries': get_allowed_organizations_for_libraries(user), + 'can_create_organizations': user_can_create_organizations(user), + } + + return home_context + + +def get_course_rerun_context(course_key, course_block, user): + """ + Utils is used to get context of course rerun. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.course import _get_course_creator_status + + course_rerun_context = { + 'source_course_key': course_key, + 'display_name': course_block.display_name, + 'user': user, + 'course_creator_status': _get_course_creator_status(user), + 'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False) + } + + return course_rerun_context diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index c289242aab3c..40abb093fdf4 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -91,8 +91,10 @@ from ..utils import ( add_instructor, get_course_settings, + get_home_context, get_lms_link_for_item, get_proctored_exam_settings_url, + get_course_rerun_context, initialize_permissions, remove_all_instructors, reverse_course_url, @@ -104,12 +106,7 @@ from .component import ADVANCED_COMPONENT_TYPES from .helpers import is_content_creator from .block import create_xblock_info -from .library import ( - LIBRARIES_ENABLED, - LIBRARY_AUTHORING_MICROFRONTEND_URL, - user_can_create_library, - should_redirect_to_library_authoring_mfe -) +from .library import LIBRARIES_ENABLED log = logging.getLogger(__name__) User = get_user_model() @@ -317,13 +314,8 @@ def course_rerun_handler(request, course_key_string): with modulestore().bulk_operations(course_key): course_block = get_course_and_check_access(course_key, request.user, depth=3) if request.method == 'GET': - return render_to_response('course-create-rerun.html', { - 'source_course_key': course_key, - 'display_name': course_block.display_name, - 'user': request.user, - 'course_creator_status': _get_course_creator_status(request.user), - 'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False) - }) + course_rerun_context = get_course_rerun_context(course_key, course_block, request.user) + return render_to_response('course-create-rerun.html', course_rerun_context) @login_required @@ -532,62 +524,8 @@ def course_listing(request): List all courses and libraries available to the logged in user """ - 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 = [] - if not split_library_view_on_dashboard() and LIBRARIES_ENABLED: - libraries = _accessible_libraries_iter(request.user) - - def format_in_process_course_view(uca): - """ - Return a dict of the data which the view requires for each unsucceeded course - """ - return { - 'display_name': uca.display_name, - 'course_key': str(uca.course_key), - 'org': uca.course_key.org, - 'number': uca.course_key.course, - 'run': uca.course_key.run, - 'is_failed': uca.state == CourseRerunUIStateManager.State.FAILED, - 'is_in_progress': uca.state == CourseRerunUIStateManager.State.IN_PROGRESS, - 'dismiss_link': reverse_course_url( - 'course_notifications_handler', - uca.course_key, - kwargs={ - 'action_state_id': uca.id, - }, - ) if uca.state == CourseRerunUIStateManager.State.FAILED else '' - } - - 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 render_to_response('index.html', { - 'courses': active_courses, - 'split_studio_home': split_library_view_on_dashboard(), - 'archived_courses': archived_courses, - 'in_process_course_actions': in_process_course_actions, - 'libraries_enabled': LIBRARIES_ENABLED, - 'redirect_to_library_authoring_mfe': should_redirect_to_library_authoring_mfe(), - 'library_authoring_mfe_url': LIBRARY_AUTHORING_MICROFRONTEND_URL, - 'libraries': [_format_library_for_view(lib, request) for lib in libraries], - 'show_new_library_button': user_can_create_library(user) and not should_redirect_to_library_authoring_mfe(), - 'user': user, - 'request_course_creator_url': reverse('request_course_creator'), - 'course_creator_status': _get_course_creator_status(user), - 'rerun_creator_status': GlobalStaff().has_user(user), - 'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False), - 'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True), - 'optimization_enabled': optimization_enabled, - 'active_tab': 'courses', - 'allowed_organizations': get_allowed_organizations(user), - 'allowed_organizations_for_libraries': get_allowed_organizations_for_libraries(user), - 'can_create_organizations': user_can_create_organizations(user), - }) + home_context = get_home_context(request) + return render_to_response('index.html', home_context) @login_required From 8abf6789c1c185c5a593d1681e112825ae023a26 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Wed, 8 May 2024 16:49:47 -0500 Subject: [PATCH 2/3] fix: format OrderedDict --- cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py index 110ee24ba150..9712f2dd0354 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py @@ -2,6 +2,7 @@ Unit tests for home page view. """ import ddt +import json from django.conf import settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_switch @@ -65,7 +66,7 @@ def test_home_page_response(self): } self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual(expected_response, response.data) + self.assertDictEqual(expected_response, json.loads(json.dumps(response.data))) @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) def test_org_query_if_passed(self): From 7d8f3a518e97d2964507331f4bca291573030e05 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Tue, 7 May 2024 11:21:56 -0500 Subject: [PATCH 3/3] fix: remove old mongo functionality --- cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py index 9712f2dd0354..268014ea2126 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py @@ -11,6 +11,7 @@ from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.views.course import ENABLE_GLOBAL_STAFF_OPTIMIZATION from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE from xmodule.modulestore.tests.factories import CourseFactory @@ -19,6 +20,7 @@ class HomePageViewTest(CourseTestCase): """ Tests for HomePageView. """ + MODULESTORE = TEST_DATA_SPLIT_MODULESTORE def setUp(self): super().setUp()