diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py index 881e62e42fc7..9c73036fbf7d 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py @@ -12,6 +12,15 @@ ) +class MessageValidation(serializers.Serializer): + """ + Serializer for representing XBlock error. + """ + + text = serializers.CharField() + type = serializers.CharField() + + class ChildAncestorSerializer(serializers.Serializer): """ Serializer for representing child blocks in the ancestor XBlock. @@ -105,6 +114,8 @@ class ChildVerticalContainerSerializer(serializers.Serializer): user_partition_info = serializers.DictField() user_partitions = serializers.ListField() actions = serializers.SerializerMethodField() + validation_messages = MessageValidation(many=True) + render_error = serializers.CharField() def get_actions(self, obj): # pylint: disable=unused-argument """ diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py index 9e76c463f314..b8e4a52bd17e 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py @@ -1,13 +1,19 @@ """ Unit tests for the vertical block. """ + from django.urls import reverse from rest_framework import status from edx_toggles.toggles.testutils import override_waffle_flag +from xblock.validation import ValidationMessage from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.toggles import ENABLE_TAGGING_TAXONOMY_LIST_PAGE -from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID +from xmodule.partitions.partitions import ( + ENROLLMENT_TRACK_PARTITION_ID, + Group, + UserPartition, +) from xmodule.modulestore.django import ( modulestore, ) # lint-amnesty, pylint: disable=wrong-import-order @@ -96,6 +102,13 @@ def publish_item(self, store, item_location): with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): store.publish(item_location, ModuleStoreEnum.UserID.test) + def set_group_access(self, xblock, value): + """ + Sets group_access to specified value and calls update_item to persist the change. + """ + xblock.group_access = value + self.store.update_item(xblock, self.user.id) + class ContainerHandlerViewTest(BaseXBlockContainer): """ @@ -161,7 +174,7 @@ def test_children_content(self): expected_user_partition_info = { "selectable_partitions": [], "selected_partition_index": -1, - "selected_groups_label": "" + "selected_groups_label": "", } expected_user_partitions = [ @@ -170,13 +183,8 @@ def test_children_content(self): "name": "Enrollment Track Groups", "scheme": "enrollment_track", "groups": [ - { - "id": 1, - "name": "Audit", - "selected": False, - "deleted": False - } - ] + {"id": 1, "name": "Audit", "selected": False, "deleted": False} + ], } ] @@ -190,16 +198,20 @@ def test_children_content(self): "actions": { "can_manage_tags": True, }, + "validation_messages": [], + "render_error": "", }, { "name": self.html_unit_second.display_name_with_default, "block_id": str(self.html_unit_second.location), "block_type": self.html_unit_second.location.block_type, - "user_partition_info": expected_user_partition_info, - "user_partitions": expected_user_partitions, "actions": { "can_manage_tags": True, }, + "user_partition_info": expected_user_partition_info, + "user_partitions": expected_user_partitions, + "validation_messages": [], + "render_error": "", }, ] self.assertEqual(response.data["children"], expected_response) @@ -224,3 +236,42 @@ def test_actions_with_turned_off_taxonomy_flag(self): response = self.client.get(url) for children in response.data["children"]: self.assertFalse(children["actions"]["can_manage_tags"]) + + def test_validation_errors(self): + """ + Check that child has an error. + """ + self.course.user_partitions = [ + UserPartition( + 0, + "first_partition", + "Test Partition", + [Group("0", "alpha"), Group("1", "beta")], + ), + ] + self.store.update_item(self.course, self.user.id) + + user_partition = self.course.user_partitions[0] + vertical = self.store.get_item(self.vertical.location) + html_unit_first = self.store.get_item(self.html_unit_first.location) + + group_first = user_partition.groups[0] + group_second = user_partition.groups[1] + + # Set access settings so html will contradict vertical + self.set_group_access(vertical, {user_partition.id: [group_second.id]}) + self.set_group_access(html_unit_first, {user_partition.id: [group_first.id]}) + + # update vertical/html + vertical = self.store.get_item(self.vertical.location) + html_unit_first = self.store.get_item(self.html_unit_first.location) + + url = self.get_reverse_url(self.vertical.location) + response = self.client.get(url) + children_response = response.data["children"] + + # Verify that html_unit_first access settings contradict its parent's access settings. + self.assertEqual(children_response[0]["validation_messages"][0]["type"], ValidationMessage.ERROR) + + # Verify that html_unit_second has no validation messages. + self.assertFalse(children_response[1]["validation_messages"]) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py index a084886cf344..f41ddc73f1eb 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py @@ -10,6 +10,8 @@ get_container_handler_context, get_user_partition_info, get_visibility_partition_info, + get_xblock_validation_messages, + get_xblock_render_error, ) from cms.djangoapps.contentstore.views.component import _get_item_in_course from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import get_xblock @@ -194,7 +196,9 @@ def get(self, request: Request, usage_key_string: str): "user_partitions": {} "actions": { "can_manage_tags": true, - } + }, + "has_validation_error": false, + "validation_errors": [], }, { "name": "Video", @@ -205,6 +209,8 @@ def get(self, request: Request, usage_key_string: str): "actions": { "can_manage_tags": true, } + "validation_messages": [], + "render_error": "", }, { "name": "Text", @@ -214,7 +220,14 @@ def get(self, request: Request, usage_key_string: str): "user_partitions": {}, "actions": { "can_manage_tags": true, - } + }, + "validation_messages": [ + { + "text": "This component's access settings contradict its parent's access settings.", + "type": "error" + } + ], + "render_error": "Unterminated control keyword: 'if' in file '../problem.html'", }, ], "is_published": false @@ -232,12 +245,17 @@ def get(self, request: Request, usage_key_string: str): child_info = modulestore().get_item(child) user_partition_info = get_visibility_partition_info(child_info, course=course) user_partitions = get_user_partition_info(child_info, course=course) + validation_messages = get_xblock_validation_messages(child_info) + render_error = get_xblock_render_error(request, child_info) + children.append({ "name": child_info.display_name_with_default, "block_id": child_info.location, "block_type": child_info.location.block_type, "user_partition_info": user_partition_info, "user_partitions": user_partitions, + "validation_messages": validation_messages, + "render_error": render_error, }) is_published = not modulestore().has_changes(current_xblock) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 0b33e88a2a65..ba9183ecaac3 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -2234,3 +2234,60 @@ def send_course_update_notification(course_key, content, user): audience_filters={}, ) COURSE_NOTIFICATION_REQUESTED.send_event(course_notification_data=notification_data) + + +def get_xblock_validation_messages(xblock): + """ + Retrieves validation messages for a given xblock. + + Args: + xblock: The xblock object to validate. + + Returns: + list: A list of validation error messages. + """ + validation_json = xblock.validate().to_json() + return validation_json['messages'] + + +def get_xblock_render_error(request, xblock): + """ + Checks if there are any rendering errors for a given block and return these. + + Args: + request: WSGI request object + xblock: The xblock object to rendering. + + Returns: + str: Error message which happened while rendering of xblock. + """ + from cms.djangoapps.contentstore.views.preview import _load_preview_block + from xmodule.studio_editable import has_author_view + from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW + + def get_xblock_render_context(request, block): + """ + Return a dict of the data needs for render of each block. + """ + can_edit = has_studio_write_access(request.user, block.usage_key.course_key) + + return { + "is_unit_page": False, + "can_edit": can_edit, + "root_xblock": xblock, + "reorderable_items": set(), + "paging": None, + "force_render": None, + "item_url": "/container/{block.location}", + "tags_count_map": {}, + } + + try: + block = _load_preview_block(request, xblock) + preview_view = AUTHOR_VIEW if has_author_view(block) else STUDENT_VIEW + render_context = get_xblock_render_context(request, block) + block.render(preview_view, render_context) + except Exception as exc: # pylint: disable=broad-except + return str(exc) + + return ""