This fixes a bug where some users would see an error that prevented the course outline page in Studio from loading, for any course.
493 lines
26 KiB
Plaintext
493 lines
26 KiB
Plaintext
<%
|
|
var releasedToStudents = xblockInfo.get('released_to_students');
|
|
var visibilityState = xblockInfo.get('visibility_state');
|
|
var published = xblockInfo.get('published');
|
|
var prereq = xblockInfo.get('prereq');
|
|
var hasPartitionGroups = xblockInfo.get('has_partition_group_components');
|
|
var userPartitionInfo = xblockInfo.get('user_partition_info');
|
|
var selectedGroupsLabel = userPartitionInfo['selected_groups_label'];
|
|
var selectedPartitionIndex = userPartitionInfo['selected_partition_index'];
|
|
|
|
var statusMessages = [];
|
|
var messageType;
|
|
var messageText;
|
|
var statusType = null;
|
|
var addStatusMessage = function (statusType, message) {
|
|
var statusIconClass = '';
|
|
if (statusType === 'warning') {
|
|
statusIconClass = 'fa-file-o';
|
|
} else if (statusType === 'error') {
|
|
statusIconClass = 'fa-warning';
|
|
} else if (statusType === 'staff-only' || statusType === 'gated') {
|
|
statusIconClass = 'fa-lock';
|
|
} else if (statusType === 'partition-groups') {
|
|
statusIconClass = 'fa-eye';
|
|
}
|
|
|
|
statusMessages.push({iconClass: statusIconClass, text: message});
|
|
};
|
|
|
|
if (prereq) {
|
|
var prereqDisplayName = '';
|
|
_.each(xblockInfo.get('prereqs'), function (p) {
|
|
if (p.block_usage_key == prereq) {
|
|
prereqDisplayName = p.block_display_name;
|
|
return false;
|
|
}
|
|
});
|
|
messageType = 'gated';
|
|
messageText = interpolate(
|
|
gettext('Prerequisite: %(prereq_display_name)s'),
|
|
{prereq_display_name: prereqDisplayName},
|
|
true
|
|
);
|
|
addStatusMessage(messageType, messageText);
|
|
}
|
|
if (staffOnlyMessage) {
|
|
messageType = 'staff-only';
|
|
messageText = gettext('Contains staff only content');
|
|
addStatusMessage(messageType, messageText);
|
|
} else {
|
|
if (visibilityState === 'needs_attention' && xblockInfo.isVertical()) {
|
|
messageType = 'warning';
|
|
if (published && releasedToStudents) {
|
|
messageText = gettext('Unpublished changes to live content');
|
|
} else if (!published) {
|
|
messageText = gettext('Unpublished units will not be released');
|
|
} else {
|
|
messageText = gettext('Unpublished changes to content that will release in the future');
|
|
}
|
|
addStatusMessage(messageType, messageText);
|
|
}
|
|
|
|
if (selectedPartitionIndex !== -1 && !isNaN(selectedPartitionIndex) && xblockInfo.isVertical()) {
|
|
messageType = 'partition-groups';
|
|
messageText = edx.StringUtils.interpolate(
|
|
gettext('Access to this unit is restricted to: {selectedGroupsLabel}'),
|
|
{
|
|
selectedGroupsLabel: selectedGroupsLabel
|
|
}
|
|
)
|
|
addStatusMessage(messageType, messageText);
|
|
} else if (hasPartitionGroups && xblockInfo.isVertical()) {
|
|
addStatusMessage(
|
|
'partition-groups',
|
|
gettext('Access to some content in this unit is restricted to specific groups of learners')
|
|
);
|
|
}
|
|
}
|
|
|
|
var gradingType = gettext('Ungraded');
|
|
var gradingPolicyMismatch = false;
|
|
if (xblockInfo.get('graded')) {
|
|
gradingType = xblockInfo.get('format')
|
|
if (gradingType) {
|
|
gradingPolicyMismatch = (
|
|
xblockInfo.get('course_graders').filter((cg) => cg.toLowerCase() === gradingType.toLowerCase())
|
|
).length === 0;
|
|
}
|
|
}
|
|
|
|
var is_proctored_exam = xblockInfo.get('is_proctored_exam');
|
|
var is_practice_exam = xblockInfo.get('is_practice_exam');
|
|
var is_onboarding_exam = xblockInfo.get('is_onboarding_exam');
|
|
var exam_value;
|
|
if (is_proctored_exam) {
|
|
if (is_onboarding_exam) {
|
|
exam_value = gettext('Onboarding Exam');
|
|
} else if (is_practice_exam) {
|
|
exam_value = gettext('Practice proctored Exam');
|
|
} else {
|
|
exam_value = gettext('Proctored Exam');
|
|
}
|
|
|
|
} else {
|
|
exam_value = gettext('Timed Exam');
|
|
}
|
|
%>
|
|
<% if (parentInfo) { %>
|
|
<li class="outline-item outline-<%- xblockType %> <%- visibilityClass %> is-draggable <%- includesChildren ? 'is-collapsible' : '' %> <%- isCollapsed ? 'is-collapsed' : '' %>"
|
|
data-parent="<%- parentInfo.get('id') %>" data-locator="<%- xblockInfo.get('id') %>" id="<%- xblockInfo.get('id') %>">
|
|
|
|
<span class="draggable-drop-indicator draggable-drop-indicator-before"><span class="icon fa fa-caret-right" aria-hidden="true"></span></span>
|
|
<% if (xblockInfo.isHeaderVisible()) { %>
|
|
<div class="<%- xblockType %>-header">
|
|
<% if (includesChildren) { %>
|
|
<h3 class="<%- xblockType %>-header-details expand-collapse <%- isCollapsed ? 'expand' : 'collapse' %> ui-toggle-expansion"
|
|
title="<%- interpolate(
|
|
gettext('Collapse/Expand this %(xblock_type)s'), { xblock_type: xblockTypeDisplayName }, true
|
|
) %>"
|
|
>
|
|
<span class="icon fa fa-caret-down" aria-hidden="true"></span>
|
|
<% } else { %>
|
|
<% if (xblockType == "unit") { %>
|
|
<h3 class="unit-header-details" style="width: 50%">
|
|
<% } else { %>
|
|
<h3 class="<%- xblockType %>-header-details">
|
|
<% } %>
|
|
<% } %>
|
|
<% if (xblockInfo.isVertical()) { %>
|
|
<span class="unit-title item-title">
|
|
<span class="wrapper-<%- xblockType %>-title wrapper-xblock-field incontext-editor is-editable" data-field="display_name" data-field-display-name="<%- gettext("Display Name") %>">
|
|
<a class="<%- xblockType %>-title item-title xblock-field-value incontext-editor-value" href="<%- xblockInfo.get('studio_url') %>"><%- xblockInfo.get('display_name') %></a>
|
|
</span>
|
|
</span>
|
|
<% } else { %>
|
|
<span class="wrapper-<%- xblockType %>-title wrapper-xblock-field incontext-editor is-editable" data-field="display_name" data-field-display-name="<%- gettext("Display Name") %>">
|
|
<span class="<%- xblockType %>-title item-title xblock-field-value incontext-editor-value"><%- xblockInfo.get('display_name') %></span>
|
|
</span>
|
|
<% } %>
|
|
</h3>
|
|
<% if (xblockType == "unit") {%>
|
|
<div class="unit-header-actions" style="width: 50%">
|
|
<% } else { %>
|
|
<div class="<%- xblockType %>-header-actions">
|
|
<% } %>
|
|
<ul class="actions-list nav-dd ui-right">
|
|
<% var discussion_settings = course.get('discussions_settings') %>
|
|
<% if ((xblockInfo.isVertical()) && discussion_settings && (!parentInfo.get('is_time_limited'))) { %>
|
|
<% if (xblockInfo.get('discussion_enabled') && (discussion_settings.provider_type == "openedx")) { %>
|
|
<% if (discussion_settings.enable_graded_units || (!discussion_settings.enable_graded_units && !parentInfo.get('graded'))) {%>
|
|
<li class="action-item" style="font-size: 75%; color:grey">
|
|
<%- gettext('Discussions enabled') %>
|
|
</li>
|
|
<% } %>
|
|
<% } %>
|
|
<% } %>
|
|
<% if (xblockInfo.isSequential() && xblockInfo.get('proctoring_exam_configuration_link')) { %>
|
|
<li class="action-item">
|
|
<a href="<%- xblockInfo.get('proctoring_exam_configuration_link') %>" data-tooltip="<%- gettext('Proctoring Settings') %>" class="proctoring-configuration-button" rel="noopener" target="_blank">
|
|
<span class="action-button-text"><%- gettext('Proctoring Settings') %></span>
|
|
</a>
|
|
</li>
|
|
<% } %>
|
|
<% if (xblockInfo.isPublishable()) { %>
|
|
<li class="action-item action-publish">
|
|
<a href="#" data-tooltip="<%- gettext('Publish') %>" class="publish-button action-button">
|
|
<span class="icon fa fa-upload" aria-hidden="true"></span>
|
|
<span class="sr action-button-text"><%- gettext('Publish') %></span>
|
|
</a>
|
|
</li>
|
|
<% } %>
|
|
<% if (typeof enableCopyPasteUnits !== "undefined" && enableCopyPasteUnits) { %>
|
|
<!--
|
|
If the ENABLE_COPY_PASTE_UNITS feature flag is enabled, all these actions (besides "Publish")
|
|
appear in a menu. We use .nav-dd on the parent element and .nav-item on this button to get the
|
|
same dropdown menu appearance and behavior as in Studio's various other nav bars.
|
|
-->
|
|
<li class="action-item action-actions-menu nav-item">
|
|
<button data-tooltip="<%- gettext('Actions') %>" class="btn-default show-actions-menu-button action-button">
|
|
<span class="icon fa fa-ellipsis-v" aria-hidden="true"></span>
|
|
<span class="sr"><%- gettext('Actions') %></span>
|
|
</button>
|
|
<div class="wrapper wrapper-nav-sub" style="right: -10px; top: 45px;">
|
|
<div class="nav-sub">
|
|
<ul>
|
|
<% if (xblockInfo.isEditableOnCourseOutline()) { %>
|
|
<li class="nav-item">
|
|
<a class="configure-button" href="#" role="button"><%- gettext('Configure') %></a>
|
|
</li>
|
|
<% } %>
|
|
<% if (xblockInfo.isVertical()) { %>
|
|
<li class="nav-item">
|
|
<a class="copy-button" href="#" role="button"><%- gettext('Copy to Clipboard') %></a>
|
|
</li>
|
|
<% } %>
|
|
<% if (xblockInfo.isDuplicable()) { %>
|
|
<li class="nav-item">
|
|
<a class="duplicate-button" href="#" role="button"><%- gettext('Duplicate') %></a>
|
|
</li>
|
|
<% } %>
|
|
<% if (xblockInfo.isDeletable()) { %>
|
|
<li class="nav-item">
|
|
<a class="delete-button" href="#" role="button"><%- gettext('Delete') %></a>
|
|
</li>
|
|
<% } %>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<% } else { %>
|
|
<% if (xblockInfo.isEditableOnCourseOutline()) { %>
|
|
<li class="action-item action-configure">
|
|
<a href="#" data-tooltip="<%- gettext('Configure') %>" class="configure-button action-button">
|
|
<span class="icon fa fa-gear" aria-hidden="true"></span>
|
|
<span class="sr action-button-text"><%- gettext('Configure') %></span>
|
|
</a>
|
|
</li>
|
|
<% } %>
|
|
<% if (xblockInfo.isDuplicable()) { %>
|
|
<li class="action-item action-duplicate">
|
|
<a href="#" data-tooltip="<%- gettext('Duplicate') %>" class="duplicate-button action-button">
|
|
<span class="icon fa fa-copy" aria-hidden="true"></span>
|
|
<span class="sr action-button-text"><%- gettext('Duplicate') %></span>
|
|
</a>
|
|
</li>
|
|
<% } %>
|
|
<% if (xblockInfo.isDeletable()) { %>
|
|
<li class="action-item action-delete">
|
|
<a href="#" data-tooltip="<%- gettext('Delete') %>" class="delete-button action-button">
|
|
<span class="icon fa fa-trash-o" aria-hidden="true"></span>
|
|
<span class="sr action-button-text"><%- gettext('Delete') %></span>
|
|
</a>
|
|
</li>
|
|
<% } %>
|
|
<% } %>
|
|
<% if (xblockInfo.isDraggable()) { %>
|
|
<li class="action-item action-drag">
|
|
<span data-tooltip="<%- gettext('Drag to reorder') %>"
|
|
class="drag-handle <%- xblockType %>-drag-handle action">
|
|
<span class="sr"><%- gettext('Drag to reorder') %></span>
|
|
</span>
|
|
</li>
|
|
<% } %>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="<%- xblockType %>-status">
|
|
<% if (!xblockInfo.isVertical()) { %>
|
|
<% if (xblockInfo.get('explanatory_message') !=null) { %>
|
|
<div class="explanatory-message">
|
|
<span>
|
|
<%- xblockInfo.get('explanatory_message') %>
|
|
</span>
|
|
</div>
|
|
<% } else { %>
|
|
<div class="status-release">
|
|
<p>
|
|
<span class="sr status-release-label"><%- gettext('Release Status:') %></span>
|
|
<span class="status-release-value">
|
|
<% if (!course.get('self_paced')) { %>
|
|
<% if (xblockInfo.get('released_to_students')) { %>
|
|
<span class="icon fa fa-check" aria-hidden="true"></span>
|
|
<%- gettext('Released:') %>
|
|
<% } else if (xblockInfo.get('release_date')) { %>
|
|
<span class="icon fa fa-clock-o" aria-hidden="true"></span>
|
|
<%- gettext('Scheduled:') %>
|
|
<% } else { %>
|
|
<span class="icon fa fa-clock-o" aria-hidden="true"></span>
|
|
<%- gettext('Unscheduled') %>
|
|
<% } %>
|
|
<% if (xblockInfo.get('release_date')) { %>
|
|
<%- xblockInfo.get('release_date') %>
|
|
<% } %>
|
|
<% } %>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<% } %>
|
|
<% if (xblockInfo.isChapter()) { %>
|
|
<div class="block-highlights">
|
|
<% var number_of_highlights = (xblockInfo.get('highlights') || []).length; %>
|
|
<button class="block-highlights-value highlights-button action-button">
|
|
<span class="number-highlights"><%- number_of_highlights %></span>
|
|
<%- gettext('Section Highlights') %>
|
|
</button>
|
|
</div>
|
|
<% } %>
|
|
<% if (xblockInfo.get('is_time_limited')) { %>
|
|
<div class="status-timed-proctored-exam">
|
|
<p>
|
|
<span class="sr status-grading-label"> <%- gettext('Graded as:') %> </span>
|
|
<span class="icon fa fa-check" aria-hidden="true"></span>
|
|
<span class="status-grading-value"> <%- gradingType %> </span>
|
|
-
|
|
<span class="sr status-proctored-exam-label"> <%- exam_value %> </span>
|
|
<span class="status-proctored-exam-value"> <%- exam_value %> </span>
|
|
<% if (xblockInfo.get('due_date') && !course.get('self_paced')) { %>
|
|
<span class="status-grading-date"> <%- gettext('Due:') %> <%- xblockInfo.get('due_date') %> </span>
|
|
<% } %>
|
|
</p>
|
|
</div>
|
|
<% if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
|
|
<div class="status-grading">
|
|
<p>
|
|
<span class="icon fa fa-calendar" aria-hidden="true"></span>
|
|
<span class="status-custom-grading-date">
|
|
<%- edx.StringUtils.interpolate(
|
|
ngettext(
|
|
'Custom due date: {relativeWeeks} week from enrollment',
|
|
'Custom due date: {relativeWeeks} weeks from enrollment',
|
|
xblockInfo.get('relative_weeks_due')),
|
|
{
|
|
relativeWeeks: xblockInfo.get('relative_weeks_due')
|
|
}
|
|
)
|
|
%>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<% } %>
|
|
<% } else if ((xblockInfo.get('due_date') && !course.get('self_paced')) || xblockInfo.get('graded')) { %>
|
|
<div class="status-grading">
|
|
<p>
|
|
<span class="sr status-grading-label"> <%- gettext('Graded as:') %> </span>
|
|
<span class="icon fa fa-check" aria-hidden="true"></span>
|
|
<span class="status-grading-value"> <%- gradingType %> </span>
|
|
<% if (xblockInfo.get('due_date') && !course.get('self_paced')) { %>
|
|
<span class="status-grading-date"> <%- gettext('Due:') %> <%- xblockInfo.get('due_date') %> </span>
|
|
<% } %>
|
|
</p>
|
|
</div>
|
|
<% if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
|
|
<div class="status-grading">
|
|
<p>
|
|
<span class="icon fa fa-calendar" aria-hidden="true"></span>
|
|
<span class="status-custom-grading-date">
|
|
<%- edx.StringUtils.interpolate(
|
|
ngettext(
|
|
'Custom due date: {relativeWeeks} week from enrollment',
|
|
'Custom due date: {relativeWeeks} weeks from enrollment',
|
|
xblockInfo.get('relative_weeks_due')),
|
|
{
|
|
relativeWeeks: xblockInfo.get('relative_weeks_due')
|
|
}
|
|
)
|
|
%>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<% } %>
|
|
<% } else if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
|
|
<div class="status-grading">
|
|
<p>
|
|
<span class="sr status-grading-label"> <%- gettext('Graded as:') %> </span>
|
|
<span class="icon fa fa-check" aria-hidden="true"></span>
|
|
<span class="status-grading-value"> <%- gradingType %> </span>
|
|
</p>
|
|
</div>
|
|
<div class="status-grading">
|
|
<p>
|
|
<span class="icon fa fa-calendar" aria-hidden="true"></span>
|
|
<span class="status-custom-grading-date">
|
|
<%- edx.StringUtils.interpolate(
|
|
ngettext(
|
|
'Custom due date: {relativeWeeks} week from enrollment',
|
|
'Custom due date: {relativeWeeks} weeks from enrollment',
|
|
xblockInfo.get('relative_weeks_due')),
|
|
{
|
|
relativeWeeks: xblockInfo.get('relative_weeks_due')
|
|
}
|
|
)
|
|
%>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<% } %>
|
|
<div class="status-hide-after-due">
|
|
<p>
|
|
<% if (xblockInfo.get('hide_after_due')) { %>
|
|
<span class="icon fa fa-eye-slash" aria-hidden="true"></span>
|
|
<span class="status-hide-after-due-value">
|
|
<% if (course.get('self_paced')) { %>
|
|
<%- gettext("Subsection is hidden after course end date") %> </span>
|
|
<% } else { %>
|
|
<%- gettext("Subsection is hidden after due date") %> </span>
|
|
<% } %>
|
|
<% } %>
|
|
</p>
|
|
</div>
|
|
<% } %>
|
|
<% if (statusMessages.length > 0) { %>
|
|
<div class="status-messages">
|
|
<% for (var i=0; i<statusMessages.length; i++) { %>
|
|
<div class="status-message">
|
|
<span class="icon fa <%- statusMessages[i].iconClass %>" aria-hidden="true"></span>
|
|
<p class="status-message-copy"><%- statusMessages[i].text %></p>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
<% } %>
|
|
<% if (gradingPolicyMismatch) { %>
|
|
<div class="status-messages">
|
|
<div class="container-message wrapper-message">
|
|
<div class="message has-warnings">
|
|
<p class="warning">
|
|
<span class="icon fa fa-warning" aria-hidden="true"></span>
|
|
<%- interpolate(
|
|
gettext("This subsection is configured as \"%(gradingType)s\", which doesn't exist in the current grading policy."),
|
|
{ gradingType: gradingType },
|
|
true
|
|
) %>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
<% } %>
|
|
<% } %>
|
|
|
|
<% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %>
|
|
<div class="no-content add-section">
|
|
<p><%- gettext("You haven't added any content to this course yet.") %>
|
|
<a href="#" class="button button-new" data-category="<%- childCategory %>"
|
|
data-parent="<%- xblockInfo.get('id') %>" data-default-name="<%- defaultNewChildName %>"
|
|
title="<%- interpolate(
|
|
gettext('Click to add a new %(xblock_type)s'), { xblock_type: defaultNewChildName }, true
|
|
) %>"
|
|
>
|
|
<span class="icon fa fa-plus" aria-hidden="true"></span><%- addChildLabel %>
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<% } else if (!xblockInfo.isVertical()) { %>
|
|
<div class="outline-content <%- xblockType %>-content">
|
|
<ol class="<%- typeListClass %> is-sortable">
|
|
<li class="ui-splint ui-splint-indicator">
|
|
<span class="draggable-drop-indicator draggable-drop-indicator-initial"><span class="icon fa fa-caret-right" aria-hidden="true"></span></span>
|
|
</li>
|
|
</ol>
|
|
|
|
<% if (childType) { %>
|
|
<% if (xblockInfo.isChildAddable()) { %>
|
|
<div class="add-<%- childType %> add-item">
|
|
<a href="#" class="button button-new" data-category="<%- childCategory %>"
|
|
data-parent="<%- xblockInfo.get('id') %>" data-default-name="<%- defaultNewChildName %>"
|
|
title="<%- interpolate(
|
|
gettext('Click to add a new %(xblock_type)s'), { xblock_type: defaultNewChildName }, true
|
|
) %>"
|
|
>
|
|
<span class="icon fa fa-plus" aria-hidden="true"></span><%- addChildLabel %>
|
|
</a>
|
|
|
|
<!--
|
|
Technically this isn't pasting a "component" (leaf XBlock), but we re-use most of the same UI
|
|
elements and CSS from the Unit page's "paste component" button, so the class names are the same.
|
|
-->
|
|
<div
|
|
class="paste-component"
|
|
style="display: none;"
|
|
data-category="<%- childCategory %>"
|
|
data-parent="<%- xblockInfo.get('id') %>"
|
|
>
|
|
<button type="button" class="button paste-component-button">
|
|
<span class="icon fa fa-paste" aria-hidden="true"></span>
|
|
<%- interpolate(
|
|
gettext('Paste %(xblock_type)s'), { xblock_type: defaultNewChildName }, true
|
|
) %>
|
|
</button>
|
|
<div class="paste-component-whats-in-clipboard" tabindex="0">
|
|
<!-- These details get filled in by JavaScript code when it makes the paste button visible: -->
|
|
<a href="#" class="clipboard-details-popup" onClick="if (this.getAttribute('href') === '#') return false;" target="_blank">
|
|
<span class="fa fa-external-link" aria-hidden="true"></span>
|
|
<strong class="detail-block-name">Block Name</strong>
|
|
<span class="detail-block-type">Type</span>
|
|
<%- gettext("From:") %> <span class="detail-course-name">Course Name Goes Here</span>
|
|
</a>
|
|
<span class="icon fa fa-question-circle" aria-hidden="true"></span>
|
|
<%- gettext("What's in my clipboard?") %>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<% } %>
|
|
<% } %>
|
|
</div>
|
|
<% } %>
|
|
|
|
<% if (parentInfo) { %>
|
|
<span class="draggable-drop-indicator draggable-drop-indicator-after"><span class="icon fa fa-caret-right" aria-hidden="true"></span></span>
|
|
</li>
|
|
<% } %>
|