feat: Unit page API as DRF
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -15,3 +15,4 @@ from .videos import (
|
||||
VideoDownloadView
|
||||
)
|
||||
from .help_urls import HelpUrlsView
|
||||
from .vertical_block import ContainerHandlerView
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
143
cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py
Normal file
143
cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py
Normal 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)
|
||||
Reference in New Issue
Block a user