Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [FC-0044] Unit page API as DRF #34036

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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,
Expand All @@ -21,3 +21,4 @@
VideoUsageSerializer,
VideoDownloadSerializer
)
from .vertical_block import ContainerHandlerSerializer
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -100,6 +102,11 @@
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
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 @@ -15,3 +15,4 @@
VideoDownloadView
)
from .help_urls import HelpUrlsView
from .vertical_block import ContainerHandlerView
6 changes: 3 additions & 3 deletions cms/djangoapps/contentstore/rest_api/v1/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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)
Expand Down Expand Up @@ -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.",
},
)
Expand Down Expand Up @@ -160,7 +160,7 @@ def get(self, request: Request):
"archived_courses": archived_courses,
"in_process_course_actions": in_process_course_actions,
}
serializer = CourseTabSerializer(courses_context)
serializer = CourseHomeTabSerializer(courses_context)
return Response(serializer.data)


Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 143 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading