feat: display error notifications on the Unit page (#34450)
This commit is contained in:
@@ -12,6 +12,15 @@ from cms.djangoapps.contentstore.helpers import (
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -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 @@ class BaseXBlockContainer(CourseTestCase):
|
||||
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 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
|
||||
expected_user_partition_info = {
|
||||
"selectable_partitions": [],
|
||||
"selected_partition_index": -1,
|
||||
"selected_groups_label": ""
|
||||
"selected_groups_label": "",
|
||||
}
|
||||
|
||||
expected_user_partitions = [
|
||||
@@ -170,13 +183,8 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
|
||||
"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 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
|
||||
"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 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
|
||||
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"])
|
||||
|
||||
@@ -10,6 +10,8 @@ from cms.djangoapps.contentstore.utils import (
|
||||
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 @@ class VerticalContainerView(APIView, ContainerHandlerMixin):
|
||||
"user_partitions": {}
|
||||
"actions": {
|
||||
"can_manage_tags": true,
|
||||
}
|
||||
},
|
||||
"has_validation_error": false,
|
||||
"validation_errors": [],
|
||||
},
|
||||
{
|
||||
"name": "Video",
|
||||
@@ -205,6 +209,8 @@ class VerticalContainerView(APIView, ContainerHandlerMixin):
|
||||
"actions": {
|
||||
"can_manage_tags": true,
|
||||
}
|
||||
"validation_messages": [],
|
||||
"render_error": "",
|
||||
},
|
||||
{
|
||||
"name": "Text",
|
||||
@@ -214,7 +220,14 @@ class VerticalContainerView(APIView, ContainerHandlerMixin):
|
||||
"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 @@ class VerticalContainerView(APIView, ContainerHandlerMixin):
|
||||
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)
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user