From 37ffda99d72cca5ba7b2d9f89464bfbd7a2dcf9f Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 31 Jan 2025 13:36:27 -0500 Subject: [PATCH 1/6] fix: Render Word cloud and conditional block editor - The xmodule-type to render is MetadataOnlyEditingDescriptor - The xmodule type `MetadataOnlyEditingDescriptor` renders a `
` with the block metadata in the `data-metadata` attribute. But is necessary to call `XBlockEditorView.xblockReady()` to run the scripts to build the editor using the metadata. - To call XBlockEditorView.xblockReady() we need a specific require.config --- cms/static/js/views/xblock_editor.js | 2 +- common/templates/xblock_v2/xblock_iframe.html | 58 +++++++++++++++---- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/cms/static/js/views/xblock_editor.js b/cms/static/js/views/xblock_editor.js index 40ab22d022fc..52d08dc76fb4 100644 --- a/cms/static/js/views/xblock_editor.js +++ b/cms/static/js/views/xblock_editor.js @@ -78,7 +78,7 @@ function($, _, gettext, BaseView, XBlockView, MetadataView, MetadataCollection) el: metadataEditor, collection: new MetadataCollection(models) }); - if (xblock.setMetadataEditor) { + if (xblock && xblock.setMetadataEditor) { xblock.setMetadataEditor(metadataView); } } diff --git a/common/templates/xblock_v2/xblock_iframe.html b/common/templates/xblock_v2/xblock_iframe.html index 57ff4684ddbd..824896dd4f3c 100644 --- a/common/templates/xblock_v2/xblock_iframe.html +++ b/common/templates/xblock_v2/xblock_iframe.html @@ -35,23 +35,43 @@ @@ -269,6 +289,20 @@ // const passElement = isStudioView && (window as any).$ ? (window as any).$(element) : element; const blockJS = new InitFunction(runtime, element, data) || {}; blockJS.element = element; + + if (data['xmodule-type'] == 'MetadataOnlyEditingDescriptor') { + // The xmodule type `MetadataOnlyEditingDescriptor` renders a `
` with + // the block metadata in the `data-metadata` attribute. But is necessary + // to call `XBlockEditorView.xblockReady()` to run the scripts to build the + // editor using the metadata. + require(['{{ cms_root_url }}/static/studio/js/views/xblock_editor.js'], function(XBlockEditorView) { + var editorView = new XBlockEditorView({ + el: element + }); + editorView.xblockReady(blockJS); + }); + } + callback(blockJS); } else { const blockJS = { element }; From b8a77f6c29db2c44cf95c390bc8965e444eff542 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 31 Jan 2025 16:07:24 -0500 Subject: [PATCH 2/6] fix: Adding save and cancel button --- common/templates/xblock_v2/xblock_iframe.html | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/common/templates/xblock_v2/xblock_iframe.html b/common/templates/xblock_v2/xblock_iframe.html index 824896dd4f3c..652c4e0edb2c 100644 --- a/common/templates/xblock_v2/xblock_iframe.html +++ b/common/templates/xblock_v2/xblock_iframe.html @@ -290,16 +290,66 @@ const blockJS = new InitFunction(runtime, element, data) || {}; blockJS.element = element; - if (data['xmodule-type'] == 'MetadataOnlyEditingDescriptor') { + if (['MetadataOnlyEditingDescriptor', 'SequenceDescriptor'].includes(data['xmodule-type'])) { // The xmodule type `MetadataOnlyEditingDescriptor` renders a `
` with // the block metadata in the `data-metadata` attribute. But is necessary // to call `XBlockEditorView.xblockReady()` to run the scripts to build the // editor using the metadata. require(['{{ cms_root_url }}/static/studio/js/views/xblock_editor.js'], function(XBlockEditorView) { var editorView = new XBlockEditorView({ - el: element + el: element }); + // To render block using metadata editorView.xblockReady(blockJS); + + // Adding cancel and save buttons + var xblockActions = ` +
+ +
+ `; + element.innerHTML += xblockActions; + + // Adding cancel functionality + $('.cancel-button', element).bind('click', function() { + runtime.notify('cancel', {}); + event.preventDefault(); + }); + + // Adding save functionality + $('.save-button', element).bind('click', function() { + event.preventDefault(); + var error_message_div = $('.xblock-editor-error-message', element); + const modifiedData = editorView.getChangedMetadata(); + + error_message_div.html(); + error_message_div.css('display', 'none'); + + var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); + + runtime.notify('save', {state: 'start', message: gettext("Saving")}); + + $.post(handlerUrl, JSON.stringify(modifiedData)).done(function(response) { + if (response.result === 'success') { + runtime.notify('save', {state: 'end'}) + window.location.reload(false); + } else { + runtime.notify('error', { + 'title': 'Error saving changes', + 'message': response.message, + }); + error_message_div.html('Error: '+response.message); + error_message_div.css('display', 'block'); + } + }); + }); }); } From 8fd986bb6d95ac630c47db994cdbe93eb5ba0c16 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 31 Jan 2025 19:47:33 -0500 Subject: [PATCH 3/6] fix: save with studio_submit of conditional_block and word_cloud_block --- common/templates/xblock_v2/xblock_iframe.html | 116 ++++++++++-------- xmodule/conditional_block.py | 31 ++++- xmodule/word_cloud_block.py | 20 +++ 3 files changed, 113 insertions(+), 54 deletions(-) diff --git a/common/templates/xblock_v2/xblock_iframe.html b/common/templates/xblock_v2/xblock_iframe.html index 652c4e0edb2c..575fdbd04b4b 100644 --- a/common/templates/xblock_v2/xblock_iframe.html +++ b/common/templates/xblock_v2/xblock_iframe.html @@ -291,66 +291,78 @@ blockJS.element = element; if (['MetadataOnlyEditingDescriptor', 'SequenceDescriptor'].includes(data['xmodule-type'])) { - // The xmodule type `MetadataOnlyEditingDescriptor` renders a `
` with - // the block metadata in the `data-metadata` attribute. But is necessary - // to call `XBlockEditorView.xblockReady()` to run the scripts to build the - // editor using the metadata. - require(['{{ cms_root_url }}/static/studio/js/views/xblock_editor.js'], function(XBlockEditorView) { - var editorView = new XBlockEditorView({ - el: element - }); - // To render block using metadata - editorView.xblockReady(blockJS); + // The xmodule type `MetadataOnlyEditingDescriptor` renders a `
` with + // the block metadata in the `data-metadata` attribute. But is necessary + // to call `XBlockEditorView.xblockReady()` to run the scripts to build the + // editor using the metadata. + require(['{{ cms_root_url }}/static/studio/js/views/xblock_editor.js'], function(XBlockEditorView) { + var editorView = new XBlockEditorView({ + el: element, + xblock: blockJS, + }); + // To render block using metadata + editorView.xblockReady(blockJS); - // Adding cancel and save buttons - var xblockActions = ` -
- -
- `; - element.innerHTML += xblockActions; + // Adding cancel and save buttons + var xblockActions = ` +
+ +
+ `; + element.innerHTML += xblockActions; - // Adding cancel functionality - $('.cancel-button', element).bind('click', function() { - runtime.notify('cancel', {}); - event.preventDefault(); - }); + const views = editorView.getMetadataEditor().views; + Object.values(views).forEach(view => { + const uniqueId = view.uniqueId; + const input = element.querySelector(`#${uniqueId}`); + if (input) { + input.addEventListener("input", function(event) { + view.model.setValue(event.target.value); + }); + } + }); - // Adding save functionality - $('.save-button', element).bind('click', function() { - event.preventDefault(); - var error_message_div = $('.xblock-editor-error-message', element); - const modifiedData = editorView.getChangedMetadata(); + // Adding cancel functionality + $('.cancel-button', element).bind('click', function() { + runtime.notify('cancel', {}); + event.preventDefault(); + }); - error_message_div.html(); - error_message_div.css('display', 'none'); + // Adding save functionality + $('.save-button', element).bind('click', function() { + //event.preventDefault(); + var error_message_div = $('.xblock-editor-error-message', element); + const modifiedData = editorView.getChangedMetadata(); - var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); + error_message_div.html(); + error_message_div.css('display', 'none'); - runtime.notify('save', {state: 'start', message: gettext("Saving")}); + var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); - $.post(handlerUrl, JSON.stringify(modifiedData)).done(function(response) { - if (response.result === 'success') { - runtime.notify('save', {state: 'end'}) - window.location.reload(false); - } else { - runtime.notify('error', { - 'title': 'Error saving changes', - 'message': response.message, - }); - error_message_div.html('Error: '+response.message); - error_message_div.css('display', 'block'); - } - }); + runtime.notify('save', {state: 'start', message: gettext("Saving")}); + + $.post(handlerUrl, JSON.stringify(modifiedData)).done(function(response) { + if (response.result === 'success') { + runtime.notify('save', {state: 'end'}) + window.location.reload(false); + } else { + runtime.notify('error', { + 'title': 'Error saving changes', + 'message': response.message, + }); + error_message_div.html('Error: '+response.message); + error_message_div.css('display', 'block'); + } + }); + }); }); - }); } callback(blockJS); diff --git a/xmodule/conditional_block.py b/xmodule/conditional_block.py index 78a5b8c1c1c9..7f519b9ef1a0 100644 --- a/xmodule/conditional_block.py +++ b/xmodule/conditional_block.py @@ -131,8 +131,6 @@ class ConditionalBlock( default=_('You must complete {link} before you can access this unit.') ) - has_children = True - _tag_name = 'conditional' resources_dir = None @@ -382,6 +380,28 @@ def validate(self): validation.summary = conditional_validation.messages[0] return validation + @XBlock.json_handler + def studio_submit(self, submissions, suffix=''): # pylint: disable=unused-argument + """ + Change the settings for this XBlock given by the Studio user. + """ + if 'display_name' in submissions: + self.display_name = submissions['display_name'] + if 'show_tag_list' in submissions: + self.show_tag_list = submissions['show_tag_list'] + if 'sources_list' in submissions: + self.sources_list = submissions['sources_list'] + if 'conditional_attr' in submissions: + self.conditional_attr = submissions['conditional_attr'] + if 'conditional_value' in submissions: + self.conditional_value = submissions['conditional_value'] + if 'conditional_message' in submissions: + self.conditional_message = submissions['conditional_message'] + + return { + 'result': 'success', + } + @property def non_editable_metadata_fields(self): non_editable_fields = super().non_editable_metadata_fields @@ -390,3 +410,10 @@ def non_editable_metadata_fields(self): ConditionalBlock.show_tag_list, ]) return non_editable_fields + + @property + def has_children(self): + """ + Determines whether this block has children + """ + return bool(self.sources_list) diff --git a/xmodule/word_cloud_block.py b/xmodule/word_cloud_block.py index 37e82400df78..f83cda8c0c83 100644 --- a/xmodule/word_cloud_block.py +++ b/xmodule/word_cloud_block.py @@ -315,6 +315,26 @@ def index_dictionary(self): return xblock_body + @XBlock.json_handler + def studio_submit(self, submissions, suffix=''): # pylint: disable=unused-argument + """ + Change the settings for this XBlock given by the Studio user + """ + if 'display_name' in submissions: + self.display_name = submissions['display_name'] + if 'instructions' in submissions: + self.instructions = submissions['instructions'] + if 'num_inputs' in submissions: + self.num_inputs = submissions['num_inputs'] + if 'num_top_words' in submissions: + self.num_top_words = submissions['num_top_words'] + if 'display_student_percents' in submissions: + self.display_student_percents = submissions['display_student_percents'] == 'True' + + return { + 'result': 'success', + } + WordCloudBlock = ( _ExtractedWordCloudBlock if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK From 0d3f52322d0802a27dcb3d54c0e194bb4909b765 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 31 Jan 2025 21:46:32 -0500 Subject: [PATCH 4/6] test: Tests for studio_submit of conditional and word cloud --- common/templates/xblock_v2/xblock_iframe.html | 2 +- xmodule/conditional_block.py | 9 ++----- xmodule/tests/test_conditional.py | 27 +++++++++++++++++++ xmodule/tests/test_word_cloud.py | 27 +++++++++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/common/templates/xblock_v2/xblock_iframe.html b/common/templates/xblock_v2/xblock_iframe.html index 575fdbd04b4b..add3c46bce55 100644 --- a/common/templates/xblock_v2/xblock_iframe.html +++ b/common/templates/xblock_v2/xblock_iframe.html @@ -291,7 +291,7 @@ blockJS.element = element; if (['MetadataOnlyEditingDescriptor', 'SequenceDescriptor'].includes(data['xmodule-type'])) { - // The xmodule type `MetadataOnlyEditingDescriptor` renders a `
` with + // The xmodule type `MetadataOnlyEditingDescriptor` and `SequenceDescriptor` renders a `
` with // the block metadata in the `data-metadata` attribute. But is necessary // to call `XBlockEditorView.xblockReady()` to run the scripts to build the // editor using the metadata. diff --git a/xmodule/conditional_block.py b/xmodule/conditional_block.py index 7f519b9ef1a0..077d9bddaa18 100644 --- a/xmodule/conditional_block.py +++ b/xmodule/conditional_block.py @@ -131,6 +131,8 @@ class ConditionalBlock( default=_('You must complete {link} before you can access this unit.') ) + has_children = True + _tag_name = 'conditional' resources_dir = None @@ -410,10 +412,3 @@ def non_editable_metadata_fields(self): ConditionalBlock.show_tag_list, ]) return non_editable_fields - - @property - def has_children(self): - """ - Determines whether this block has children - """ - return bool(self.sources_list) diff --git a/xmodule/tests/test_conditional.py b/xmodule/tests/test_conditional.py index d1fcde0c7561..80a6d259f2f0 100644 --- a/xmodule/tests/test_conditional.py +++ b/xmodule/tests/test_conditional.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch from django.conf import settings +from webob import Request from fs.memoryfs import MemoryFS from lxml import etree from opaque_keys.edx.keys import CourseKey @@ -402,3 +403,29 @@ def test_validation_messages(self): assert validation.summary.type == StudioValidationMessage.NOT_CONFIGURED assert validation.summary.action_class == 'edit-button' assert validation.summary.action_label == 'Configure list of sources' + + def test_studio_submit_handler(self): + """ + Test studio_submint handler + """ + TEST_SUBMIT_DATA = { + 'display_name': "New Conditional", + 'show_tag_list': ["1", "2"], + 'sources_list': ["1", "2"], + 'conditional_attr': "test", + 'conditional_value': 'value', + 'conditional_message': 'message', + } + body = json.dumps(TEST_SUBMIT_DATA) + request = Request.blank('/') + request.method = 'POST' + request.body = body.encode('utf-8') + res = self.conditional.handle('studio_submit', request) + assert json.loads(res.body.decode('utf8')) == {'result': 'success'} + + assert self.conditional.display_name == TEST_SUBMIT_DATA['display_name'] + assert self.conditional.show_tag_list == TEST_SUBMIT_DATA['show_tag_list'] + assert self.conditional.sources_list == TEST_SUBMIT_DATA['sources_list'] + assert self.conditional.conditional_attr == TEST_SUBMIT_DATA['conditional_attr'] + assert self.conditional.conditional_value == TEST_SUBMIT_DATA['conditional_value'] + assert self.conditional.conditional_message == TEST_SUBMIT_DATA['conditional_message'] diff --git a/xmodule/tests/test_word_cloud.py b/xmodule/tests/test_word_cloud.py index 6c928465262b..9fbd02a612db 100644 --- a/xmodule/tests/test_word_cloud.py +++ b/xmodule/tests/test_word_cloud.py @@ -6,6 +6,7 @@ from django.test import TestCase from fs.memoryfs import MemoryFS from lxml import etree +from webob import Request from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from webob.multidict import MultiDict from xblock.field_data import DictFieldData @@ -115,3 +116,29 @@ def test_indexibility(self): {'content_type': 'Word Cloud', 'content': {'display_name': 'Word Cloud Block', 'instructions': 'Enter some random words that comes to your mind'}} + + def test_studio_submit_handler(self): + """ + Test studio_submint handler + """ + TEST_SUBMIT_DATA = { + 'display_name': "New Word Cloud", + 'instructions': "This is a Test", + 'num_inputs': 5, + 'num_top_words': 10, + 'display_student_percents': 'False', + } + module_system = get_test_system() + block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock()) + body = json.dumps(TEST_SUBMIT_DATA) + request = Request.blank('/') + request.method = 'POST' + request.body = body.encode('utf-8') + res = block.handle('studio_submit', request) + assert json.loads(res.body.decode('utf8')) == {'result': 'success'} + + assert block.display_name == TEST_SUBMIT_DATA['display_name'] + assert block.instructions == TEST_SUBMIT_DATA['instructions'] + assert block.num_inputs == TEST_SUBMIT_DATA['num_inputs'] + assert block.num_top_words == TEST_SUBMIT_DATA['num_top_words'] + assert block.display_student_percents == (TEST_SUBMIT_DATA['display_student_percents'] == "True") From 0be1cfee1fbb0654f98bdbe962a6aee58558f764 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 6 Feb 2025 14:16:39 -0500 Subject: [PATCH 5/6] revert: Delete studio_submit of conditional block. It is not supported --- xmodule/conditional_block.py | 22 ---------------------- xmodule/tests/test_conditional.py | 26 -------------------------- 2 files changed, 48 deletions(-) diff --git a/xmodule/conditional_block.py b/xmodule/conditional_block.py index 077d9bddaa18..78a5b8c1c1c9 100644 --- a/xmodule/conditional_block.py +++ b/xmodule/conditional_block.py @@ -382,28 +382,6 @@ def validate(self): validation.summary = conditional_validation.messages[0] return validation - @XBlock.json_handler - def studio_submit(self, submissions, suffix=''): # pylint: disable=unused-argument - """ - Change the settings for this XBlock given by the Studio user. - """ - if 'display_name' in submissions: - self.display_name = submissions['display_name'] - if 'show_tag_list' in submissions: - self.show_tag_list = submissions['show_tag_list'] - if 'sources_list' in submissions: - self.sources_list = submissions['sources_list'] - if 'conditional_attr' in submissions: - self.conditional_attr = submissions['conditional_attr'] - if 'conditional_value' in submissions: - self.conditional_value = submissions['conditional_value'] - if 'conditional_message' in submissions: - self.conditional_message = submissions['conditional_message'] - - return { - 'result': 'success', - } - @property def non_editable_metadata_fields(self): non_editable_fields = super().non_editable_metadata_fields diff --git a/xmodule/tests/test_conditional.py b/xmodule/tests/test_conditional.py index 80a6d259f2f0..ba6cb6593a2c 100644 --- a/xmodule/tests/test_conditional.py +++ b/xmodule/tests/test_conditional.py @@ -403,29 +403,3 @@ def test_validation_messages(self): assert validation.summary.type == StudioValidationMessage.NOT_CONFIGURED assert validation.summary.action_class == 'edit-button' assert validation.summary.action_label == 'Configure list of sources' - - def test_studio_submit_handler(self): - """ - Test studio_submint handler - """ - TEST_SUBMIT_DATA = { - 'display_name': "New Conditional", - 'show_tag_list': ["1", "2"], - 'sources_list': ["1", "2"], - 'conditional_attr': "test", - 'conditional_value': 'value', - 'conditional_message': 'message', - } - body = json.dumps(TEST_SUBMIT_DATA) - request = Request.blank('/') - request.method = 'POST' - request.body = body.encode('utf-8') - res = self.conditional.handle('studio_submit', request) - assert json.loads(res.body.decode('utf8')) == {'result': 'success'} - - assert self.conditional.display_name == TEST_SUBMIT_DATA['display_name'] - assert self.conditional.show_tag_list == TEST_SUBMIT_DATA['show_tag_list'] - assert self.conditional.sources_list == TEST_SUBMIT_DATA['sources_list'] - assert self.conditional.conditional_attr == TEST_SUBMIT_DATA['conditional_attr'] - assert self.conditional.conditional_value == TEST_SUBMIT_DATA['conditional_value'] - assert self.conditional.conditional_message == TEST_SUBMIT_DATA['conditional_message'] From 4d018b457866bb9bc12957400d3637fc2161d3ff Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 6 Feb 2025 14:22:41 -0500 Subject: [PATCH 6/6] style: Fix lint --- xmodule/tests/test_conditional.py | 1 - 1 file changed, 1 deletion(-) diff --git a/xmodule/tests/test_conditional.py b/xmodule/tests/test_conditional.py index ba6cb6593a2c..d1fcde0c7561 100644 --- a/xmodule/tests/test_conditional.py +++ b/xmodule/tests/test_conditional.py @@ -5,7 +5,6 @@ from unittest.mock import Mock, patch from django.conf import settings -from webob import Request from fs.memoryfs import MemoryFS from lxml import etree from opaque_keys.edx.keys import CourseKey