Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions cms/djangoapps/contentstore/tests/test_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def _refresh_children(self, lib_content_block, status_code_expected=200):
lib_content_block.runtime._services['user'] = user_service # pylint: disable=protected-access

handler_url = reverse_usage_url(
'component_handler',
'preview_handler',
lib_content_block.location,
kwargs={'handler': 'refresh_children'}
)
Expand Down Expand Up @@ -359,8 +359,6 @@ def test_change_after_first_sync(self):
self.assertEqual(resp.status_code, 200)
lc_block = modulestore().get_item(lc_block.location)
self.assertEqual(len(lc_block.children), 1) # Children should not be deleted due to a bad setting.
html_block = modulestore().get_item(lc_block.children[0])
self.assertEqual(html_block.data, data_value)

def test_refreshes_children_if_libraries_change(self):
""" Tests that children are automatically refreshed if libraries list changes """
Expand Down Expand Up @@ -406,7 +404,7 @@ def test_refreshes_children_if_libraries_change(self):
html_block = modulestore().get_item(lc_block.children[0])
self.assertEqual(html_block.data, data2)

@patch("xmodule.library_tools.SearchEngine.get_search_engine", Mock(return_value=None, autospec=True))
@patch("xmodule.tasks.SearchEngine.get_search_engine", Mock(return_value=None, autospec=True))
def test_refreshes_children_if_capa_type_change(self):
""" Tests that children are automatically refreshed if capa type field changes """
name1, name2 = "Option Problem", "Multiple Choice Problem"
Expand Down Expand Up @@ -993,27 +991,23 @@ def test_duplicated_version(self):
self.library = store.get_library(self.lib_key)

# Refresh our reference to the block
self.lc_block = store.get_item(self.lc_block.location)
self.lc_block = self._refresh_children(self.lc_block)
self.problem_in_course = store.get_item(self.problem_in_course.location)

# The library has changed...
self.assertEqual(len(self.library.children), 2)

# But the block hasn't.
self.assertEqual(len(self.lc_block.children), 1)
self.assertEqual(self.problem_in_course.location, self.lc_block.children[0])
self.assertEqual(self.problem_in_course.display_name, self.original_display_name)
# and the block has changed too.
self.assertEqual(len(self.lc_block.children), 2)

# Duplicate self.lc_block:
duplicate = store.get_item(
_duplicate_item(self.course.location, self.lc_block.location, self.user)
)
# The duplicate should have identical children to the original:
self.assertEqual(len(duplicate.children), 1)
self.assertEqual(len(duplicate.children), 2)
self.assertTrue(self.lc_block.source_library_version)
self.assertEqual(self.lc_block.source_library_version, duplicate.source_library_version)
problem2_in_course = store.get_item(duplicate.children[0])
self.assertEqual(problem2_in_course.display_name, self.original_display_name)


class TestIncompatibleModuleStore(LibraryTestCase):
Expand Down
45 changes: 40 additions & 5 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@
log = logging.getLogger(__name__)

# NOTE: This list is disjoint from ADVANCED_COMPONENT_TYPES
COMPONENT_TYPES = ['discussion', 'html', 'openassessment', 'problem', 'video']
COMPONENT_TYPES = ['discussion', 'library', 'html', 'openassessment', 'problem', 'video']

ADVANCED_COMPONENT_TYPES = sorted({name for name, class_ in XBlock.load_classes()} - set(COMPONENT_TYPES))

ADVANCED_PROBLEM_TYPES = settings.ADVANCED_PROBLEM_TYPES

LIBRARY_BLOCK_TYPES = settings.LIBRARY_BLOCK_TYPES

CONTAINER_TEMPLATES = [
"basic-modal", "modal-button", "edit-xblock-modal",
"editor-mode-button", "upload-dialog",
Expand Down Expand Up @@ -202,7 +204,8 @@ def container_handler(request, usage_key_string):
'xblock_info': xblock_info,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'templates': CONTAINER_TEMPLATES
'templates': CONTAINER_TEMPLATES,
'is_sourced_block': xblock.location.block_type == 'library_sourced',
})
else:
return HttpResponseBadRequest("Only supports HTML requests")
Expand Down Expand Up @@ -278,7 +281,8 @@ def create_support_legend_dict():
'html': _("Text"),
'problem': _("Problem"),
'video': _("Video"),
'openassessment': _("Open Response")
'openassessment': _("Open Response"),
'library': _("Library Content"),
}

component_templates = []
Expand All @@ -287,8 +291,8 @@ def create_support_legend_dict():
# by the components in the order listed in COMPONENT_TYPES.
component_types = COMPONENT_TYPES[:]

# Libraries do not support discussions and openassessment
component_not_supported_by_library = ['discussion', 'openassessment']
# Libraries do not support discussions and openassessment and other libraries
component_not_supported_by_library = ['discussion', 'library', 'openassessment']
if library:
component_types = [component for component in component_types
if component not in set(component_not_supported_by_library)]
Expand Down Expand Up @@ -383,6 +387,37 @@ def create_support_legend_dict():
)
categories.add(component)

# Add library block types.
if category == 'library' and not library:
disabled_block_names = [block.name for block in disabled_xblocks()]
library_block_types = [problem_type for problem_type in LIBRARY_BLOCK_TYPES
if problem_type['component'] not in disabled_block_names]
for library_block_type in library_block_types:
component = library_block_type['component']
boilerplate_name = library_block_type['boilerplate_name']
authorable_variations = authorable_xblocks(allow_unsupported=allow_unsupported, name=component)
library_component_support_level = component_support_level(
authorable_variations, component, boilerplate_name
)
if library_component_support_level:
try:
component_display_name = xblock_type_display_name(component, default_display_name=component)
except PluginMissingError:
log.warning(
"Unable to load xblock type %s to read display_name",
component
)
else:
templates_for_category.append(
create_template_dict(
component_display_name,
component,
library_component_support_level,
boilerplate_name
)
)
categories.add(component)

component_templates.append({
"type": category,
"templates": templates_for_category,
Expand Down
12 changes: 11 additions & 1 deletion cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def xblock_handler(request, usage_key_string=None):
_delete_item(usage_key, request.user)
return JsonResponse()
else: # Since we have a usage_key, we are updating an existing xblock.
return _save_xblock(
response = _save_xblock(
request.user,
_get_xblock(usage_key, request.user),
data=request.json.get('data'),
Expand All @@ -213,6 +213,8 @@ def xblock_handler(request, usage_key_string=None):
publish=request.json.get('publish'),
fields=request.json.get('fields'),
)
_post_editor_saved_callback(_get_xblock(usage_key, request.user))
return response
elif request.method in ('PUT', 'POST'):
if 'duplicate_source_locator' in request.json:
parent_usage_key = usage_key_with_run(request.json['parent_locator'])
Expand Down Expand Up @@ -528,6 +530,14 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
return modulestore().update_item(xblock, user.id)


def _post_editor_saved_callback(xblock):
"""
Updates the xblock in the modulestore after saving xblock.
"""
if callable(getattr(xblock, "post_editor_saved", None)):
xblock.post_editor_saved()


def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, nullout=None, # lint-amnesty, pylint: disable=too-many-statements
grader_type=None, is_prereq=None, prereq_usage_key=None, prereq_min_score=None,
prereq_min_completion=None, publish=None, fields=None):
Expand Down
7 changes: 7 additions & 0 deletions cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ def _preview_module_system(request, descriptor, field_data):
else:
preview_anonymous_user_id = anonymous_id_for_user(request.user, course_id)

# Avoid circular import issues
from .item import StudioPermissionsService

return PreviewModuleSystem(
static_url=settings.STATIC_URL,
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
Expand All @@ -220,6 +223,7 @@ def _preview_module_system(request, descriptor, field_data):
# Get the raw DescriptorSystem, not the CombinedSystem
descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access
services={
"studio_user_permissions": StudioPermissionsService(request.user),
"field-data": field_data,
"i18n": ModuleI18nService,
'mako': mako_service,
Expand Down Expand Up @@ -309,6 +313,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'content': frag.content,
'is_root': is_root,
'is_reorderable': is_reorderable,
'is_loading': context.get('is_loading', False),
'is_selected': context.get('is_selected', False),
'selectable': context.get('selectable', False),
'can_edit': context.get('can_edit', True),
'can_edit_visibility': context.get('can_edit_visibility', xblock.scope_ids.usage_id.context_key.is_course),
'selected_groups_label': selected_groups_label,
Expand Down
8 changes: 6 additions & 2 deletions cms/djangoapps/contentstore/views/tests/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def test_get_empty_container_fragment(self):
self.assertNotRegex(html, r'wrapper-xblock[^-]+')

# Verify that the header and article tags are still added
self.assertIn('<header class="xblock-header xblock-header-vertical">', html)
self.assertIn('<header class="xblock-header xblock-header-vertical ">', html)
self.assertIn('<article class="xblock-render">', html)

def test_get_container_fragment(self):
Expand All @@ -232,7 +232,7 @@ def test_get_container_fragment(self):

# Verify that the Studio nesting wrapper has been added
self.assertIn('level-nesting', html)
self.assertIn('<header class="xblock-header xblock-header-vertical">', html)
self.assertIn('<header class="xblock-header xblock-header-vertical ">', html)
self.assertIn('<article class="xblock-render">', html)

# Verify that the Studio element wrapper has been added
Expand Down Expand Up @@ -2253,6 +2253,9 @@ def setUp(self):
XBlockStudioConfiguration.objects.create(name='video', enabled=True, support_level="us")
# ORA Block has it's own category.
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")
XBlockStudioConfiguration.objects.create(name='library_content', enabled=True, support_level="fs")
# XBlock masquerading as a problem
XBlockStudioConfiguration.objects.create(name='drag-and-drop-v2', enabled=True, support_level="fs")
XBlockStudioConfiguration.objects.create(name='staffgradedxblock', enabled=True, support_level="us")
Expand Down Expand Up @@ -2295,6 +2298,7 @@ def test_basic_components(self):
self._verify_basic_component_display_name("discussion", "Discussion")
self._verify_basic_component_display_name("video", "Video")
self._verify_basic_component_display_name("openassessment", "Open Response")
self.assertGreater(len(self.get_templates_of_type('library')), 0)
self.assertGreater(len(self.get_templates_of_type('html')), 0)
self.assertGreater(len(self.get_templates_of_type('problem')), 0)
self.assertIsNone(self.get_templates_of_type('advanced'))
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/views/tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ def test_get_component_templates(self):
self.assertNotIn('discussion', templates)
self.assertNotIn('advanced', templates)
self.assertNotIn('openassessment', templates)
self.assertNotIn('library', templates)

def test_advanced_problem_types(self):
"""
Expand Down
11 changes: 11 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1975,6 +1975,17 @@
}
]

LIBRARY_BLOCK_TYPES = [
{
'component': 'library_sourced',
'boilerplate_name': None
},
{
'component': 'library_content',
'boilerplate_name': None
}
]

############### Settings for Retirement #####################
# See annotations in lms/envs/common.py for details.
RETIRED_USERNAME_PREFIX = 'retired__user_'
Expand Down
7 changes: 5 additions & 2 deletions cms/lib/xblock/tagging/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,12 @@ def test_preview_html(self):
tree = etree.parse(StringIO(problem_html), parser)

main_div_nodes = tree.xpath('/html/body/div/section/div')
self.assertEqual(len(main_div_nodes), 1)
self.assertEqual(len(main_div_nodes), 2)

div_node = main_div_nodes[0]
loader_div_node = main_div_nodes[0]
self.assertIn('ui-loading', loader_div_node.get('class'))

div_node = main_div_nodes[1]
self.assertEqual(div_node.get('data-init'), 'StructuredTagsInit')
self.assertEqual(div_node.get('data-runtime-class'), 'PreviewRuntime')
self.assertEqual(div_node.get('data-block-type'), 'tagging_aside')
Expand Down
Binary file added cms/static/images/large-library-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 65 additions & 3 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock',
'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal',
'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor',
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils'],
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils', 'js/utils/module'],
function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
ContainerSubviews, UnitOutlineView, XBlockUtils) {
ContainerSubviews, UnitOutlineView, XBlockUtils, ModuleUtils) {
'use strict';
var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model
Expand All @@ -20,7 +20,9 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
'click .duplicate-button': 'duplicateXBlock',
'click .move-button': 'showMoveXBlockModal',
'click .delete-button': 'deleteXBlock',
'click .new-component-button': 'scrollToNewComponentButtons'
'click .save-button': 'saveSelectedLibraryComponents',
'click .new-component-button': 'scrollToNewComponentButtons',
'change .header-library-checkbox': 'toggleLibraryComponent'
},

options: {
Expand All @@ -42,6 +44,7 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
BasePage.prototype.initialize.call(this, options);
this.viewClass = options.viewClass || this.defaultViewClass;
this.isLibraryPage = (this.model.attributes.category === 'library');
this.isLibrarySourced = (this.model.attributes.category === 'library_sourced');
this.nameEditor = new XBlockStringFieldEditor({
el: this.$('.wrapper-xblock-field'),
model: this.model
Expand Down Expand Up @@ -96,6 +99,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
});
this.unitOutlineView.render();
}
if (this.isLibrarySourced) {
this.selectedLibraryComponents = [];
this.storedSelectedLibraryComponents = [];
this.getSelectedLibraryComponents();
}

this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved);
},
Expand Down Expand Up @@ -300,6 +308,60 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
});
},

getSelectedLibraryComponents: function() {
var self = this;
var locator = this.$el.find('.studio-xblock-wrapper').data('locator');
$.getJSON(
ModuleUtils.getUpdateUrl(locator) + '/handler/get_block_ids',
function(data) {
self.selectedLibraryComponents = Array.from(data.source_block_ids);
self.storedSelectedLibraryComponents = Array.from(data.source_block_ids);
}
);
},

saveSelectedLibraryComponents: function(e) {
var self = this;
var locator = this.$el.find('.studio-xblock-wrapper').data('locator');
e.preventDefault();
$.postJSON(
ModuleUtils.getUpdateUrl(locator) + '/handler/submit_studio_edits',
{values: {source_block_ids: self.storedSelectedLibraryComponents}},
function() {
self.selectedLibraryComponents = Array.from(self.storedSelectedLibraryComponents);
self.toggleSaveButton();
}
);
},

toggleLibraryComponent: function(event) {
var componentId = $(event.target).closest('.studio-xblock-wrapper').data('locator');
var storeIndex = this.storedSelectedLibraryComponents.indexOf(componentId);
if (storeIndex > -1) {
this.storedSelectedLibraryComponents.splice(storeIndex, 1);
this.toggleSaveButton();
} else {
this.storedSelectedLibraryComponents.push(componentId);
this.toggleSaveButton();
}
},

toggleSaveButton: function() {
var $saveButton = $('.nav-actions .save-button');
if (JSON.stringify(this.selectedLibraryComponents.sort()) === JSON.stringify(this.storedSelectedLibraryComponents.sort())) {
$saveButton.addClass('is-hidden');
window.removeEventListener('beforeunload', this.onBeforePageUnloadCallback);
} else {
$saveButton.removeClass('is-hidden');
window.addEventListener('beforeunload', this.onBeforePageUnloadCallback);
}
},

onBeforePageUnloadCallback: function (event) {
event.preventDefault();
event.returnValue = '';
},

onDelete: function(xblockElement) {
// get the parent so we can remove this component from its parent.
var xblockView = this.xblockView,
Expand Down
7 changes: 7 additions & 0 deletions cms/static/sass/assets/_graphics.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,10 @@
height: ($baseline*3);
background: url('#{$static-path}/images/large-openassessment-icon.png') center no-repeat;
}

.large-library-icon {
display: inline-block;
width: ($baseline*3);
height: ($baseline*3);
background: url('#{$static-path}/images/large-library-icon.png') center no-repeat;
}
Loading