feat: separate home api call into load, course, and library info (#33909)

This commit is contained in:
Kristin Aoki
2023-12-19 08:32:50 -05:00
committed by GitHub
parent e1b9da21e8
commit 9f5252c6ee
7 changed files with 278 additions and 88 deletions

View File

@@ -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,

View File

@@ -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()

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,