From d5fc1d3cf5f3fe0c1c98962203404a357dfeeebb Mon Sep 17 00:00:00 2001 From: ruzniaievdm Date: Wed, 10 Jan 2024 13:33:04 +0200 Subject: [PATCH] feat: [AXIMST-111] create API for course textbooks (#2491) * feat: [AXIMST-111] create API for course textbooks * fix: add tabulation --- .../rest_api/v1/serializers/__init__.py | 1 + .../rest_api/v1/serializers/textbooks.py | 32 +++++++ .../contentstore/rest_api/v1/urls.py | 6 ++ .../rest_api/v1/views/__init__.py | 1 + .../rest_api/v1/views/tests/test_textbooks.py | 43 +++++++++ .../v1/views/tests/test_vertical_block.py | 4 +- .../rest_api/v1/views/textbooks.py | 90 +++++++++++++++++++ cms/djangoapps/contentstore/utils.py | 16 ++++ cms/djangoapps/contentstore/views/course.py | 11 +-- 9 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/v1/serializers/textbooks.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/tests/test_textbooks.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/textbooks.py diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index d5fd1619dc47..f0c3ff4c6056 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -15,6 +15,7 @@ ProctoringErrorsSerializer ) from .settings import CourseSettingsSerializer +from .textbooks import CourseTextbooksSerializer from .videos import ( CourseVideosSerializer, VideoUploadSerializer, diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/textbooks.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/textbooks.py new file mode 100644 index 000000000000..1ee5b73fd198 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/textbooks.py @@ -0,0 +1,32 @@ +""" +API Serializers for textbooks page +""" + +from rest_framework import serializers + + +class CourseTextbookChapterSerializer(serializers.Serializer): + """ + Serializer for representing textbook chapter. + """ + + title = serializers.CharField() + url = serializers.CharField() + + +class CourseTextbookItemSerializer(serializers.Serializer): + """ + Serializer for representing textbook item. + """ + + id = serializers.CharField() + chapters = CourseTextbookChapterSerializer(many=True) + tab_title = serializers.CharField() + + +class CourseTextbooksSerializer(serializers.Serializer): + """ + Serializer for representing course's textbooks. + """ + + textbooks = serializers.ListField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 8bfdfcbde025..daf2a5927c1e 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -9,6 +9,7 @@ CourseCertificatesView, CourseDetailsView, CourseTeamView, + CourseTextbooksView, CourseIndexView, CourseGradingView, CourseRerunView, @@ -108,6 +109,11 @@ CourseCertificatesView.as_view(), name="certificates" ), + re_path( + fr'^textbooks/{COURSE_ID_PATTERN}$', + CourseTextbooksView.as_view(), + name="textbooks" + ), re_path( fr'^container_handler/{settings.USAGE_KEY_PATTERN}$', ContainerHandlerView.as_view(), diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index d7da4f890f72..804854184651 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -10,6 +10,7 @@ from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView from .home import HomePageView, HomePageCoursesView, HomePageLibrariesView from .settings import CourseSettingsView +from .textbooks import CourseTextbooksView from .videos import ( CourseVideosView, VideoUsageView, diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_textbooks.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_textbooks.py new file mode 100644 index 000000000000..d4dd80f8ab05 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_textbooks.py @@ -0,0 +1,43 @@ +""" +Unit tests for the course's textbooks. +""" +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase + +from ...mixins import PermissionAccessMixin + + +class CourseTextbooksViewTest(CourseTestCase, PermissionAccessMixin): + """ + Tests for CourseTextbooksView. + """ + + def setUp(self): + super().setUp() + self.url = reverse( + "cms.djangoapps.contentstore:v1:textbooks", + kwargs={"course_id": self.course.id}, + ) + + def test_success_response(self): + """ + Check that endpoint is valid and success response. + """ + expected_textbook = [ + { + "tab_title": "Textbook Name", + "chapters": [ + {"title": "Chapter 1", "url": "/static/book.pdf"}, + {"title": "Chapter 2", "url": "/static/story.pdf"}, + ], + "id": "Textbook_Name", + } + ] + self.course.pdf_textbooks = expected_textbook + self.save_course() + + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["textbooks"], expected_textbook) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py index 848c579f093e..6fec78b65077 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py @@ -118,7 +118,7 @@ def test_not_valid_usage_key_string(self): ) url = self.get_reverse_url(usage_key_string) response = self.client.get(url) - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) class ContainerVerticalViewTest(BaseXBlockContainer): @@ -177,4 +177,4 @@ def test_not_valid_usage_key_string(self): ) url = self.get_reverse_url(usage_key_string) response = self.client.get(url) - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/textbooks.py b/cms/djangoapps/contentstore/rest_api/v1/views/textbooks.py new file mode 100644 index 000000000000..620b40235b73 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/textbooks.py @@ -0,0 +1,90 @@ +""" API Views for course textbooks """ + +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_textbooks_context +from cms.djangoapps.contentstore.rest_api.v1.serializers import ( + CourseTextbooksSerializer, +) +from common.djangoapps.student.auth import has_studio_read_access +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 CourseTextbooksView(DeveloperErrorViewMixin, APIView): + """ + View for course textbooks page. + """ + + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "course_id", apidocs.ParameterLocation.PATH, description="Course ID" + ), + ], + responses={ + 200: CourseTextbooksSerializer, + 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's textbooks. + + **Example Request** + + GET /api/contentstore/v1/textbooks/{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 textbooks. + + **Example Response** + + ```json + { + "textbooks": [ + { + "tab_title": "Textbook Name", + "chapters": [ + { + "title": "Chapter 1", + "url": "/static/Present_Perfect.pdf" + }, + { + "title": "Chapter 2", + "url": "/static/Lear.pdf" + } + ], + "id": "Textbook_Name" + } + ] + } + ``` + """ + course_key = CourseKey.from_string(course_id) + store = modulestore() + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + with store.bulk_operations(course_key): + course = modulestore().get_course(course_key) + textbooks_context = get_textbooks_context(course) + serializer = CourseTextbooksSerializer(textbooks_context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index e86ddea4a1b2..79b3fc39bf9e 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1960,6 +1960,22 @@ def get_certificates_context(course, user): return context +def get_textbooks_context(course): + """ + Utils is used to get context for textbooks for course. + It is used for both DRF and django views. + """ + + upload_asset_url = reverse_course_url('assets_handler', course.id) + textbook_url = reverse_course_url('textbooks_list_handler', course.id) + return { + 'context_course': course, + 'textbooks': course.pdf_textbooks, + 'upload_asset_url': upload_asset_url, + 'textbook_url': textbook_url, + } + + 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 eac3c1048d7a..a58c337b46f6 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -109,6 +109,7 @@ get_grading_url, get_schedule_details_url, get_course_rerun_context, + get_textbooks_context, initialize_permissions, remove_all_instructors, reverse_course_url, @@ -1347,14 +1348,8 @@ def textbooks_list_handler(request, course_key_string): if "application/json" not in request.META.get('HTTP_ACCEPT', 'text/html'): # return HTML page - upload_asset_url = reverse_course_url('assets_handler', course_key) - textbook_url = reverse_course_url('textbooks_list_handler', course_key) - return render_to_response('textbooks.html', { - 'context_course': course, - 'textbooks': course.pdf_textbooks, - 'upload_asset_url': upload_asset_url, - 'textbook_url': textbook_url, - }) + textbooks_context = get_textbooks_context(course) + return render_to_response('textbooks.html', textbooks_context) # from here on down, we know the client has requested JSON if request.method == 'GET':