feat: separate home api call into load, course, and library info (#33909)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user