fix: Render Word cloud block editor in libraries [FC-0076] (#36199)
* fix: Render Word cloud and conditional block editor - The xmodule-type to render is MetadataOnlyEditingDescriptor - The xmodule type `MetadataOnlyEditingDescriptor` renders a `<div>` 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 * fix: Adding save and cancel button * fix: save with studio_submit of conditional_block and word_cloud_block * test: Tests for studio_submit of conditional and word cloud * revert: Delete studio_submit of conditional block. It is not supported * style: Fix lint --------- Co-authored-by: Navin Karkera <navin@opencraft.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,23 +35,43 @@
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/common/js/vendor/require.js"></script>
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/js/RequireJS-namespace-undefine.js"></script>
|
||||
<script>
|
||||
// The minimal RequireJS configuration required for common LMS building XBlock types to work:
|
||||
// The minimal RequireJS configuration required for common LMS and CMS building XBlock types to work:
|
||||
require = require || RequireJS.require;
|
||||
define = define || RequireJS.define;
|
||||
(function (require, define) {
|
||||
require.config({
|
||||
baseUrl: "{{ lms_root_url }}/static/",
|
||||
paths: {
|
||||
accessibility: 'js/src/accessibility_tools',
|
||||
draggabilly: 'js/vendor/draggabilly',
|
||||
hls: 'common/js/vendor/hls',
|
||||
moment: 'common/js/vendor/moment-with-locales',
|
||||
HtmlUtils: 'edx-ui-toolkit/js/utils/html-utils',
|
||||
},
|
||||
});
|
||||
if ('{{ view_name | safe }}' === 'studio_view') {
|
||||
// Call `require-config.js` of the CMS
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = "{{ cms_root_url }}/static/studio/cms/js/require-config.js";
|
||||
document.head.appendChild(script);
|
||||
|
||||
require.config({
|
||||
baseUrl: "{{ cms_root_url }}/static/studio",
|
||||
paths: {
|
||||
accessibility: 'js/src/accessibility_tools',
|
||||
draggabilly: 'js/vendor/draggabilly',
|
||||
hls: 'common/js/vendor/hls',
|
||||
moment: 'common/js/vendor/moment-with-locales',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
require.config({
|
||||
baseUrl: "{{ lms_root_url }}/static/",
|
||||
paths: {
|
||||
accessibility: 'js/src/accessibility_tools',
|
||||
draggabilly: 'js/vendor/draggabilly',
|
||||
hls: 'common/js/vendor/hls',
|
||||
moment: 'common/js/vendor/moment-with-locales',
|
||||
HtmlUtils: 'edx-ui-toolkit/js/utils/html-utils',
|
||||
},
|
||||
});
|
||||
}
|
||||
define('gettext', [], function() { return window.gettext; });
|
||||
define('jquery', [], function() { return window.jQuery; });
|
||||
define('jquery-migrate', [], function() { return window.jQuery; });
|
||||
define('underscore', [], function() { return window._; });
|
||||
}).call(this, require || RequireJS.require, define || RequireJS.define);
|
||||
}).call(this, require, define);
|
||||
</script>
|
||||
<!-- edX HTML Utils requires GlobalLoader -->
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/edx-ui-toolkit/js/utils/global-loader.js"></script>
|
||||
@@ -269,6 +289,82 @@
|
||||
// const passElement = isStudioView && (window as any).$ ? (window as any).$(element) : element;
|
||||
const blockJS = new InitFunction(runtime, element, data) || {};
|
||||
blockJS.element = element;
|
||||
|
||||
if (['MetadataOnlyEditingDescriptor', 'SequenceDescriptor'].includes(data['xmodule-type'])) {
|
||||
// The xmodule type `MetadataOnlyEditingDescriptor` and `SequenceDescriptor` renders a `<div>` 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 = `
|
||||
<div class="xblock-actions">
|
||||
<ul>
|
||||
<li class="action-item">
|
||||
<input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;">
|
||||
</li>
|
||||
<li class="action-item">
|
||||
<a href="#" class="button cancel-button">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
element.innerHTML += xblockActions;
|
||||
|
||||
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 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
callback(blockJS);
|
||||
} else {
|
||||
const blockJS = { element };
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import Mock
|
||||
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 @@ class WordCloudBlockTest(TestCase):
|
||||
{'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")
|
||||
|
||||
@@ -315,6 +315,26 @@ class _BuiltInWordCloudBlock( # pylint: disable=abstract-method
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user