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