From 9e28ba92ecd4adad7ff0ad7d9738b4af950e54f3 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Sat, 19 Oct 2024 17:03:13 -0700 Subject: [PATCH] feat: minimal UI for the Problem Bank block --- cms/djangoapps/contentstore/utils.py | 2 +- cms/djangoapps/contentstore/views/preview.py | 8 ++- cms/envs/common.py | 3 + .../views/modals/select_v2_library_content.js | 68 +++++++++++++++++++ cms/static/js/views/pages/container.js | 44 +++++++++++- cms/templates/studio_xblock_wrapper.html | 3 +- xmodule/item_bank_block.py | 42 ++++++++---- xmodule/templates/item_bank/author_view.html | 46 +++++++++++++ .../templates/item_bank/author_view_add.html | 14 ++++ xmodule/tests/test_item_bank.py | 9 +-- 10 files changed, 213 insertions(+), 26 deletions(-) create mode 100644 cms/static/js/views/modals/select_v2_library_content.js create mode 100644 xmodule/templates/item_bank/author_view.html create mode 100644 xmodule/templates/item_bank/author_view_add.html diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index d40a5ec79475..964f0c57e757 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -436,7 +436,7 @@ def get_library_content_picker_url(course_locator) -> str: content_picker_url = None if libraries_v2_enabled(): mfe_base_url = get_course_authoring_url(course_locator) - content_picker_url = f'{mfe_base_url}/component-picker' + content_picker_url = f'{mfe_base_url}/component-picker?variant=published' return content_picker_url diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index acc5fc95dfe3..aa7421bb87b3 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -300,8 +300,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): selected_groups_label = _('Access restricted to: {list_of_groups}').format(list_of_groups=selected_groups_label) # lint-amnesty, pylint: disable=line-too-long course = modulestore().get_course(xblock.location.course_key) can_edit = context.get('can_edit', True) + can_add = context.get('can_add', True) # Is this a course or a library? - is_course = xblock.scope_ids.usage_id.context_key.is_course + is_course = xblock.context_key.is_course tags_count_map = context.get('tags_count_map') tags_count = 0 if tags_count_map: @@ -320,7 +321,10 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): 'is_selected': context.get('is_selected', False), 'selectable': context.get('selectable', False), 'selected_groups_label': selected_groups_label, - 'can_add': context.get('can_add', True), + 'can_add': can_add, + # Generally speaking, "if you can add, you can delete". One exception is itembank (Problem Bank) + # which has its own separate "add" workflow but uses the normal delete workflow for its child blocks. + 'can_delete': can_add or (root_xblock and root_xblock.scope_ids.block_type == "itembank" and can_edit), 'can_move': context.get('can_move', is_course), 'language': getattr(course, 'language', None), 'is_course': is_course, diff --git a/cms/envs/common.py b/cms/envs/common.py index 3942c9d68be2..d23b040e6696 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1648,6 +1648,9 @@ 'corsheaders', 'openedx.core.djangoapps.cors_csrf', + # Provides the 'django_markup' template library so we can use 'interpolate_html' in django templates + 'xss_utils', + # History tables 'simple_history', diff --git a/cms/static/js/views/modals/select_v2_library_content.js b/cms/static/js/views/modals/select_v2_library_content.js new file mode 100644 index 000000000000..87523679678c --- /dev/null +++ b/cms/static/js/views/modals/select_v2_library_content.js @@ -0,0 +1,68 @@ +/** + * Provides utilities to open and close the library content picker. + * + */ +define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'], +function($, _, gettext, BaseModal) { + 'use strict'; + + var SelectV2LibraryContent = BaseModal.extend({ + options: $.extend({}, BaseModal.prototype.options, { + modalName: 'add-component-from-library', + modalSize: 'lg', + view: 'studio_view', + viewSpecificClasses: 'modal-add-component-picker confirm', + // Translators: "title" is the name of the current component being edited. + titleFormat: gettext('Add library content'), + addPrimaryActionButton: false, + }), + + initialize: function() { + BaseModal.prototype.initialize.call(this); + // Add event listen to close picker when the iframe tells us to + const handleMessage = (event) => { + if (event.data?.type === 'pickerComponentSelected') { + var requestData = { + library_content_key: event.data.usageKey, + category: event.data.category, + } + this.callback(requestData); + this.hide(); + } + }; + this.messageListener = window.addEventListener("message", handleMessage); + this.cleanupListener = () => { window.removeEventListener("message", handleMessage) }; + }, + + hide: function() { + BaseModal.prototype.hide.call(this); + this.cleanupListener(); + }, + + /** + * Adds the action buttons to the modal. + */ + addActionButtons: function() { + this.addActionButton('cancel', gettext('Cancel')); + }, + + /** + * Show a component picker modal from library. + * @param contentPickerUrl Url for component picker + * @param callback A function to call with the selected block(s) + */ + showComponentPicker: function(contentPickerUrl, callback) { + this.contentPickerUrl = contentPickerUrl; + this.callback = callback; + + this.render(); + this.show(); + }, + + getContentHtml: function() { + return `