diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/home.py b/cms/djangoapps/contentstore/rest_api/v2/views/home.py index 8f2759c537b8..4c7ee4151535 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v2/views/home.py @@ -1,14 +1,44 @@ """HomePageCoursesViewV2 APIView for getting content available to the logged in user.""" import edx_api_doc_tools as apidocs -from rest_framework.request import Request +from collections import OrderedDict from rest_framework.response import Response +from rest_framework.request import Request from rest_framework.views import APIView +from rest_framework.pagination import PageNumberPagination + from openedx.core.lib.api.view_utils import view_auth_classes from cms.djangoapps.contentstore.utils import get_course_context_v2 from cms.djangoapps.contentstore.rest_api.v2.serializers import CourseHomeTabSerializerV2 +class HomePageCoursesPaginator(PageNumberPagination): + + def get_paginated_response(self, data): + """Return a paginated style `Response` object for the given output data.""" + return Response(OrderedDict([ + ('count', self.page.paginator.count), + ('num_pages', self.page.paginator.num_pages), + ('next', self.get_next_link()), + ('previous', self.get_previous_link()), + ('results', data), + ])) + + def paginate_queryset(self, queryset, request, view=None): + """ + Paginate a queryset if required, either returning a page object, + or `None` if pagination is not configured for this view. + + This method is a modified version of the original `paginate_queryset` method + from the `PageNumberPagination` class. The original method was modified to + handle the case where the `queryset` is a `filter` object. + """ + if isinstance(queryset, filter): + queryset = list(queryset) + + return super().paginate_queryset(queryset, request, view) + + @view_auth_classes(is_authenticated=True) class HomePageCoursesViewV2(APIView): """View for getting all courses available to the logged in user.""" @@ -40,6 +70,11 @@ class HomePageCoursesViewV2(APIView): apidocs.ParameterLocation.QUERY, description="Query param to filter by archived courses only", ), + apidocs.string_parameter( + "page", + apidocs.ParameterLocation.QUERY, + description="Query param to paginate the courses", + ), ], responses={ 200: CourseHomeTabSerializerV2, @@ -58,6 +93,7 @@ def get(self, request: Request): GET /api/contentstore/v2/home/courses?order=-org GET /api/contentstore/v2/home/courses?active_only=true GET /api/contentstore/v2/home/courses?archived_only=true + GET /api/contentstore/v2/home/courses?page=2 **Response Values** @@ -88,11 +124,15 @@ def get(self, request: Request): } ``` """ - courses, in_process_course_actions = get_course_context_v2(request) - courses_context = { - "courses": courses, - "in_process_course_actions": in_process_course_actions, - } - serializer = CourseHomeTabSerializerV2(courses_context) - return Response(serializer.data) + paginator = HomePageCoursesPaginator() + courses_page = paginator.paginate_queryset( + courses, + self.request, + view=self + ) + serializer = CourseHomeTabSerializerV2({ + 'courses': courses_page, + 'in_process_course_actions': in_process_course_actions, + }) + return paginator.get_paginated_response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/tests/__init__.py b/cms/djangoapps/contentstore/rest_api/v2/views/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py new file mode 100644 index 000000000000..3e58b6d92491 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py @@ -0,0 +1,69 @@ +""" +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 HomePageCoursesViewTest(CourseTestCase): + """ + Tests for HomePageView. + """ + + def setUp(self): + super().setUp() + self.url = reverse("cms.djangoapps.contentstore:v2: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 = { + "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) + 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)