Skip to content

Commit

Permalink
feat: [AXIMST-111] create API for course textbooks (#2491)
Browse files Browse the repository at this point in the history
* feat: [AXIMST-111] create API for course textbooks

* fix: add tabulation
  • Loading branch information
ruzniaievdm authored and monteri committed Jan 30, 2024
1 parent 82095be commit 2db9b8b
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ProctoringErrorsSerializer
)
from .settings import CourseSettingsSerializer
from .textbooks import CourseTextbooksSerializer
from .videos import (
CourseVideosSerializer,
VideoUploadSerializer,
Expand Down
32 changes: 32 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/textbooks.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CourseCertificatesView,
CourseDetailsView,
CourseTeamView,
CourseTextbooksView,
CourseIndexView,
CourseGradingView,
CourseRerunView,
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
90 changes: 90 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/textbooks.py
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 16 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 3 additions & 8 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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':
Expand Down

0 comments on commit 2db9b8b

Please sign in to comment.