From 781c5e6a4fd88e60474619fde02a8de72f02bf6b Mon Sep 17 00:00:00 2001 From: ruzniaievdm Date: Wed, 6 Sep 2023 17:05:25 +0300 Subject: [PATCH] feat: create course home api DRF (#33173) --- .../rest_api/v1/serializers/__init__.py | 2 + .../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 +-- .../contentstore/rest_api/v1/urls.py | 15 ++- .../rest_api/v1/views/__init__.py | 2 + .../rest_api/v1/views/course_rerun.py | 76 +++++++++++ .../contentstore/rest_api/v1/views/home.py | 120 ++++++++++++++++++ .../v1/views/tests/test_course_rerun.py | 36 ++++++ .../rest_api/v1/views/tests/test_home.py | 89 +++++++++++++ cms/djangoapps/contentstore/utils.py | 108 +++++++++++++++- cms/djangoapps/contentstore/views/course.py | 80 ++---------- 13 files changed, 552 insertions(+), 90 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/views/course_rerun.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/home.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_rerun.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/tests/test_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 2dc803d47bf2..c60d33bc4448 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -1,8 +1,10 @@ """ Serializers for v1 contentstore API. """ +from .home import CourseHomeSerializer from .course_details import CourseDetailsSerializer from .course_team import CourseTeamSerializer +from .course_rerun import CourseRerunSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, 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 feec2606205e..742d198a7ad4 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() @@ -38,7 +26,7 @@ class CourseSettingsSerializer(serializers.Serializer): marketing_enabled = serializers.BooleanField() mfe_proctored_exam_settings_url = serializers.CharField(required=False, allow_null=True, allow_blank=True) platform_name = serializers.CharField() - 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/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 13fec10ddc92..286424f5e1fd 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -1,8 +1,7 @@ """ Contenstore API v1 URLs. """ -from django.urls import path -from django.urls import re_path from django.conf import settings +from django.urls import re_path, path from openedx.core.constants import COURSE_ID_PATTERN @@ -10,7 +9,9 @@ CourseDetailsView, CourseTeamView, CourseGradingView, + CourseRerunView, CourseSettingsView, + HomePageView, ProctoredExamSettingsView, ProctoringErrorsView, xblock, @@ -25,6 +26,11 @@ VIDEO_ID_PATTERN = r'(?:(?P[-\w]+))' urlpatterns = [ + path( + 'home', + HomePageView.as_view(), + name="home" + ), re_path( fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$', ProctoredExamSettingsView.as_view(), @@ -92,4 +98,9 @@ HelpUrlsView.as_view(), name="help_urls" ), + 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 044e4653e05d..dc5f1f77c3de 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -3,8 +3,10 @@ """ from .course_details import CourseDetailsView from .course_team import CourseTeamView +from .course_rerun import CourseRerunView from .grading import CourseGradingView from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView +from .home import HomePageView from .settings import CourseSettingsView from .xblock import XblockView from .assets import AssetsView 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/rest_api/v1/views/tests/test_course_rerun.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_rerun.py new file mode 100644 index 000000000000..e25904ad465f --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/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/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py new file mode 100644 index 000000000000..110ee24ba150 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/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/utils.py b/cms/djangoapps/contentstore/utils.py index 321b97be6f78..97b70e828ce2 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -24,6 +24,7 @@ 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_modes.models import CourseMode from common.djangoapps.edxmako.services import MakoService from common.djangoapps.student import auth @@ -59,6 +60,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 ( + split_library_view_on_dashboard, use_new_advanced_settings_page, use_new_course_outline_page, use_new_export_page, @@ -68,12 +70,13 @@ 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, use_new_custom_pages, ) -from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_video_editor from cms.djangoapps.models.settings.course_grading import CourseGradingModel from xmodule.library_tools import LibraryToolsService from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order @@ -1387,6 +1390,109 @@ def get_help_urls(): return help_tokens +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 + + 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 28de4cfde9bb..d74a99a8e391 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -31,6 +31,7 @@ from organizations.exceptions import InvalidOrganizationException from rest_framework.exceptions import ValidationError +from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import create_xblock_info from cms.djangoapps.course_creators.views import add_user_with_status_unrequested, get_course_creator_status from cms.djangoapps.course_creators.models import CourseCreator from cms.djangoapps.models.settings.course_grading import CourseGradingModel @@ -100,6 +101,7 @@ add_instructor, get_course_settings, get_course_grading, + get_home_context, get_lms_link_for_item, get_proctored_exam_settings_url, get_course_outline_url, @@ -108,6 +110,7 @@ get_advanced_settings_url, get_grading_url, get_schedule_details_url, + get_course_rerun_context, initialize_permissions, remove_all_instructors, reverse_course_url, @@ -118,15 +121,7 @@ update_course_discussions_settings, ) from .component import ADVANCED_COMPONENT_TYPES -from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers 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() @@ -334,13 +329,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 @@ -551,62 +541,8 @@ def course_listing(request): if use_new_home_page(): return redirect(get_studio_home_url()) - 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