160 lines
6.2 KiB
Python
160 lines
6.2 KiB
Python
"""
|
|
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('<div class="library-sourced-content">')
|
|
for frag in child_frags:
|
|
result.add_content(frag.content)
|
|
result.add_content('</div>')
|
|
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
|