Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support V2 libraries in LibraryContentBlock (randomized only) #33258

Closed
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: 7 additions & 11 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,24 +991,22 @@ 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_block(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)
self.assertEqual(problem2_in_course.display_name, self.problem.display_name)
5 changes: 5 additions & 0 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ def container_handler(request, usage_key_string):

# Get the status of the user's clipboard so they can paste components if they have something to paste
user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request)
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 @@ -203,6 +206,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 @@ -218,6 +222,7 @@ def container_handler(request, usage_key_string):
'templates': CONTAINER_TEMPLATES,
# Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API.
'user_clipboard': user_clipboard,
'is_fullwidth_content': is_library_xblock,
})
else:
return HttpResponseBadRequest("Only supports HTML requests")
Expand Down
6 changes: 5 additions & 1 deletion cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
wrap_xblock_aside
)

from ..utils import get_visibility_partition_info
from ..utils import get_visibility_partition_info, StudioPermissionsService
from .access import get_user_role
from .session_kv_store import SessionKeyValueStore

Expand Down Expand Up @@ -198,6 +198,7 @@ def _prepare_runtime_for_preview(request, block):
deprecated_anonymous_user_id = anonymous_id_for_user(request.user, None)

services = {
"studio_user_permissions": StudioPermissionsService(request.user),
"i18n": XBlockI18nService,
'mako': mako_service,
"settings": SettingsService(),
Expand Down Expand Up @@ -310,6 +311,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'is_reorderable': is_reorderable,
'can_edit': can_edit,
'can_edit_visibility': context.get('can_edit_visibility', is_course),
'is_loading': context.get('is_loading', False),
'is_selected': context.get('is_selected', False),
'selectable': context.get('selectable', False),
'selected_groups_label': selected_groups_label,
'can_add': context.get('can_add', True),
'can_move': context.get('can_move', is_course),
Expand Down
9 changes: 3 additions & 6 deletions cms/djangoapps/contentstore/views/tests/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,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 @@ -233,7 +233,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 @@ -2675,10 +2675,7 @@ def setUp(self):
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"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def handle_xblock(request, usage_key_string=None):
the public studio content API.
"""
if usage_key_string:

usage_key = usage_key_with_run(usage_key_string)

access_check = (
Expand Down Expand Up @@ -218,15 +219,16 @@ def handle_xblock(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 modify_xblock(usage_key, request)
modified_xblock = modify_xblock(usage_key, request)
_post_editor_saved_callback(get_xblock(usage_key, request.user))
return modified_xblock

elif request.method in ("PUT", "POST"):
if "duplicate_source_locator" in request.json:
parent_usage_key = usage_key_with_run(request.json["parent_locator"])
duplicate_source_usage_key = usage_key_with_run(
request.json["duplicate_source_locator"]
)

source_course = duplicate_source_usage_key.course_key
dest_course = parent_usage_key.course_key
if not has_studio_write_access(
Expand All @@ -253,6 +255,8 @@ def handle_xblock(request, usage_key_string=None):
request.user,
request.json.get("display_name"),
)
_post_editor_saved_callback(get_xblock(dest_usage_key, request.user))

return JsonResponse(
{
"locator": str(dest_usage_key),
Expand Down Expand Up @@ -296,7 +300,6 @@ def handle_xblock(request, usage_key_string=None):

def modify_xblock(usage_key, request):
request_data = request.json
print(f'In modify_xblock with data = {request_data.get("data")}, fields = {request_data.get("fields")}')
return _save_xblock(
request.user,
get_xblock(usage_key, request.user),
Expand Down Expand Up @@ -372,7 +375,15 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
return modulestore().update_item(xblock, user.id)


def _save_xblock( # lint-amnesty, pylint: disable=too-many-statements
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,
Expand All @@ -387,12 +398,11 @@ def _save_xblock( # lint-amnesty, pylint: disable=too-many-statements
publish=None,
fields=None,
summary_configuration_enabled=None,
):
): # lint-amnesty, pylint: disable=too-many-statements
"""
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
to default).

"""
store = modulestore()
# Perform all xblock changes within a (single-versioned) transaction
Expand Down
2 changes: 0 additions & 2 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1994,8 +1994,6 @@
]

LIBRARY_BLOCK_TYPES = [
# Per https://github.com/openedx/build-test-release-wg/issues/231
# we removed the library source content block from defaults until complete.
{
'component': 'library_content',
'boilerplate_name': None
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
Loading
Loading