* feat: library unit sync * feat: create component link only for component xblocks * feat: container link model * feat: update downstream api views * feat: delete extra components in container on sync (not working) * fix: duplicate definitions of LibraryXBlockMetadata * test: add a new integration test suite for syncing * feat: partially implement container+child syncing * fix: blockserializer wasn't always serializing all HTML block fields * feat: handle reorder, addition and deletion of components in sync Updates children components of unit in course based on upstream unit, deletes removed component, adds new ones and updates order as per upstream. * feat: return unit upstreamInfo and disallow edits to units in courses that are sourced from a library (#773) * feat: Add upstream_info to unit * feat: disallow edits to units in courses that are sourced from a library (#774) --------- Co-authored-by: Jillian Vogel <jill@opencraft.com> Co-authored-by: Rômulo Penido <romulo.penido@gmail.com> * docs: capitalization of XBlock Co-authored-by: David Ormsbee <dave@axim.org> * refactor: (minor) change python property name to reflect type better * fix: lots of "Tried to inspect a missing...upstream link" warnings when viewing a unit in Studio * docs: mention potential REST API for future refactor * fix: check if upstream actually exists before making unit read-only * chore: fix camel-case var * fix: test failure when mocked XBlock doesn't have UpstreamSyncMixin --------- Co-authored-by: Braden MacDonald <braden@opencraft.com> Co-authored-by: Chris Chávez <xnpiochv@gmail.com> Co-authored-by: Jillian Vogel <jill@opencraft.com> Co-authored-by: Rômulo Penido <romulo.penido@gmail.com> Co-authored-by: Braden MacDonald <mail@bradenm.com> Co-authored-by: David Ormsbee <dave@axim.org>
275 lines
14 KiB
HTML
275 lines
14 KiB
HTML
<%page expression_filter="h"/>
|
|
<%!
|
|
from django.utils.translation import gettext as _
|
|
from cms.djangoapps.contentstore.helpers import xblock_studio_url
|
|
from cms.djangoapps.contentstore.utils import is_visible_to_specific_partition_groups, get_editor_page_base_url, determine_label
|
|
from lms.lib.utils import is_unit
|
|
from openedx.core.djangolib.js_utils import (
|
|
dump_js_escaped_json, js_escaped_string
|
|
)
|
|
from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, use_video_gallery_flow
|
|
from cms.lib.xblock.upstream_sync import UpstreamLink
|
|
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
|
|
%>
|
|
<%
|
|
use_new_editor_text = use_new_text_editor(xblock.context_key)
|
|
use_new_editor_video = use_new_video_editor(xblock.context_key)
|
|
use_new_editor_problem = use_new_problem_editor(xblock.context_key)
|
|
use_new_video_gallery_flow = use_video_gallery_flow()
|
|
use_tagging = not is_tagging_feature_disabled()
|
|
xblock_url = xblock_studio_url(xblock)
|
|
show_inline = xblock.has_children and not xblock_url
|
|
section_class = "level-nesting" if show_inline else "level-element"
|
|
label = determine_label(xblock.display_name_with_default, xblock.scope_ids.block_type)
|
|
messages = xblock.validate().to_json()
|
|
has_not_configured_message = messages.get('summary',{}).get('type', None) == 'not-configured'
|
|
block_is_unit = is_unit(xblock)
|
|
|
|
upstream_info = UpstreamLink.try_get_for_block(xblock, log_error=False)
|
|
%>
|
|
|
|
<%namespace name='static' file='static_content.html'/>
|
|
|
|
<%block name="header_extras">
|
|
<script type="text/template" id="xblock-validation-messages-tpl">
|
|
<%static:include path="js/xblock-validation-messages.underscore" />
|
|
</script>
|
|
<script type="text/template" id="tag-count-tpl">
|
|
<%static:include path="js/tag-count.underscore" />
|
|
</script>
|
|
</%block>
|
|
|
|
<script type="text/javascript">
|
|
XBlockValidationFactory(
|
|
${messages | n, dump_js_escaped_json},
|
|
${bool(xblock_url) | n, dump_js_escaped_json}, // xblock_url will be None or a string
|
|
${bool(is_root) | n, dump_js_escaped_json}, // is_root will be None or a boolean
|
|
${bool(block_is_unit) | n, dump_js_escaped_json}, // block_is_unit will be None or a boolean
|
|
$('div.xblock-validation-messages[data-locator="${xblock.location | n, js_escaped_string}"]')
|
|
);
|
|
</script>
|
|
|
|
<%static:webpack entry="js/factories/tag_count">
|
|
TagCountFactory({
|
|
tags_count: "${tags_count | n, js_escaped_string}",
|
|
content_id: "${xblock.location | n, js_escaped_string}",
|
|
course_authoring_url: "${course_authoring_url | n, js_escaped_string}",
|
|
},
|
|
$('li.tag-count[data-locator="${xblock.location | n, js_escaped_string}"]')
|
|
);
|
|
</%static:webpack>
|
|
|
|
% if not is_root:
|
|
% if is_reorderable:
|
|
<li class="studio-xblock-wrapper is-draggable" id="${xblock.location}" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
|
|
% else:
|
|
<div class="studio-xblock-wrapper" id="${xblock.location}" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
|
|
% endif
|
|
|
|
<section class="wrapper-xblock is-collapsible ${section_class}
|
|
% if is_visible_to_specific_partition_groups(xblock):
|
|
has-group-visibility-set
|
|
% endif
|
|
">
|
|
<div class="ui-loading ${'is-hidden' if not is_loading else ''}">
|
|
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Importing components")}</span></p>
|
|
</div>
|
|
% endif
|
|
|
|
<header class="xblock-header xblock-header-${xblock.category} ${'is-hidden' if is_loading else ''}">
|
|
<div class="xblock-header-primary
|
|
% if not show_preview:
|
|
is-collapsed
|
|
% endif
|
|
"
|
|
use-new-editor-text = ${use_new_editor_text}
|
|
use-new-editor-video = ${use_new_editor_video}
|
|
use-new-editor-problem = ${use_new_editor_problem}
|
|
use-video-gallery-flow = ${use_new_video_gallery_flow}
|
|
authoring_MFE_base_url = ${get_editor_page_base_url(xblock.location.course_key)}
|
|
data-block-type = ${xblock.scope_ids.block_type}
|
|
data-usage-id = ${xblock.scope_ids.usage_id}
|
|
% if upstream_info.upstream_ref:
|
|
data-upstream-ref = ${upstream_info.upstream_ref}
|
|
data-version-synced = ${upstream_info.version_synced}
|
|
%endif
|
|
>
|
|
<div class="header-details">
|
|
% if show_inline:
|
|
<a href="#" data-tooltip="${_('Expand or Collapse')}" class="action expand-collapse collapse">
|
|
<span class="icon fa fa-caret-down ui-toggle-expansion" aria-hidden="true"></span>
|
|
<span class="sr">${_('Expand or Collapse')}</span>
|
|
</a>
|
|
% endif
|
|
<div class="xblock-display-title">
|
|
% if selectable:
|
|
<input type="checkbox" name="library-component" id="${xblock.location}"
|
|
class="header-library-checkbox" ${'checked="checked"' if is_selected else ''}>
|
|
<label for="${xblock.location}" class="xblock-display-name header-library-checkbox-label">
|
|
<span class="sr-only">${_("Select this problem")}</span>
|
|
${label}
|
|
</label>
|
|
% else:
|
|
% if upstream_info.upstream_ref:
|
|
% if upstream_info.error_message:
|
|
<!-- "broken link" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:link_off:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24&icon.color=%235f6368&icon.query=link -->
|
|
<svg role="img" data-tooltip="${_("Sourced from a library - but the upstream link is broken/invalid.")}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" style="vertical-align: middle; padding-bottom: 4px;">
|
|
<path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/>
|
|
</svg>
|
|
<span class="sr-only">${_("Sourced from a library - but the upstream link is broken/invalid.")}</span>
|
|
% else:
|
|
<!-- "library" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:newsstand:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24 -->
|
|
<svg role="img" data-tooltip="${_("Sourced from a library.")}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle; padding-bottom: 4px;">
|
|
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z"/>
|
|
</svg>
|
|
<span class="sr-only">${_("Sourced from a library.")}</span>
|
|
% endif
|
|
% endif
|
|
<span class="xblock-display-name">${label}</span>
|
|
% endif
|
|
% if selected_groups_label:
|
|
<p class="xblock-group-visibility-label">${selected_groups_label}</p>
|
|
% endif
|
|
</div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<ul class="actions-list nav-dd ui-right">
|
|
% if not is_root:
|
|
% if can_edit:
|
|
% if upstream_info.ready_to_sync:
|
|
<li class="action-item">
|
|
<button
|
|
class="btn-default library-sync-button action-button"
|
|
data-tooltip="${_("Update available - click to sync")}"
|
|
>
|
|
<span class="icon fa fa-refresh" aria-hidden="true"></span>
|
|
<span class="action-button-text">${_("Update available")}</span>
|
|
</button>
|
|
</li>
|
|
% endif
|
|
% if use_tagging:
|
|
<li class="action-item tag-count" data-locator="${xblock.location}"></li>
|
|
% endif
|
|
% if not show_inline:
|
|
<li class="action-item action-edit">
|
|
<button class="btn-default edit-button action-button" data-usage-id=${xblock.scope_ids.usage_id}>
|
|
<span class="icon fa fa-pencil" aria-hidden="true"></span>
|
|
<span class="action-button-text">${_("Edit")}</span>
|
|
</button>
|
|
</li>
|
|
% endif
|
|
<li class="action-item action-actions-menu nav-item">
|
|
<button data-tooltip="${_("Actions")}" class="btn-default show-actions-menu-button action-button">
|
|
<span class="icon fa fa-ellipsis-v" aria-hidden="true"></span>
|
|
<span class="sr">${_("Actions")}</span>
|
|
</button>
|
|
<div class="wrapper wrapper-nav-sub" style="right: -10px; top: 45px;">
|
|
<div class="nav-sub">
|
|
<ul>
|
|
% if not show_inline:
|
|
% if can_edit_visibility:
|
|
<li class="nav-item">
|
|
<a class="access-button" href="#" role="button">${_("Manage Access")}</a>
|
|
</li>
|
|
% endif
|
|
% if can_move:
|
|
<li class="nav-item">
|
|
<a class="move-button" href="#" role="button">${_("Move")}</a>
|
|
</li>
|
|
% endif
|
|
% if use_tagging:
|
|
<li class="nav-item">
|
|
<a class="manage-tags-button" href="#" role="button">${_("Manage Tags")}</a>
|
|
</li>
|
|
% endif
|
|
% if is_course:
|
|
<!--
|
|
Only show the "Copy to Clipboard" button for xblocks inside courses since
|
|
the copy/paste functionality is not yet implemented for LibraryContent.
|
|
-->
|
|
<li class="nav-item">
|
|
<a class="copy-button" href="#" role="button">${_("Copy to Clipboard")}</a>
|
|
</li>
|
|
% endif
|
|
% if can_add:
|
|
<li class="nav-item">
|
|
<a class="duplicate-button" href="#" role="button">${_("Duplicate")}</a>
|
|
</li>
|
|
% endif
|
|
% endif
|
|
% if can_delete:
|
|
<li class="nav-item">
|
|
<a class="delete-button" href="#" role="button">${_("Delete")}</a>
|
|
</li>
|
|
% endif
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
% if is_reorderable:
|
|
<li class="action-item action-drag">
|
|
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
|
|
</li>
|
|
% endif
|
|
% if not show_inline:
|
|
<li class="action-item action-edit action-edit-view-only">
|
|
<a href="#" class="edit-button action-button">
|
|
<span class="action-button-text">${_("Details")}</span>
|
|
</a>
|
|
</li>
|
|
% endif
|
|
% endif
|
|
% endif
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
% if not is_root:
|
|
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location}"/>
|
|
% if xblock_url and not has_not_configured_message:
|
|
<div class="xblock-header-secondary">
|
|
<div class="meta-info">${_('This block contains multiple components.')}</div>
|
|
<ul class="actions-list">
|
|
<li class="action-item action-view">
|
|
<a href="${xblock_url}" class="action-button xblock-view-action-button">
|
|
## Translators: this is a verb describing the action of viewing more details
|
|
<span class="action-button-text">${('View')}</span>
|
|
<span class="icon fa fa-arrow-right" aria-hidden="true"></span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
% endif
|
|
% endif
|
|
</header>
|
|
|
|
% if is_root:
|
|
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location}"/>
|
|
% endif
|
|
|
|
% if show_preview:
|
|
% if is_root or not xblock_url:
|
|
% if not is_root and language:
|
|
<article class="xblock-render" lang="${language}">
|
|
% else:
|
|
<article class="xblock-render">
|
|
% endif
|
|
${content | n, decode.utf8}
|
|
</article>
|
|
% else:
|
|
<div class="xblock-message-area">
|
|
${content | n, decode.utf8}
|
|
</div>
|
|
% endif
|
|
% endif
|
|
|
|
% if not is_root:
|
|
<!-- footer for xblock_aside -->
|
|
</section>
|
|
|
|
% if is_reorderable:
|
|
</li>
|
|
% else:
|
|
</div>
|
|
% endif
|
|
% endif
|