Commit 2211ae33 by Nimisha Asthagiri Committed by GitHub

Merge pull request #16304 from edx/pacing/studio-updates

Dynamic pacing: Studio support of Section Highlights
parents 59490244 ee1a003c
......@@ -1182,6 +1182,11 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info.update({
'hide_after_due': xblock.hide_after_due,
})
elif xblock.category == 'chapter':
xblock_info.update({
'highlights': xblock.highlights,
'highlights_enabled': settings.FEATURES.get('ENABLE_SECTION_HIGHLIGHTS', False),
})
# update xblock_info with special exam information if the feature flag is enabled
if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
......
......@@ -2389,7 +2389,8 @@ class TestXBlockInfo(ItemTest):
super(TestXBlockInfo, self).setUp()
user_id = self.user.id
self.chapter = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name="Week 1", user_id=user_id
parent_location=self.course.location, category='chapter', display_name="Week 1", user_id=user_id,
highlights=['highlight'],
)
self.sequential = ItemFactory.create(
parent_location=self.chapter.location, category='sequential', display_name="Lesson 1", user_id=user_id
......@@ -2596,6 +2597,7 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['graded'], False)
self.assertEqual(xblock_info['due'], None)
self.assertEqual(xblock_info['format'], None)
self.assertEqual(xblock_info['highlights'], self.chapter.highlights)
# Finally, validate the entire response for consistency
self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info)
......
......@@ -264,7 +264,10 @@ FEATURES = {
# Whether archived courses (courses with end dates in the past) should be
# shown in Studio in a separate list.
'ENABLE_SEPARATE_ARCHIVED_COURSES': True
'ENABLE_SEPARATE_ARCHIVED_COURSES': True,
# Whether section-level highlights are enabled on the platform.
'ENABLE_SECTION_HIGHLIGHTS': False,
}
ENABLE_JASMINE = False
......
......@@ -326,6 +326,8 @@ SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = True
FEATURES['ENABLE_SECTION_HIGHLIGHTS'] = True
########################## AUTHOR PERMISSION #######################
FEATURES['ENABLE_CREATOR_GROUP'] = False
......
......@@ -159,7 +159,12 @@ function(Backbone, _, str, ModuleUtils) {
* some additional fields that are not stored in the course descriptor
* (for example, which groups are selected for this particular XBlock).
*/
user_partitions: null
user_partitions: null,
/**
* This xBlock's Highlights to message to learners.
*/
highlights: null,
highlights_enabled: null
},
initialize: function() {
......
......@@ -7,7 +7,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, getItemsOfType, getItemHeaders,
verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, selectBasicSettings,
selectVisibilitySettings, selectAdvancedSettings, createMockCourseJSON, createMockSectionJSON,
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON,
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced,
mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore');
......@@ -55,7 +55,9 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
},
user_partitions: [],
group_access: {},
user_partition_info: {}
user_partition_info: {},
highlights: [],
highlights_enabled: true
}, options, {child_info: {children: children}});
};
......@@ -160,6 +162,11 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
this.$(".modal-section .settings-tab-button[data-tab='advanced']").click();
};
setSelfPaced = function() {
/* global course */
course.set('self_paced', true);
};
createCourseOutlinePage = function(test, courseJSON, createOnly) {
requests = AjaxHelpers.requests(test);
model = new XBlockOutlineInfo(courseJSON, {parse: true});
......@@ -255,7 +262,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor', 'unit-access-editor', 'content-visibility-editor',
'settings-modal-tabs', 'timed-examination-preference-editor', 'access-editor',
'show-correctness-editor'
'show-correctness-editor', 'highlights-editor'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON({}, [
......@@ -522,6 +529,166 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
});
});
describe('Section Highlights', function() {
var createCourse, createCourseWithHighlights, createCourseWithHighlightsDisabled, mockHighlightValues,
highlightsLink, highlightInputs, openHighlights, saveHighlights, setHighlights,
expectHighlightLinkTextToBe, expectHighlightsToBe, expectServerHandshakeWithHighlights,
expectHighlightsToUpdate,
maxNumHighlights = 5;
beforeEach(function() {
setSelfPaced();
});
createCourse = function(sectionOptions) {
createCourseOutlinePage(this,
createMockCourseJSON({}, [
createMockSectionJSON(sectionOptions)
])
);
};
createCourseWithHighlights = function(highlights) {
createCourse({highlights: highlights});
};
createCourseWithHighlightsDisabled = function() {
createCourse({highlights_enabled: false});
};
mockHighlightValues = function(numberOfHighlights) {
var highlights = [],
i;
for (i = 0; i < numberOfHighlights; i++) {
highlights.push('Highlight' + (i + 1));
}
return highlights;
};
highlightsLink = function() {
return outlinePage.$('.section-status >> .highlights-button');
};
highlightInputs = function() {
return $('.highlight-input-text');
};
openHighlights = function() {
highlightsLink().click();
};
saveHighlights = function() {
$('.wrapper-modal-window .action-save').click();
};
setHighlights = function(highlights) {
var i;
for (i = 0; i < highlights.length; i++) {
$(highlightInputs()[i]).val(highlights[i]);
}
for (i = highlights.length; i < maxNumHighlights; i++) {
$(highlightInputs()[i]).val('');
}
};
expectHighlightLinkTextToBe = function(expectedValue) {
expect(highlightsLink()).toContainText(expectedValue);
};
expectHighlightsToBe = function(expectedHighlights) {
var highlights = highlightInputs(),
i;
expect(highlights).toHaveLength(maxNumHighlights);
for (i = 0; i < expectedHighlights.length; i++) {
expect(highlights[i]).toHaveValue(expectedHighlights[i]);
}
for (i = expectedHighlights.length; i < maxNumHighlights; i++) {
expect(highlights[i]).toHaveValue('');
expect(highlights[i]).toHaveAttr('placeholder', 'A highlight to look forward to this week.');
}
};
expectServerHandshakeWithHighlights = function(highlights) {
// POST to update section
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
publish: 'republish',
metadata: {
highlights: highlights
}
});
AjaxHelpers.respondWithJson(requests, {});
// GET updated section
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, createMockSectionJSON({highlights: highlights}));
};
expectHighlightsToUpdate = function(originalHighlights, updatedHighlights) {
createCourseWithHighlights(originalHighlights);
openHighlights();
setHighlights(updatedHighlights);
saveHighlights();
expectServerHandshakeWithHighlights(updatedHighlights);
openHighlights();
expectHighlightsToBe(updatedHighlights);
};
it('does not display a link when highlights is disabled', function() {
createCourseWithHighlightsDisabled();
expect(highlightsLink()).toHaveLength(0);
});
it('displays link when no highlights exist', function() {
createCourseWithHighlights([]);
expectHighlightLinkTextToBe('Enter Section Highlights');
});
it('displays link when highlights exist', function() {
var highlights = mockHighlightValues(2);
createCourseWithHighlights(highlights);
expectHighlightLinkTextToBe('Section Highlights: 2 entered');
});
it('can view when no highlights exist', function() {
createCourseWithHighlights([]);
openHighlights();
expectHighlightsToBe([]);
});
it('can view existing highlights', function() {
var highlights = mockHighlightValues(2);
createCourseWithHighlights(highlights);
openHighlights();
expectHighlightsToBe(highlights);
});
it('can add highlights', function() {
expectHighlightsToUpdate(
mockHighlightValues(0),
mockHighlightValues(1)
);
});
it('can remove highlights', function() {
expectHighlightsToUpdate(
mockHighlightValues(5),
mockHighlightValues(3)
);
});
it('can edit highlights', function() {
var originalHighlights = mockHighlightValues(3),
editedHighlights = originalHighlights;
editedHighlights[2] = 'A New Value';
expectHighlightsToUpdate(originalHighlights, editedHighlights);
});
});
describe('Section', function() {
var getDisplayNameWrapper;
......@@ -530,7 +697,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
};
it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy(), requestCount;
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, createMockCourseJSON({}, [
createMockSectionJSON(),
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})
......@@ -922,8 +1089,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
])
]);
createCourseOutlinePage(this, mockCourseJSON, false);
/* global course */
course.set('self_paced', true);
setSelfPaced();
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.edit-settings-release').length).toBe(0);
expect($('.grading-due-date').length).toBe(0);
......
......@@ -197,6 +197,19 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components
}
},
highlightsXBlock: function() {
var modal = CourseOutlineModalsFactory.getModal('highlights', this.model, {
onSave: this.refresh.bind(this),
xblockType: XBlockViewUtils.getXBlockType(
this.model.get('category'), this.parentView.model, true
)
});
if (modal) {
modal.show();
}
},
addButtonActions: function(element) {
XBlockOutlineView.prototype.addButtonActions.apply(this, arguments);
element.find('.configure-button').click(function(event) {
......@@ -207,6 +220,10 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components
event.preventDefault();
this.publishXBlock();
}.bind(this));
element.find('.highlights-button').click(function(event) {
event.preventDefault();
this.highlightsXBlock();
}.bind(this));
},
makeContentDraggable: function(element) {
......
......@@ -5,7 +5,7 @@
*
* getTitle():
* returns the title for the modal.
* getHTMLContent():
* getContentHtml():
* returns the HTML content to be shown inside the modal.
*
* A modal implementation should also provide the following options:
......
......@@ -13,10 +13,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
$, Backbone, _, gettext, BaseView, BaseModal, date, XBlockViewUtils, DateUtils, HtmlUtils, StringUtils
) {
'use strict';
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, HighlightsXBlockModal,
AbstractEditor, BaseDateEditor,
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor,
StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor,
AccessEditor, ShowCorrectnessEditor;
AccessEditor, ShowCorrectnessEditor, HighlightsEditor;
CourseOutlineXBlockModal = BaseModal.extend({
events: _.extend({}, BaseModal.prototype.events, {
......@@ -206,6 +207,38 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
});
HighlightsXBlockModal = CourseOutlineXBlockModal.extend({
initialize: function() {
CourseOutlineXBlockModal.prototype.initialize.call(this);
if (this.options.xblockType) {
this.options.modalName = 'highlights-' + this.options.xblockType;
}
},
getTitle: function() {
return StringUtils.interpolate(
gettext('Highlights for {display_name}'),
{display_name: this.model.get('display_name')}
);
},
getIntroductionMessage: function() {
return StringUtils.interpolate(
gettext(
'The highlights you provide here are messaged (i.e., emailed) to learners. Each {item}\'s ' +
'highlights are emailed at the time that we expect the learner to start working on that {item}. ' +
'At this time, we assume that each {item} will take 1 week to complete.'
),
{item: this.options.xblockType}
);
},
addActionButtons: function() {
this.addActionButton('save', gettext('Save'), true);
this.addActionButton('cancel', gettext('Cancel'));
}
});
AbstractEditor = BaseView.extend({
tagName: 'section',
templateName: null,
......@@ -844,12 +877,58 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
});
HighlightsEditor = AbstractEditor.extend({
templateName: 'highlights-editor',
className: 'edit-show-highlights',
currentValue: function() {
var highlights = [];
$('.highlight-input-text').each(function() {
var value = $(this).val();
if (value !== '' && value !== null) {
highlights.push(value);
}
});
return highlights;
},
hasChanges: function() {
return this.model.get('highlights') !== this.currentValue();
},
getRequestData: function() {
if (this.hasChanges()) {
return {
publish: 'republish',
metadata: {
highlights: this.currentValue()
}
};
} else {
return {};
}
},
getContext: function() {
return $.extend(
{},
AbstractEditor.prototype.getContext.call(this),
{
highlights: this.model.get('highlights') || []
}
);
}
});
return {
getModal: function(type, xblockInfo, options) {
if (type === 'edit') {
return this.getEditModal(xblockInfo, options);
} else if (type === 'publish') {
return this.getPublishModal(xblockInfo, options);
} else if (type === 'highlights') {
return this.getHighlightsModal(xblockInfo, options);
} else {
return null;
}
},
......@@ -918,6 +997,13 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
editors: [PublishEditor],
model: xblockInfo
}, options));
},
getHighlightsModal: function(xblockInfo, options) {
return new HighlightsXBlockModal($.extend({
editors: [HighlightsEditor],
model: xblockInfo
}, options));
}
};
});
......@@ -634,6 +634,23 @@
}
}
// outline: highlight settings
.highlights-button {
cursor: pointer;
color: $uxpl-blue-base;
}
.highlight-input-text {
width: 100%;
margin-bottom: 5px;
margin-top: 5px;
}
.highlights-description {
font-size: 80%;
font-weight: bolder;
}
// outline: edit item settings
.wrapper-modal-window-bulkpublish-section,
.wrapper-modal-window-bulkpublish-subsection,
......
......@@ -26,7 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text
<%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor']:
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
......
......@@ -200,6 +200,22 @@ if (is_proctored_exam) {
</p>
</div>
<% } %>
<% if (xblockInfo.get('highlights_enabled') && course.get('self_paced') && xblockInfo.isChapter()) { %>
<div class="block-highlights">
<span class="sr block-highlights-label"><%- gettext('Highlights:') %></span>
<% var number_of_highlights = (xblockInfo.get('highlights') || []).length; %>
<% if (number_of_highlights > 0) { %>
<span class="block-highlights-value highlights-button action-button">
<%- edx.StringUtils.interpolate(
gettext('Section Highlights: {number_of_highlights} entered'),
{number_of_highlights: number_of_highlights}
) %>
</span>
<% } else { %>
<span class="block-highlights-value highlights-button action-button"><%- gettext('Enter Section Highlights') %></span>
<% } %>
</div>
<% } %>
<% if (xblockInfo.get('is_time_limited')) { %>
<div class="status-timed-proctored-exam">
<p>
......
<form>
<h3 class="modal-section-title" id="highlights_label"><%- gettext('Section Highlights') %></h3>
<div class="modal-section-content block-highlights">
<div role="group" class="list-fields" aria-labelledby="highlights_label">
<p class='field-message highlights-description' id='highlights_description'>
<%- gettext('Please enter 3-5 highlights to be sent as separate bullet points in the message.') %>
</p>
<%
var max_number_of_highlights = 5;
%>
<% _.each(highlights, function(highlight){ %>
<input
class="input input-text highlight-input-text"
type="text" maxlength="250" aria-describedby="highlights_description"
value="<%= _.escape(highlight) %>"
/>
<% }); %>
<% for (i = highlights.length; i < max_number_of_highlights; i++) { %>
<input
class="input input-text highlight-input-text"
type="text" maxlength="250" aria-describedby="highlights_description"
placeholder="<%- gettext('A highlight to look forward to this week.') %>"
/>
<% } %>
</div>
</div>
</form>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment