Skip to content

Commit

Permalink
feat: [AXIMST-26] API endpoint to return components in unit (#2488)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruzniaievdm authored and monteri committed Jan 24, 2024
1 parent ef972dc commit 990a6f1
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 18 deletions.
25 changes: 24 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v1/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
Common mixins for module.
"""
import json
import logging
from unittest.mock import patch

from django.http import Http404
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from rest_framework import status

log = logging.getLogger(__name__)


class PermissionAccessMixin:
"""
Expand All @@ -30,7 +36,7 @@ def test_permissions_unauthenticated(self):
self.assertEqual(error, "Authentication credentials were not provided.")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

@patch.dict('django.conf.settings.FEATURES', {'DISABLE_ADVANCED_SETTINGS': True})
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_ADVANCED_SETTINGS": True})
def test_permissions_unauthorized(self):
"""
Test that an error is returned if the user is unauthorised.
Expand All @@ -40,3 +46,20 @@ def test_permissions_unauthorized(self):
error = self.get_and_check_developer_response(response)
self.assertEqual(error, "You do not have permission to perform this action.")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class ContainerHandlerMixin:
"""
A mixin providing common functionality for container handler views.
"""

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)
return usage_key
except InvalidKeyError as err:
log.error(f"Invalid usage key: {usage_key_string}", exc_info=True)
raise Http404(f"Object not found for usage key: {usage_key_string}") from err
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
VideoUsageSerializer,
VideoDownloadSerializer
)
from .vertical_block import ContainerHandlerSerializer, VerticalContainerSerializer
110 changes: 110 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
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


class ChildVerticalContainerSerializer(serializers.Serializer):
"""
Serializer for representing a xblock child of vertical container.
"""

name = serializers.CharField(source="display_name_with_default")
block_id = serializers.CharField(source="location")


class VerticalContainerSerializer(serializers.Serializer):
"""
Serializer for representing a vertical container with state and children.
"""

children = ChildVerticalContainerSerializer(many=True)
is_published = serializers.BooleanField()
13 changes: 12 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
ProctoringErrorsView,
HelpUrlsView,
VideoUsageView,
VideoDownloadView
VideoDownloadView,
VerticalContainerView,
)

app_name = 'v1'
Expand Down Expand Up @@ -100,6 +101,16 @@
CourseRerunView.as_view(),
name="course_rerun"
),
re_path(
fr'^container_handler/{settings.USAGE_KEY_PATTERN}$',
ContainerHandlerView.as_view(),
name="container_handler"
),
re_path(
fr'^container/vertical/{settings.USAGE_KEY_PATTERN}/children$',
VerticalContainerView.as_view(),
name="container_vertical"
),

# 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, VerticalContainerView
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""
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
from xmodule.modulestore import (
ModuleStoreEnum,
) # lint-amnesty, pylint: disable=wrong-import-order


class BaseXBlockContainer(CourseTestCase):
"""
Base xBlock container handler.
Contains common function for processing course xblocks.
"""

view_name = None

def setUp(self):
super().setUp()
self.store = modulestore()
self.setup_xblock()

def setup_xblock(self):
"""
Set up XBlock objects for testing purposes.
This method creates XBlock objects representing a course structure with chapters,
sequentials, verticals and others.
"""
self.chapter = self.create_block(
parent=self.course.location,
category="chapter",
display_name="Week 1",
)

self.sequential = self.create_block(
parent=self.chapter.location,
category="sequential",
display_name="Lesson 1",
)

self.vertical = self.create_block(self.sequential.location, "vertical", "Unit")

self.html_unit_first = self.create_block(
parent=self.vertical.location,
category="html",
display_name="Html Content 1",
)

self.html_unit_second = self.create_block(
parent=self.vertical.location,
category="html",
display_name="Html Content 2",
)

def create_block(self, parent, category, display_name, **kwargs):
"""
Creates a block without publishing it.
"""
return BlockFactory.create(
parent_location=parent,
category=category,
display_name=display_name,
modulestore=self.store,
publish_item=False,
user_id=self.user.id,
**kwargs,
)

def get_reverse_url(self, location):
"""
Creates url to current view api name
"""
return reverse(
f"cms.djangoapps.contentstore:v1:{self.view_name}",
kwargs={"usage_key_string": location},
)

def publish_item(self, store, item_location):
"""
Publish the item at the given location
"""
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
store.publish(item_location, ModuleStoreEnum.UserID.test)


class ContainerHandlerViewTest(BaseXBlockContainer):
"""
Unit tests for the ContainerHandlerView.
"""

view_name = "container_handler"

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)


class ContainerVerticalViewTest(BaseXBlockContainer):
"""
Unit tests for the ContainerVerticalViewTest.
"""

view_name = "container_vertical"

def test_success_response(self):
"""
Check that endpoint returns valid response data.
"""
url = self.get_reverse_url(self.vertical.location)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["children"]), 2)
self.assertFalse(response.data["is_published"])

def test_xblock_is_published(self):
"""
Check that published xBlock container returns.
"""
self.publish_item(self.store, self.vertical.location)
url = self.get_reverse_url(self.vertical.location)
response = self.client.get(url)
self.assertTrue(response.data["is_published"])

def test_children_content(self):
"""
Check that returns valid response with children of vertical container.
"""
url = self.get_reverse_url(self.vertical.location)
response = self.client.get(url)

expected_response = [
{
"name": self.html_unit_first.display_name_with_default,
"block_id": str(self.html_unit_first.location),
},
{
"name": self.html_unit_second.display_name_with_default,
"block_id": str(self.html_unit_second.location),
},
]
self.assertEqual(response.data["children"], expected_response)

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)
Loading

0 comments on commit 990a6f1

Please sign in to comment.