Skip to content

Commit

Permalink
feat: [FC-0044] XBlock's children API as DRF (openedx#34055)
Browse files Browse the repository at this point in the history
* feat: XBlock's children API as DRF

* fix: 500 error appears if user adds a Content Experiment

* fix: wrap into try/except block getting icon for xblock (#2509)

* fix: wrap into try/except block getting icon for xblock

* fix: revision after review
  • Loading branch information
ruzniaievdm authored Mar 12, 2024
1 parent 844db29 commit cd5c4c9
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 47 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,4 +21,4 @@
VideoUsageSerializer,
VideoDownloadSerializer
)
from .vertical_block import ContainerHandlerSerializer
from .vertical_block import ContainerHandlerSerializer, VerticalContainerSerializer
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,21 @@ def get_assets_url(self, obj):
"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()
8 changes: 7 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
ProctoringErrorsView,
HelpUrlsView,
VideoUsageView,
VideoDownloadView
VideoDownloadView,
VerticalContainerView,
)

app_name = 'v1'
Expand Down Expand Up @@ -107,6 +108,11 @@
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
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
VideoDownloadView
)
from .help_urls import HelpUrlsView
from .vertical_block import ContainerHandlerView
from .vertical_block import ContainerHandlerView, VerticalContainerView
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,174 @@
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.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 ContainerHandlerViewTest(CourseTestCase):
class BaseXBlockContainer(CourseTestCase):
"""
Unit tests for the ContainerHandlerView.
Base xBlock container handler.
Contains common function for processing course xblocks.
"""

view_name = None

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)
self.setup_xblock()

def _get_reverse_url(self, location):
def setup_xblock(self):
"""
Creates url to current handler view api
Set up XBlock objects for testing purposes.
This method creates XBlock objects representing a course structure with chapters,
sequentials, verticals and others.
"""
return reverse(
"cms.djangoapps.contentstore:v1:container_handler",
kwargs={"usage_key_string": location},
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):
def create_block(self, parent, category, display_name, **kwargs):
"""
Creates a block without publishing it.
"""
return BlockFactory.create(
parent=parent,
parent_location=parent,
category=category,
display_name=display_name,
modulestore=self.store,
publish_item=False,
user_id=self.user.id,
**kwargs
**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)
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)
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 cd5c4c9

Please sign in to comment.