Skip to content
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 @@ -11,6 +11,7 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
"""
Serializer for course waffle flags
"""

use_new_home_page = serializers.SerializerMethodField()
use_new_custom_pages = serializers.SerializerMethodField()
use_new_schedule_details_page = serializers.SerializerMethodField()
Expand All @@ -31,6 +32,7 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
use_react_markdown_editor = serializers.SerializerMethodField()
use_video_gallery_flow = serializers.SerializerMethodField()
enable_course_optimizer_check_prev_run_links = serializers.SerializerMethodField()
enable_unit_expanded_view = serializers.SerializerMethodField()

def get_course_key(self):
"""
Expand Down Expand Up @@ -175,3 +177,10 @@ def get_enable_course_optimizer_check_prev_run_links(self, obj):
"""
course_key = self.get_course_key()
return toggles.enable_course_optimizer_check_prev_run_links(course_key)

def get_enable_unit_expanded_view(self, obj):
"""
Method to get the enable_unit_expanded_view waffle flag
"""
course_key = self.get_course_key()
return toggles.enable_unit_expanded_view(course_key)
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 @@ -25,6 +25,7 @@
HomePageView,
ProctoredExamSettingsView,
ProctoringErrorsView,
UnitComponentsView,
VideoDownloadView,
VideoUsageView,
vertical_container_children_redirect_view,
Expand Down Expand Up @@ -144,6 +145,11 @@
CourseWaffleFlagsView.as_view(),
name="course_waffle_flags"
),
re_path(
fr'^unit_handler/{settings.USAGE_KEY_PATTERN}$',
UnitComponentsView.as_view(),
name="unit_components"
),

# 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
7 changes: 2 additions & 5 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView
from .settings import CourseSettingsView
from .textbooks import CourseTextbooksView
from .unit_handler import UnitComponentsView
from .vertical_block import ContainerHandlerView, vertical_container_children_redirect_view
from .videos import (
CourseVideosView,
VideoDownloadView,
VideoUsageView,
)
from .videos import CourseVideosView, VideoDownloadView, VideoUsageView
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class CourseWaffleFlagsViewTest(CourseTestCase):
"use_react_markdown_editor": False,
"use_video_gallery_flow": False,
"enable_course_optimizer_check_prev_run_links": False,
"enable_unit_expanded_view": False,
}

def setUp(self):
Expand Down
135 changes: 135 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/unit_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""API Views for unit components handler"""

import logging

import edx_api_doc_tools as apidocs
from django.http import HttpResponseBadRequest, HttpResponseForbidden
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.rest_api.v1.mixins import ContainerHandlerMixin
from cms.djangoapps.contentstore.toggles import enable_unit_expanded_view
from openedx.core.lib.api.view_utils import view_auth_classes
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError

log = logging.getLogger(__name__)


@view_auth_classes(is_authenticated=True)
class UnitComponentsView(APIView, ContainerHandlerMixin):
"""
View to get all components in a unit by usage key.
"""

@apidocs.schema(
parameters=[
apidocs.string_parameter(
"usage_key_string",
apidocs.ParameterLocation.PATH,
description="Unit usage key",
),
],
responses={
200: "List of components in the unit",
400: "Invalid usage key or unit not found.",
401: "The requester is not authenticated.",
404: "The requested unit does not exist.",
},
)
def get(self, request: Request, usage_key_string: str):
"""
Get all components in a unit.

**Example Request**

GET /api/contentstore/v1/unit_handler/{usage_key_string}

**Response Values**

If the request is successful, an HTTP 200 "OK" response is returned.

The HTTP 200 response contains a dict with a list of all components
in the unit, including their display names, block types, and block IDs.

**Example Response**

```json
{
"unit_id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_id",
"display_name": "My Unit",
"components": [
{
"block_id": "block-v1:edX+DemoX+Demo_Course+type@video+block@video_id",
"block_type": "video",
"display_name": "Introduction Video"
},
{
"block_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_id",
"block_type": "html",
"display_name": "Text Content"
},
{
"block_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@problem_id",
"block_type": "problem",
"display_name": "Practice Problem"
}
]
}
```
"""
try:
usage_key = UsageKey.from_string(usage_key_string)
except Exception as e: # pylint: disable=broad-exception-caught
log.error(f"Invalid usage key: {usage_key_string}, error: {str(e)}")
return HttpResponseBadRequest("Invalid usage key format")

try:
# Get the unit xblock
unit_xblock = modulestore().get_item(usage_key)

# Verify it's a vertical (unit)
if unit_xblock.category != "vertical":
return HttpResponseBadRequest(
"The provided usage key is not a unit (vertical)"
)

if not enable_unit_expanded_view(unit_xblock.location.course_key):
return HttpResponseForbidden(
"Unit expanded view is disabled for this course"
)

components = []

# Get all children (components) of the unit
if unit_xblock.has_children:
for child_usage_key in unit_xblock.children:
try:
child_xblock = modulestore().get_item(child_usage_key)
components.append(
{
"block_id": str(child_xblock.location),
"block_type": child_xblock.category,
"display_name": child_xblock.display_name_with_default,
}
)
except ItemNotFoundError:
log.warning(f"Child block not found: {child_usage_key}")
continue

response_data = {
"unit_id": str(usage_key),
"display_name": unit_xblock.display_name_with_default,
"components": components,
}

return Response(response_data)

except ItemNotFoundError:
log.error(f"Unit not found: {usage_key_string}")
return HttpResponseBadRequest("Unit not found")
except Exception as e: # pylint: disable=broad-exception-caught
log.error(f"Error retrieving unit components: {str(e)}")
return HttpResponseBadRequest(f"Error retrieving unit components: {str(e)}")
20 changes: 20 additions & 0 deletions cms/djangoapps/contentstore/toggles.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,3 +682,23 @@ def enable_course_optimizer_check_prev_run_links(course_key):
Returns a boolean if previous run course optimizer feature is enabled for the given course.
"""
return ENABLE_COURSE_OPTIMIZER_CHECK_PREV_RUN_LINKS.is_enabled(course_key)


# .. toggle_name: contentstore.enable_unit_expanded_view
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: When enabled, the Unit Expanded View feature in the Course Outline is activated.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2026-01-01
# .. toggle_target_removal_date: 2026-06-01
# .. toggle_tickets: TNL2-473
ENABLE_UNIT_EXPANDED_VIEW = CourseWaffleFlag(
f"{CONTENTSTORE_NAMESPACE}.enable_unit_expanded_view", __name__
)


def enable_unit_expanded_view(course_key):
"""
Returns a boolean if the Unit Expanded View feature is enabled for the given course.
"""
return ENABLE_UNIT_EXPANDED_VIEW.is_enabled(course_key)
Loading