Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eff3eb5
feat: implement V2 libraries usage for library content block
dyudyunov Jun 16, 2022
379881a
feat: hide problem type field for v2 libs
dyudyunov Jul 14, 2022
a2b5fd2
style: formating the code
dyudyunov Jul 14, 2022
1ec23db
feat: Edit modal for Library Sourced Content
UvgenGen Jun 20, 2022
49efc9b
feat: remove react studio view for Library Sourced Content
UvgenGen Jun 20, 2022
df07051
feat: add "selectable" context for author_view
UvgenGen Jun 20, 2022
21975e1
feat: add selection functionality to library components
ihor-romaniuk Jun 24, 2022
9e0d22d
feat: show selected blocks for student_view
UvgenGen Jun 23, 2022
aab4742
test: update tests for LibrarySourcedBlock
UvgenGen Jun 24, 2022
1cfeca9
feat: add context for library_sourced container page
UvgenGen Jun 24, 2022
7393316
feat: code review
UvgenGen Jul 4, 2022
4260e16
feat: Take the Library Content xblocks out the Advanced section to th…
UvgenGen Jun 13, 2022
49284cb
feat: add celery task for importing blocks
UvgenGen Jul 12, 2022
5d9120b
feat: update tests
UvgenGen Jul 13, 2022
1f19f16
feat: update tests
UvgenGen Jul 14, 2022
83e7382
feat: update getting is_loading context
UvgenGen Jul 20, 2022
936d847
feat: implement collapse on the library units page
ihor-romaniuk Jun 13, 2022
ccd0fb0
fix: prevent click on collapse button
ihor-romaniuk Jun 14, 2022
f3ec7c8
feat: add context for library xblocks
UvgenGen Jun 15, 2022
bdd0dd8
feat: rename View button to Configure
UvgenGen Jul 19, 2022
f6723d0
fix: fix errors
UvgenGen Jul 21, 2022
2954fdf
fix: 500 error appears if instructor doesn't choose the Library
UvgenGen Jul 26, 2022
b69d173
feat: Problem with resave xblock
UvgenGen Jul 26, 2022
5d04c53
fix: "Configure" replace to "View"
UvgenGen Jul 26, 2022
3a4805f
feat: remove library component from library category
UvgenGen Jul 22, 2022
f429515
fix: fix updating preview for library xblocks after uploading childrens
UvgenGen Aug 9, 2022
d7e8080
feat: add validation for empty library
UvgenGen Aug 10, 2022
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
52 changes: 46 additions & 6 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 @@ -182,6 +184,9 @@ def container_handler(request, usage_key_string):
break
index += 1

library_block_types = [problem_type['component'] for problem_type in LIBRARY_BLOCK_TYPES]
is_library_xblock = xblock.location.block_type in library_block_types

return render_to_response('container.html', {
'language_code': request.LANGUAGE_CODE,
'context_course': course, # Needed only for display of menus at top of page.
Expand All @@ -190,6 +195,7 @@ def container_handler(request, usage_key_string):
'xblock_locator': xblock.location,
'unit': unit,
'is_unit_page': is_unit_page,
'is_collapsible': is_library_xblock,
'subsection': subsection,
'section': section,
'position': index,
Expand All @@ -202,7 +208,9 @@ 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',
'is_fullwidth_content': is_library_xblock,
})
else:
return HttpResponseBadRequest("Only supports HTML requests")
Expand Down Expand Up @@ -278,7 +286,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 +296,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 All @@ -307,7 +316,7 @@ def create_support_legend_dict():
templates_for_category = []
component_class = _load_mixed_class(category)

if support_level_without_template:
if support_level_without_template and category != 'library':
# add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
Expand Down Expand Up @@ -383,6 +392,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
8 changes: 8 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,10 @@ 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_collapse': context.get('can_collapse', 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.
Loading