feat: Unit page API as DRF

This commit is contained in:
ruzniaievdm
2024-01-10 11:31:20 +02:00
parent 2eabfe10c0
commit ff8d3eae72
11 changed files with 459 additions and 112 deletions

View File

@@ -6,7 +6,7 @@ from .course_rerun import CourseRerunSerializer
from .course_team import CourseTeamSerializer
from .course_index import CourseIndexSerializer
from .grading import CourseGradingModelSerializer, CourseGradingSerializer
from .home import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer
from .home import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer
from .proctoring import (
LimitedProctoredExamSettingsSerializer,
ProctoredExamConfigurationSerializer,
@@ -21,3 +21,4 @@ from .videos import (
VideoUsageSerializer,
VideoDownloadSerializer
)
from .vertical_block import ContainerHandlerSerializer

View File

@@ -31,7 +31,7 @@ class LibraryViewSerializer(serializers.Serializer):
can_edit = serializers.BooleanField()
class CourseTabSerializer(serializers.Serializer):
class CourseHomeTabSerializer(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)

View File

@@ -0,0 +1,92 @@
"""
API Serializers for unit page
"""
from django.urls import reverse
from rest_framework import serializers
from cms.djangoapps.contentstore.helpers import (
xblock_studio_url,
xblock_type_display_name,
)
class ChildAncestorSerializer(serializers.Serializer):
"""
Serializer for representing child blocks in the ancestor XBlock.
"""
url = serializers.SerializerMethodField()
display_name = serializers.CharField(source="display_name_with_default")
def get_url(self, obj):
"""
Method to generate studio URL for the child block.
"""
return xblock_studio_url(obj)
class AncestorXBlockSerializer(serializers.Serializer):
"""
Serializer for representing the ancestor XBlock and its children.
"""
children = ChildAncestorSerializer(many=True)
title = serializers.CharField()
is_last = serializers.BooleanField()
class ContainerXBlock(serializers.Serializer):
"""
Serializer for representing XBlock data. Doesn't include all data about XBlock.
"""
display_name = serializers.CharField(source="display_name_with_default")
display_type = serializers.SerializerMethodField()
category = serializers.CharField()
def get_display_type(self, obj):
"""
Method to get the display type name for the container XBlock.
"""
return xblock_type_display_name(obj)
class ContainerHandlerSerializer(serializers.Serializer):
"""
Serializer for container handler
"""
language_code = serializers.CharField()
action = serializers.CharField()
xblock = ContainerXBlock()
is_unit_page = serializers.BooleanField()
is_collapsible = serializers.BooleanField()
position = serializers.IntegerField(min_value=1)
prev_url = serializers.CharField(allow_null=True)
next_url = serializers.CharField(allow_null=True)
new_unit_category = serializers.CharField()
outline_url = serializers.CharField()
ancestor_xblocks = AncestorXBlockSerializer(many=True)
component_templates = serializers.ListField(child=serializers.DictField())
xblock_info = serializers.DictField()
draft_preview_link = serializers.CharField()
published_preview_link = serializers.CharField()
show_unit_tags = serializers.BooleanField()
user_clipboard = serializers.DictField()
is_fullwidth_content = serializers.BooleanField()
assets_url = serializers.SerializerMethodField()
unit_block_id = serializers.CharField(source="unit.location.block_id")
subsection_location = serializers.CharField(source="subsection.location")
def get_assets_url(self, obj):
"""
Method to get the assets URL based on the course id.
"""
context_course = obj.get("context_course", None)
if context_course:
return reverse(
"assets_handler", kwargs={"course_key_string": context_course.id}
)
return None

View File

@@ -1,10 +1,12 @@
""" Contenstore API v1 URLs. """
from django.conf import settings
from django.urls import re_path, path
from openedx.core.constants import COURSE_ID_PATTERN
from .views import (
ContainerHandlerView,
CourseDetailsView,
CourseTeamView,
CourseIndexView,
@@ -100,6 +102,11 @@ urlpatterns = [
CourseRerunView.as_view(),
name="course_rerun"
),
re_path(
fr'^container_handler/{settings.USAGE_KEY_PATTERN}$',
ContainerHandlerView.as_view(),
name="container_handler"
),
# Authoring API
# Do not use under v1 yet (Nov. 23). The Authoring API is still experimental and the v0 versions should be used

View File

@@ -15,3 +15,4 @@ from .videos import (
VideoDownloadView
)
from .help_urls import HelpUrlsView
from .vertical_block import ContainerHandlerView

View File

@@ -8,7 +8,7 @@ from rest_framework.views import APIView
from openedx.core.lib.api.view_utils import view_auth_classes
from ....utils import get_home_context, get_course_context, get_library_context
from ..serializers import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer
from ..serializers import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer
@view_auth_classes(is_authenticated=True)
@@ -102,7 +102,7 @@ class HomePageCoursesView(APIView):
description="Query param to filter by course org",
)],
responses={
200: CourseTabSerializer,
200: CourseHomeTabSerializer,
401: "The requester is not authenticated.",
},
)
@@ -160,7 +160,7 @@ class HomePageCoursesView(APIView):
"archived_courses": archived_courses,
"in_process_course_actions": in_process_course_actions,
}
serializer = CourseTabSerializer(courses_context)
serializer = CourseHomeTabSerializer(courses_context)
return Response(serializer.data)

View File

@@ -0,0 +1,67 @@
"""
Unit tests for the vertical block.
"""
from django.urls import reverse
from rest_framework import status
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
class ContainerHandlerViewTest(CourseTestCase):
"""
Unit tests for the ContainerHandlerView.
"""
def setUp(self):
super().setUp()
self.chapter = BlockFactory.create(
parent=self.course, category="chapter", display_name="Week 1"
)
self.sequential = BlockFactory.create(
parent=self.chapter, category="sequential", display_name="Lesson 1"
)
self.vertical = self._create_block(self.sequential, "vertical", "Unit")
self.store = modulestore()
self.store.publish(self.vertical.location, self.user.id)
def _get_reverse_url(self, location):
"""
Creates url to current handler view api
"""
return reverse(
"cms.djangoapps.contentstore:v1:container_handler",
kwargs={"usage_key_string": location},
)
def _create_block(self, parent, category, display_name, **kwargs):
"""
Creates a block without publishing it.
"""
return BlockFactory.create(
parent=parent,
category=category,
display_name=display_name,
publish_item=False,
user_id=self.user.id,
**kwargs
)
def test_success_response(self):
"""
Check that endpoint is valid and success response.
"""
url = self._get_reverse_url(self.vertical.location)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_not_valid_usage_key_string(self):
"""
Check that invalid 'usage_key_string' raises Http404.
"""
usage_key_string = "i4x://InvalidOrg/InvalidCourse/vertical/static/InvalidContent"
url = self._get_reverse_url(usage_key_string)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1,143 @@
""" API Views for unit page """
import edx_api_doc_tools as apidocs
from django.http import Http404, HttpResponseBadRequest
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
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_container_handler_context
from cms.djangoapps.contentstore.views.component import _get_item_in_course
from cms.djangoapps.contentstore.rest_api.v1.serializers import ContainerHandlerSerializer
from openedx.core.lib.api.view_utils import view_auth_classes
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
@view_auth_classes(is_authenticated=True)
class ContainerHandlerView(APIView):
"""
View for container xblock requests to get vertical data.
"""
def get_object(self, usage_key_string):
"""
Get an object by usage-id of the block
"""
try:
usage_key = UsageKey.from_string(usage_key_string)
except InvalidKeyError:
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
return usage_key
@apidocs.schema(
parameters=[
apidocs.string_parameter(
"usage_key_string",
apidocs.ParameterLocation.PATH,
description="Container usage key",
),
],
responses={
200: ContainerHandlerSerializer,
401: "The requester is not authenticated.",
404: "The requested locator does not exist.",
},
)
def get(self, request: Request, usage_key_string: str):
"""
Get an object containing vertical data.
**Example Request**
GET /api/contentstore/v1/container_handler/{usage_key_string}
**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 vertical's container data.
**Example Response**
```json
{
"language_code": "zh-cn",
"action": "view",
"xblock": {
"display_name": "Labs and Demos",
"display_type": "单元",
"category": "vertical"
},
"is_unit_page": true,
"is_collapsible": false,
"position": 1,
"prev_url": "block-v1-edX%2BDemo_Course%2Btype%40vertical%2Bblock%404e592689563243c484",
"next_url": "block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%40vertical_aae927868e55",
"new_unit_category": "vertical",
"outline_url": "/course/course-v1:edX+DemoX+Demo_Course?format=concise",
"ancestor_xblocks": [
{
"children": [
{
"url": "/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%",
"display_name": "Introduction"
},
...
],
"title": "Example Week 2: Get Interactive",
"is_last": false
},
...
],
"component_templates": [
{
"type": "advanced",
"templates": [
{
"display_name": "批注",
"category": "annotatable",
"boilerplate_name": null,
"hinted": false,
"tab": "common",
"support_level": true
},
...
},
...
],
"xblock_info": {},
"draft_preview_link": "//preview.localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/...",
"published_preview_link": "///courses/course-v1:edX+DemoX+Demo_Course/jump_to/...",
"show_unit_tags": false,
"user_clipboard": {
"content": null,
"source_usage_key": "",
"source_context_title": "",
"source_edit_url": ""
},
"is_fullwidth_content": false,
"assets_url": "/assets/course-v1:edX+DemoX+Demo_Course/",
"unit_block_id": "d6cee45205a449369d7ef8f159b22bdf",
"subsection_location": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations"
}
```
"""
usage_key = self.get_object(usage_key_string)
course_key = usage_key.course_key
with modulestore().bulk_operations(course_key):
try:
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
except ItemNotFoundError:
return HttpResponseBadRequest()
context = get_container_handler_context(request, usage_key, course, xblock)
context.update({
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
})
serializer = ContainerHandlerSerializer(context)
return Response(serializer.data)