Skip to content

Commit

Permalink
fix: Render Word cloud block editor in libraries [FC-0076] (openedx#3…
Browse files Browse the repository at this point in the history
…6199)

* 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]>
  • Loading branch information
ChrisChV and navinkarkera authored Feb 19, 2025
1 parent b7a2ffa commit d29ff63
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 13 deletions.
2 changes: 1 addition & 1 deletion cms/static/js/views/xblock_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
120 changes: 108 additions & 12 deletions common/templates/xblock_v2/xblock_iframe.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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 };
Expand Down
27 changes: 27 additions & 0 deletions xmodule/tests/test_word_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
20 changes: 20 additions & 0 deletions xmodule/word_cloud_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d29ff63

Please sign in to comment.