Skip to content

Commit d29ff63

Browse files
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 <[email protected]>
1 parent b7a2ffa commit d29ff63

File tree

4 files changed

+156
-13
lines changed

4 files changed

+156
-13
lines changed

cms/static/js/views/xblock_editor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function($, _, gettext, BaseView, XBlockView, MetadataView, MetadataCollection)
7878
el: metadataEditor,
7979
collection: new MetadataCollection(models)
8080
});
81-
if (xblock.setMetadataEditor) {
81+
if (xblock && xblock.setMetadataEditor) {
8282
xblock.setMetadataEditor(metadataView);
8383
}
8484
}

common/templates/xblock_v2/xblock_iframe.html

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,43 @@
3535
<script type="text/javascript" src="{{ lms_root_url }}/static/common/js/vendor/require.js"></script>
3636
<script type="text/javascript" src="{{ lms_root_url }}/static/js/RequireJS-namespace-undefine.js"></script>
3737
<script>
38-
// The minimal RequireJS configuration required for common LMS building XBlock types to work:
38+
// The minimal RequireJS configuration required for common LMS and CMS building XBlock types to work:
39+
require = require || RequireJS.require;
40+
define = define || RequireJS.define;
3941
(function (require, define) {
40-
require.config({
41-
baseUrl: "{{ lms_root_url }}/static/",
42-
paths: {
43-
accessibility: 'js/src/accessibility_tools',
44-
draggabilly: 'js/vendor/draggabilly',
45-
hls: 'common/js/vendor/hls',
46-
moment: 'common/js/vendor/moment-with-locales',
47-
HtmlUtils: 'edx-ui-toolkit/js/utils/html-utils',
48-
},
49-
});
42+
if ('{{ view_name | safe }}' === 'studio_view') {
43+
// Call `require-config.js` of the CMS
44+
var script = document.createElement('script');
45+
script.type = 'text/javascript';
46+
script.src = "{{ cms_root_url }}/static/studio/cms/js/require-config.js";
47+
document.head.appendChild(script);
48+
49+
require.config({
50+
baseUrl: "{{ cms_root_url }}/static/studio",
51+
paths: {
52+
accessibility: 'js/src/accessibility_tools',
53+
draggabilly: 'js/vendor/draggabilly',
54+
hls: 'common/js/vendor/hls',
55+
moment: 'common/js/vendor/moment-with-locales',
56+
},
57+
});
58+
} else {
59+
require.config({
60+
baseUrl: "{{ lms_root_url }}/static/",
61+
paths: {
62+
accessibility: 'js/src/accessibility_tools',
63+
draggabilly: 'js/vendor/draggabilly',
64+
hls: 'common/js/vendor/hls',
65+
moment: 'common/js/vendor/moment-with-locales',
66+
HtmlUtils: 'edx-ui-toolkit/js/utils/html-utils',
67+
},
68+
});
69+
}
5070
define('gettext', [], function() { return window.gettext; });
5171
define('jquery', [], function() { return window.jQuery; });
5272
define('jquery-migrate', [], function() { return window.jQuery; });
5373
define('underscore', [], function() { return window._; });
54-
}).call(this, require || RequireJS.require, define || RequireJS.define);
74+
}).call(this, require, define);
5575
</script>
5676
<!-- edX HTML Utils requires GlobalLoader -->
5777
<script type="text/javascript" src="{{ lms_root_url }}/static/edx-ui-toolkit/js/utils/global-loader.js"></script>
@@ -269,6 +289,82 @@
269289
// const passElement = isStudioView && (window as any).$ ? (window as any).$(element) : element;
270290
const blockJS = new InitFunction(runtime, element, data) || {};
271291
blockJS.element = element;
292+
293+
if (['MetadataOnlyEditingDescriptor', 'SequenceDescriptor'].includes(data['xmodule-type'])) {
294+
// The xmodule type `MetadataOnlyEditingDescriptor` and `SequenceDescriptor` renders a `<div>` with
295+
// the block metadata in the `data-metadata` attribute. But is necessary
296+
// to call `XBlockEditorView.xblockReady()` to run the scripts to build the
297+
// editor using the metadata.
298+
require(['{{ cms_root_url }}/static/studio/js/views/xblock_editor.js'], function(XBlockEditorView) {
299+
var editorView = new XBlockEditorView({
300+
el: element,
301+
xblock: blockJS,
302+
});
303+
// To render block using metadata
304+
editorView.xblockReady(blockJS);
305+
306+
// Adding cancel and save buttons
307+
var xblockActions = `
308+
<div class="xblock-actions">
309+
<ul>
310+
<li class="action-item">
311+
<input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;">
312+
</li>
313+
<li class="action-item">
314+
<a href="#" class="button cancel-button">Cancel</a>
315+
</li>
316+
</ul>
317+
</div>
318+
`;
319+
element.innerHTML += xblockActions;
320+
321+
const views = editorView.getMetadataEditor().views;
322+
Object.values(views).forEach(view => {
323+
const uniqueId = view.uniqueId;
324+
const input = element.querySelector(`#${uniqueId}`);
325+
if (input) {
326+
input.addEventListener("input", function(event) {
327+
view.model.setValue(event.target.value);
328+
});
329+
}
330+
});
331+
332+
// Adding cancel functionality
333+
$('.cancel-button', element).bind('click', function() {
334+
runtime.notify('cancel', {});
335+
event.preventDefault();
336+
});
337+
338+
// Adding save functionality
339+
$('.save-button', element).bind('click', function() {
340+
//event.preventDefault();
341+
var error_message_div = $('.xblock-editor-error-message', element);
342+
const modifiedData = editorView.getChangedMetadata();
343+
344+
error_message_div.html();
345+
error_message_div.css('display', 'none');
346+
347+
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
348+
349+
runtime.notify('save', {state: 'start', message: gettext("Saving")});
350+
351+
$.post(handlerUrl, JSON.stringify(modifiedData)).done(function(response) {
352+
if (response.result === 'success') {
353+
runtime.notify('save', {state: 'end'})
354+
window.location.reload(false);
355+
} else {
356+
runtime.notify('error', {
357+
'title': 'Error saving changes',
358+
'message': response.message,
359+
});
360+
error_message_div.html('Error: '+response.message);
361+
error_message_div.css('display', 'block');
362+
}
363+
});
364+
});
365+
});
366+
}
367+
272368
callback(blockJS);
273369
} else {
274370
const blockJS = { element };

xmodule/tests/test_word_cloud.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.test import TestCase
77
from fs.memoryfs import MemoryFS
88
from lxml import etree
9+
from webob import Request
910
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
1011
from webob.multidict import MultiDict
1112
from xblock.field_data import DictFieldData
@@ -115,3 +116,29 @@ def test_indexibility(self):
115116
{'content_type': 'Word Cloud',
116117
'content': {'display_name': 'Word Cloud Block',
117118
'instructions': 'Enter some random words that comes to your mind'}}
119+
120+
def test_studio_submit_handler(self):
121+
"""
122+
Test studio_submint handler
123+
"""
124+
TEST_SUBMIT_DATA = {
125+
'display_name': "New Word Cloud",
126+
'instructions': "This is a Test",
127+
'num_inputs': 5,
128+
'num_top_words': 10,
129+
'display_student_percents': 'False',
130+
}
131+
module_system = get_test_system()
132+
block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock())
133+
body = json.dumps(TEST_SUBMIT_DATA)
134+
request = Request.blank('/')
135+
request.method = 'POST'
136+
request.body = body.encode('utf-8')
137+
res = block.handle('studio_submit', request)
138+
assert json.loads(res.body.decode('utf8')) == {'result': 'success'}
139+
140+
assert block.display_name == TEST_SUBMIT_DATA['display_name']
141+
assert block.instructions == TEST_SUBMIT_DATA['instructions']
142+
assert block.num_inputs == TEST_SUBMIT_DATA['num_inputs']
143+
assert block.num_top_words == TEST_SUBMIT_DATA['num_top_words']
144+
assert block.display_student_percents == (TEST_SUBMIT_DATA['display_student_percents'] == "True")

xmodule/word_cloud_block.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,26 @@ def index_dictionary(self):
315315

316316
return xblock_body
317317

318+
@XBlock.json_handler
319+
def studio_submit(self, submissions, suffix=''): # pylint: disable=unused-argument
320+
"""
321+
Change the settings for this XBlock given by the Studio user
322+
"""
323+
if 'display_name' in submissions:
324+
self.display_name = submissions['display_name']
325+
if 'instructions' in submissions:
326+
self.instructions = submissions['instructions']
327+
if 'num_inputs' in submissions:
328+
self.num_inputs = submissions['num_inputs']
329+
if 'num_top_words' in submissions:
330+
self.num_top_words = submissions['num_top_words']
331+
if 'display_student_percents' in submissions:
332+
self.display_student_percents = submissions['display_student_percents'] == 'True'
333+
334+
return {
335+
'result': 'success',
336+
}
337+
318338

319339
WordCloudBlock = (
320340
_ExtractedWordCloudBlock if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK

0 commit comments

Comments
 (0)