From 265701c01dcc7687efff51d71f9083ca20371026 Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Fri, 15 Sep 2023 10:04:38 -0400 Subject: [PATCH] feat!: remove library_sourced block (#33257) Originally, we planned to add support for V2 libraries and for static (hand-selected) library block reference via new block type: library_sourced. We have since decided that it would be better to add those capabilities in-place to the existing library_content block. This will ease V1->V2 library migration and make adoption of the new features easier for current library users. It will also avoid duplication of logic between two block types, we we fear would be error-prone. For details, see this ADR: https://github.com/openedx/edx-platform/pull/33231 So, we are removing the library_sourced block. This block has existed in edx-platform for a few years now, but was not enabled by default and never officially supported. It was only usable via the experimental V2 content library feature. Operators who added library_sourced blocks to their course will now see them render as `HiddenBlock` instances, i.e.: > ERROR: "library_sourced" is an unknown component type... This should not impact other component types in such courses and should not impact import/export. --- .../contentstore/views/tests/test_block.py | 5 +- .../0013-library-reference-content-block.rst | 8 +- openedx/core/lib/xblock_utils/__init__.py | 2 +- setup.py | 1 - webpack.common.config.js | 1 - xmodule/assets/README.rst | 9 +- .../LibrarySourcedBlockPicker.jsx | 236 ------------------ .../public/js/library_source_block.js | 58 ----- xmodule/assets/library_source_block/style.css | 58 ----- xmodule/library_sourced_block.py | 159 ------------ xmodule/library_tools.py | 4 +- .../library-sourced-block-studio-view.html | 19 -- xmodule/tests/test_library_sourced_block.py | 71 ------ 13 files changed, 13 insertions(+), 618 deletions(-) delete mode 100644 xmodule/assets/library_source_block/LibrarySourcedBlockPicker.jsx delete mode 100644 xmodule/assets/library_source_block/public/js/library_source_block.js delete mode 100644 xmodule/assets/library_source_block/style.css delete mode 100644 xmodule/library_sourced_block.py delete mode 100644 xmodule/templates/library-sourced-block-studio-view.html delete mode 100644 xmodule/tests/test_library_sourced_block.py diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index 49e08b6a9c..26b3f91a0b 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -2675,10 +2675,7 @@ class TestComponentTemplates(CourseTestCase): XBlockStudioConfiguration.objects.create( name="openassessment", enabled=True, support_level="us" ) - # Library Sourced Block and Library Content block has it's own category. - XBlockStudioConfiguration.objects.create( - name="library_sourced", enabled=True, support_level="fs" - ) + # Library Content block has its own category. XBlockStudioConfiguration.objects.create( name="library_content", enabled=True, support_level="fs" ) diff --git a/docs/decisions/0013-library-reference-content-block.rst b/docs/decisions/0013-library-reference-content-block.rst index cd842d8b0f..3d8dcb2153 100644 --- a/docs/decisions/0013-library-reference-content-block.rst +++ b/docs/decisions/0013-library-reference-content-block.rst @@ -3,7 +3,13 @@ Referencing Content Blocks in Library V2 Status ======= -Pending + +**Deferred** as of September 2023. + +The goals described in the ADR are still relevant to future development, +but for the intial launch of Blockstore-backed content libraries, +the existing ``library_content`` block will be used instead, +and it will continue to copy blocks from Blockstore into Modulestore as necessary. Context ======= diff --git a/openedx/core/lib/xblock_utils/__init__.py b/openedx/core/lib/xblock_utils/__init__.py index 0910018f60..26127dbfb3 100644 --- a/openedx/core/lib/xblock_utils/__init__.py +++ b/openedx/core/lib/xblock_utils/__init__.py @@ -452,7 +452,7 @@ def xblock_resource_pkg(block): ProblemBlock, and most other built-in blocks currently. Handling for these assets does not interact with this function. 2. The (preferred) standard XBlock runtime resource loading system, used by - LibrarySourcedBlock. Handling for these assets *does* interact with this + LibraryContentBlock. Handling for these assets *does* interact with this function. We hope to migrate to (2) eventually, tracked by: diff --git a/setup.py b/setup.py index 17d0f07b01..f405f92a95 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ XBLOCKS = [ "image = xmodule.template_block:TranslateCustomTagBlock", "library = xmodule.library_root_xblock:LibraryRoot", "library_content = xmodule.library_content_block:LibraryContentBlock", - "library_sourced = xmodule.library_sourced_block:LibrarySourcedBlock", "lti = xmodule.lti_block:LTIBlock", "poll_question = xmodule.poll_block:PollBlock", "problem = xmodule.capa_block:ProblemBlock", diff --git a/webpack.common.config.js b/webpack.common.config.js index 4a7bb7366b..13d368c559 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -77,7 +77,6 @@ module.exports = Merge.smart({ // Studio Import: './cms/static/js/features/import/factories/import.js', CourseOrLibraryListing: './cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx', - LibrarySourcedBlockPicker: './xmodule/assets/library_source_block/LibrarySourcedBlockPicker.jsx', // eslint-disable-line max-len 'js/factories/textbooks': './cms/static/js/factories/textbooks.js', 'js/factories/container': './cms/static/js/factories/container.js', 'js/factories/context_course': './cms/static/js/factories/context_course.js', diff --git a/xmodule/assets/README.rst b/xmodule/assets/README.rst index a39b389a35..a697915db8 100644 --- a/xmodule/assets/README.rst +++ b/xmodule/assets/README.rst @@ -46,11 +46,6 @@ It is collected into the static root, and then linked to from XBlock fragments b .. _annotatable/_display.scss: https://github.com/openedx/edx-platform/tree/master/xmodule/assets/annotatable/_display.scss .. _simplify things: https://github.com/openedx/edx-platform/issues/32621 -Static CSS (.css) -***************** - -Non-themable, ready-to-seve CSS may also be added to the any block type's -subdirectory. For example, see `library_source_block/style.css`_. JavaScript (.js) **************** @@ -72,7 +67,7 @@ Currently, edx-platform XBlock JS is defined both here in `xmodule/assets`_ and * For other "purer" blocks, the JS is used as a standard XBlock fragment. Example blocks: * `VerticalBlock`_ - * `LibrarySourcedBlock`_ + * `LibraryContentBlock`_ As part of an `active build refactoring`_, we will soon consolidate all edx-platform XBlock JS here in `xmodule/assets`_. @@ -82,7 +77,7 @@ As part of an `active build refactoring`_, we will soon consolidate all edx-plat .. _HtmlBlock: https://github.com/openedx/edx-platform/blob/master/xmodule/html_block.py .. _AnnotatableBlock: https://github.com/openedx/edx-platform/blob/master/xmodule/annotatable_block.py .. _VerticalBlock: https://github.com/openedx/edx-platform/blob/master/xmodule/vertical_block.py -.. _LibrarySourcedBlock: https://github.com/openedx/edx-platform/blob/master/xmodule/library_sourced_block.py +.. _LibraryContentBlock: https://github.com/openedx/edx-platform/blob/master/xmodule/library_content_block.py .. _active build refactoring: https://github.com/openedx/edx-platform/issues/31624 .. _builtin_assets.py: https://github.com/openedx/edx-platform/tree/master/xmodule/util/builtin_assets.py .. _static_content.py: https://github.com/openedx/edx-platform/blob/master/xmodule/static_content.py diff --git a/xmodule/assets/library_source_block/LibrarySourcedBlockPicker.jsx b/xmodule/assets/library_source_block/LibrarySourcedBlockPicker.jsx deleted file mode 100644 index 0b6363893a..0000000000 --- a/xmodule/assets/library_source_block/LibrarySourcedBlockPicker.jsx +++ /dev/null @@ -1,236 +0,0 @@ -/* globals gettext */ - -import 'whatwg-fetch'; -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import styles from './style.css'; - -class LibrarySourcedBlockPicker extends React.Component { - constructor(props) { - super(props); - this.state = { - libraries: [], - xblocks: [], - // eslint-disable-next-line react/no-unused-state - searchedLibrary: '', - libraryLoading: false, - xblocksLoading: false, - selectedLibrary: undefined, - selectedXblocks: new Set(this.props.selectedXblocks), - }; - this.onLibrarySearchInput = this.onLibrarySearchInput.bind(this); - this.onXBlockSearchInput = this.onXBlockSearchInput.bind(this); - this.onLibrarySelected = this.onLibrarySelected.bind(this); - this.onXblockSelected = this.onXblockSelected.bind(this); - this.onDeleteClick = this.onDeleteClick.bind(this); - } - - componentDidMount() { - this.fetchLibraries(); - } - - // eslint-disable-next-line react/sort-comp - fetchLibraries(textSearch = '', page = 1, append = false) { - this.setState({ - // eslint-disable-next-line react/no-access-state-in-setstate - libraries: append ? this.state.libraries : [], - libraryLoading: true, - }, async function() { - try { - let res = await fetch(`/api/libraries/v2/?pagination=true&page=${page}&text_search=${textSearch}`); - res = await res.json(); - this.setState({ - // eslint-disable-next-line react/no-access-state-in-setstate - libraries: this.state.libraries.concat(res.results), - libraryLoading: false, - }, () => { - if (res.next) { - this.fetchLibraries(textSearch, page + 1, true); - } - }); - } catch (error) { - $('#library-sourced-block-picker').trigger('error', { - title: 'Could not fetch library', - message: error, - }); - this.setState({ - libraries: [], - libraryLoading: false, - }); - } - }); - } - - fetchXblocks(library, textSearch = '', page = 1, append = false) { - this.setState({ - // eslint-disable-next-line react/no-access-state-in-setstate - xblocks: append ? this.state.xblocks : [], - xblocksLoading: true, - }, async function() { - try { - let res = await fetch(`/api/libraries/v2/${library}/blocks/?pagination=true&page=${page}&text_search=${textSearch}`); - res = await res.json(); - this.setState({ - // eslint-disable-next-line react/no-access-state-in-setstate - xblocks: this.state.xblocks.concat(res.results), - xblocksLoading: false, - }, () => { - if (res.next) { - this.fetchXblocks(library, textSearch, page + 1, true); - } - }); - } catch (error) { - $('#library-sourced-block-picker').trigger('error', { - title: 'Could not fetch xblocks', - message: error, - }); - this.setState({ - xblocks: [], - xblocksLoading: false, - }); - } - }); - } - - onLibrarySearchInput(event) { - event.persist(); - this.setState({ - // eslint-disable-next-line react/no-unused-state - searchedLibrary: event.target.value, - }); - if (!this.debouncedFetchLibraries) { - this.debouncedFetchLibraries = _.debounce(value => { - this.fetchLibraries(value); - }, 300); - } - this.debouncedFetchLibraries(event.target.value); - } - - onXBlockSearchInput(event) { - event.persist(); - if (!this.debouncedFetchXblocks) { - this.debouncedFetchXblocks = _.debounce(value => { - this.fetchXblocks(this.state.selectedLibrary, value); - }, 300); - } - this.debouncedFetchXblocks(event.target.value); - } - - onLibrarySelected(event) { - this.setState({ - selectedLibrary: event.target.value, - }); - this.fetchXblocks(event.target.value); - } - - onXblockSelected(event) { - // eslint-disable-next-line prefer-const, react/no-access-state-in-setstate - let state = new Set(this.state.selectedXblocks); - if (event.target.checked) { - state.add(event.target.value); - } else { - state.delete(event.target.value); - } - this.setState({ - selectedXblocks: state, - }, this.updateList); - } - - onDeleteClick(event) { - let value; - if (event.target.tagName == 'SPAN') { - value = event.target.parentElement.dataset.value; - } else { - value = event.target.dataset.value; - } - // eslint-disable-next-line prefer-const, react/no-access-state-in-setstate - let state = new Set(this.state.selectedXblocks); - state.delete(value); - this.setState({ - selectedXblocks: state, - }, this.updateList); - } - - updateList(list) { - $('#library-sourced-block-picker').trigger('selected-xblocks', { - sourceBlockIds: Array.from(this.state.selectedXblocks), - }); - } - - render() { - return ( -
-
-
-

-

-
-
-
-
- -
- { - this.state.libraries.map(lib => ( -
- - -
- )) - } - { this.state.libraryLoading && {gettext('Loading...')} } -
-
-
- -
- { - this.state.xblocks.map(block => ( -
- - -
- )) - } - { this.state.xblocksLoading && {gettext('Loading...')} } -
-
-
-

{gettext('Selected blocks')}

-
    - { - Array.from(this.state.selectedXblocks).map(block => ( -
  • - {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - - {/* eslint-disable-next-line react/button-has-type */} - -
  • - )) - } -
-
-
-
- ); - } -} - -LibrarySourcedBlockPicker.propTypes = { - // eslint-disable-next-line react/forbid-prop-types - selectedXblocks: PropTypes.array, -}; - -LibrarySourcedBlockPicker.defaultProps = { - selectedXblocks: [], -}; - -export {LibrarySourcedBlockPicker}; // eslint-disable-line import/prefer-default-export diff --git a/xmodule/assets/library_source_block/public/js/library_source_block.js b/xmodule/assets/library_source_block/public/js/library_source_block.js deleted file mode 100644 index 9674080d59..0000000000 --- a/xmodule/assets/library_source_block/public/js/library_source_block.js +++ /dev/null @@ -1,58 +0,0 @@ -/* JavaScript for allowing editing options on LibrarySourceBlock's studio view */ -window.LibrarySourceBlockStudioView = function(runtime, element) { - 'use strict'; - var self = this; - - $('#library-sourced-block-picker', element).on('selected-xblocks', function(e, params) { - self.sourceBlockIds = params.sourceBlockIds; - }); - - $('#library-sourced-block-picker', element).on('error', function(e, params) { - runtime.notify('error', {title: gettext(params.title), message: params.message}); - }); - - $('.save-button', element).on('click', function(e) { - e.preventDefault(); - var url = $(e.target).data('submit-url'); - var data = { - values: { - source_block_ids: self.sourceBlockIds - }, - defaults: ['display_name'] - }; - - runtime.notify('save', { - state: 'start', - message: gettext('Saving'), - element: element - }); - $.ajax({ - type: 'POST', - url: url, - data: JSON.stringify(data), - global: false // Disable error handling that conflicts with studio's notify('save') and notify('cancel') - }).done(function() { - runtime.notify('save', { - state: 'end', - element: element - }); - }).fail(function(jqXHR) { - var message = gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'); // eslint-disable-line max-len - if (jqXHR.responseText) { // Is there a more specific error message we can show? - try { - message = JSON.parse(jqXHR.responseText).error; - if (typeof message === 'object' && message.messages) { - // e.g. {"error": {"messages": [{"text": "Unknown user 'bob'!", "type": "error"}, ...]}} etc. - message = $.map(message.messages, function(msg) { return msg.text; }).join(', '); - } - } catch (error) { message = jqXHR.responseText.substr(0, 300); } - } - runtime.notify('error', {title: gettext('Unable to update settings'), message: message}); - }); - }); - - $('.cancel-button', element).on('click', function(e) { - e.preventDefault(); - runtime.notify('cancel', {}); - }); -}; diff --git a/xmodule/assets/library_source_block/style.css b/xmodule/assets/library_source_block/style.css deleted file mode 100644 index 4892f20405..0000000000 --- a/xmodule/assets/library_source_block/style.css +++ /dev/null @@ -1,58 +0,0 @@ -.column { - display: flex; - flex-direction: column; - margin: 10px; - max-width: 300px; - flex-grow: 1; -} -.elementList { - margin-top: 10px; -} -input.search { - width: 100% !important; - height: auto !important; -} -.element > input[type='checkbox'], -.element > input[type='radio'] { - position: absolute; - width: 0 !important; - height: 0 !important; - top: -9999px; -} -.element > .elementItem { - display: flex; - flex-grow: 1; - padding: 0.625rem 1.25rem; - border: 1px solid rgba(0, 0, 0, 0.25); -} -.element + .element > label { - border-top: 0; -} -.element > input[type='checkbox']:focus + label, -.element > input[type='radio']:focus + label, -.element > input:hover + label { - background: #f6f6f7; - cursor: pointer; -} -.element > input:checked + label { - background: #23419f; - color: #fff; -} -.element > input[type='checkbox']:checked:focus + label, -.element > input[type='radio']:checked:focus + label, -.element > input:checked:hover + label { - background: #193787; - cursor: pointer; -} -.selectedBlocks { - padding: 12px 8px 20px; -} -button.remove { - background: #e00; - color: #fff; - border: solid rgba(0,0,0,0.25) 1px; -} -button.remove:focus, -button.remove:hover { - background: #d00; -} diff --git a/xmodule/library_sourced_block.py b/xmodule/library_sourced_block.py deleted file mode 100644 index d96741d6e4..0000000000 --- a/xmodule/library_sourced_block.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -Library Sourced Content XBlock -""" -import logging - -from copy import copy -from mako.template import Template as MakoTemplate -from xblock.core import XBlock -from xblock.fields import Scope, String, List -from xblock.validation import ValidationMessage -from xblockutils.resources import ResourceLoader -from xblockutils.studio_editable import StudioEditableXBlockMixin -from webob import Response -from web_fragments.fragment import Fragment - -from xmodule.studio_editable import StudioEditableBlock as EditableChildrenMixin -from xmodule.validation import StudioValidation, StudioValidationMessage - -log = logging.getLogger(__name__) -loader = ResourceLoader(__name__) - -# Make '_' a no-op so we can scrape strings. Using lambda instead of -# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file -_ = lambda text: text - - -@XBlock.wants('library_tools') # Only needed in studio -class LibrarySourcedBlock(StudioEditableXBlockMixin, EditableChildrenMixin, XBlock): - """ - Library Sourced Content XBlock - - Allows copying specific XBlocks from a Blockstore-based content library into - a modulestore-based course. The selected blocks are copied and become - children of this block. - - When we implement support for Blockstore-based courses, it's expected we'll - use a different mechanism for importing library content into a course. - """ - display_name = String( - help=_("The display name for this component."), - default="Library Sourced Content", - display_name=_("Display Name"), - scope=Scope.content, - ) - source_block_ids = List( - display_name=_("Library Blocks List"), - help=_("Enter the IDs of the library XBlocks that you wish to use."), - scope=Scope.content, - ) - editable_fields = ("display_name", "source_block_ids") - has_children = True - has_author_view = True - resources_dir = 'assets/library_source_block' - MAX_BLOCKS_ALLOWED = 10 - - def __str__(self): - return f"LibrarySourcedBlock: {self.display_name}" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if not self.source_block_ids: - self.has_children = False - - def studio_view(self, context): - """ - Render a form for editing this XBlock - """ - fragment = Fragment() - static_content = ResourceLoader('common.djangoapps.pipeline_mako').load_unicode('templates/static_content.html') - render_react = MakoTemplate(static_content, default_filters=[]).get_def('renderReact') - react_content = render_react.render( - component="LibrarySourcedBlockPicker", - id="library-sourced-block-picker", - props={ - 'selectedXblocks': self.source_block_ids, - } - ) - fragment.content = loader.render_django_template('templates/library-sourced-block-studio-view.html', { - 'react_content': react_content, - 'save_url': self.runtime.handler_url(self, 'submit_studio_edits'), - }) - - fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/library_source_block.js')) - fragment.initialize_js('LibrarySourceBlockStudioView') - - return fragment - - def author_view(self, context): - """ - Renders the Studio preview view. - """ - fragment = Fragment() - context = {} if not context else copy(context) # Isolate context - without this there are weird bugs in Studio - # EditableChildrenMixin.render_children will render HTML that allows instructors to make edits to the children - context['can_move'] = False - self.render_children(context, fragment, can_reorder=False, can_add=False) - return fragment - - def student_view(self, context): - """ - Renders the view that learners see. - """ - result = Fragment() - child_frags = self.runtime.render_children(self, context=context) - result.add_resources(child_frags) - result.add_content('
') - for frag in child_frags: - result.add_content(frag.content) - result.add_content('
') - return result - - def validate_field_data(self, validation, data): - """ - Validate this block's field data. Instead of checking fields like self.name, check the - fields set on data, e.g. data.name. This allows the same validation method to be re-used - for the studio editor. - """ - if len(data.source_block_ids) > self.MAX_BLOCKS_ALLOWED: - # Because importing library blocks is an expensive operation - validation.add( - ValidationMessage( - ValidationMessage.ERROR, - _("A maximum of {0} components may be added.").format(self.MAX_BLOCKS_ALLOWED) - ) - ) - - def validate(self): - """ - Validates the state of this library_sourced_xblock Instance. This is the override of the general XBlock method, - and it will also ask its superclass to validate. - """ - validation = super().validate() - validation = StudioValidation.copy(validation) - - if not self.source_block_ids: - validation.set_summary( - StudioValidationMessage( - StudioValidationMessage.NOT_CONFIGURED, - _("No XBlock has been configured for this component. Use the editor to select the target blocks."), - action_class='edit-button', - action_label=_("Open Editor") - ) - ) - return validation - - @XBlock.handler - def submit_studio_edits(self, data, suffix=''): - """ - Save changes to this block, applying edits made in Studio. - """ - response = super().submit_studio_edits(data, suffix) - # Replace our current children with the latest ones from the libraries. - lib_tools = self.runtime.service(self, 'library_tools') - try: - lib_tools.import_from_blockstore(self, self.source_block_ids) - except Exception as err: # pylint: disable=broad-except - log.exception(err) - return Response(_("Importing Library Block failed - are the IDs valid and readable?"), status=400) - return response diff --git a/xmodule/library_tools.py b/xmodule/library_tools.py index 58cd821241..748f1b58ac 100644 --- a/xmodule/library_tools.py +++ b/xmodule/library_tools.py @@ -196,7 +196,7 @@ class LibraryToolsService: content library) into modulestore, as a new child of dest_block. Any existing children of dest_block are replaced. - This is only used by LibrarySourcedBlock. It should verify first that + This is only used by LibraryContentBlock. It should verify first that the number of block IDs is reasonable. """ dest_key = dest_block.scope_ids.usage_id @@ -216,7 +216,7 @@ class LibraryToolsService: raise PermissionDenied() # Read the source block; this will also confirm that user has permission to read it. - # (This could be slow and use lots of memory, except for the fact that LibrarySourcedBlock which calls this + # (This could be slow and use lots of memory, except for the fact that LibraryContentBlock which calls this # should be limiting the number of blocks to a reasonable limit. We load them all now instead of one at a # time in order to raise any errors before we start actually copying blocks over.) orig_blocks = [load_block(UsageKey.from_string(key), user) for key in blockstore_block_ids] diff --git a/xmodule/templates/library-sourced-block-studio-view.html b/xmodule/templates/library-sourced-block-studio-view.html deleted file mode 100644 index 08a2882b51..0000000000 --- a/xmodule/templates/library-sourced-block-studio-view.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load i18n %} -
-
-
- {{ react_content|safe }} -
-
- -
diff --git a/xmodule/tests/test_library_sourced_block.py b/xmodule/tests/test_library_sourced_block.py deleted file mode 100644 index 5ec6291542..0000000000 --- a/xmodule/tests/test_library_sourced_block.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Tests for Source from Library XBlock -""" - -from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest -from common.djangoapps.student.roles import CourseInstructorRole -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory -from xmodule.tests import prepare_block_runtime -from xmodule.x_module import STUDENT_VIEW # lint-amnesty, pylint: disable=unused-import - - -class LibrarySourcedBlockTestCase(ContentLibrariesRestApiTest): - """ - Tests for LibraryToolsService which interact with blockstore-based content libraries - """ - def setUp(self): - super().setUp() - self.store = modulestore() - # Create a modulestore course - course = CourseFactory.create(modulestore=self.store, user_id=self.user.id) - CourseInstructorRole(course.id).add_users(self.user) - # Add a "Source from Library" block to the course - self.source_block = BlockFactory.create( - category="library_sourced", - parent=course, - parent_location=course.location, - user_id=self.user.id, - modulestore=self.store - ) - self.submit_url = f'/xblock/{self.source_block.scope_ids.usage_id}/handler/submit_studio_edits' - - def test_block_views(self): - # Create a blockstore content library - library = self._create_library(slug="testlib1_preview", title="Test Library 1", description="Testing XBlocks") - # Add content to the library - html_block_1 = self._add_block_to_library(library["id"], "html", "html_student_preview_1")["id"] - self._set_library_block_olx(html_block_1, 'Student Preview Test 1') - html_block_2 = self._add_block_to_library(library["id"], "html", "html_student_preview_2")["id"] - self._set_library_block_olx(html_block_2, 'Student Preview Test 2') - - # Import the html blocks from the library to the course - post_data = {"values": {"source_block_ids": [html_block_1, html_block_2]}, "defaults": ["display_name"]} - res = self.client.post(self.submit_url, data=post_data, format='json') - - # Check if student_view renders the children correctly - res = self.get_block_view(self.source_block, STUDENT_VIEW) - assert 'Student Preview Test 1' in res - assert 'Student Preview Test 2' in res - - def test_block_limits(self): - # Create a blockstore content library - library = self._create_library(slug="testlib2_preview", title="Test Library 2", description="Testing XBlocks") - # Add content to the library - blocks = [self._add_block_to_library(library["id"], "html", f"block_{i}")["id"] for i in range(11)] - - # Import the html blocks from the library to the course - post_data = {"values": {"source_block_ids": blocks}, "defaults": ["display_name"]} - res = self.client.post(self.submit_url, data=post_data, format='json') - assert res.status_code == 400 - assert res.json()['error']['messages'][0]['text'] == 'A maximum of 10 components may be added.' - - def get_block_view(self, block, view, context=None): - """ - Renders the specified view for a given XBlock - """ - context = context or {} - block = self.store.get_item(block.location) - prepare_block_runtime(block.runtime) - block.bind_for_student(self.user.id) - return block.runtime.render(block, view, context).content