diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 5a11820e97..9ad65877de 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -1188,11 +1188,17 @@ 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': + elif xblock.category in ('chapter', 'course'): + if xblock.category == 'chapter': + xblock_info.update({ + 'highlights': xblock.highlights, + }) + elif xblock.category == 'course': + xblock_info.update({ + 'highlights_enabled_for_messaging': course.highlights_enabled_for_messaging, + }) xblock_info.update({ - 'highlights': xblock.highlights, 'highlights_enabled': highlights_setting.is_enabled(), - 'highlights_enabled_for_messaging': course.highlights_enabled_for_messaging, 'highlights_preview_only': not COURSE_UPDATE_WAFFLE_FLAG.is_enabled(course.id), 'highlights_doc_url': HelpUrlExpert.the_one().url_for_token('content_highlights'), }) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 5dd760fed6..959cac7360 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -2577,8 +2577,10 @@ class TestXBlockInfo(ItemTest): self.store.update_item(self.course, None) chapter = self.store.get_item(self.chapter.location) with highlights_setting.override(): - xblock_info = create_xblock_info(chapter) - self.assertTrue(xblock_info['highlights_enabled']) + chapter_xblock_info = create_xblock_info(chapter) + course_xblock_info = create_xblock_info(self.course) + self.assertTrue(chapter_xblock_info['highlights_enabled']) + self.assertTrue(course_xblock_info['highlights_enabled_for_messaging']) def validate_course_xblock_info(self, xblock_info, has_child_info=True, course_outline=False): """ @@ -2588,6 +2590,7 @@ class TestXBlockInfo(ItemTest): self.assertEqual(xblock_info['id'], unicode(self.course.location)) self.assertEqual(xblock_info['display_name'], self.course.display_name) self.assertTrue(xblock_info['published']) + self.assertFalse(xblock_info['highlights_enabled_for_messaging']) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info, course_outline=course_outline) @@ -2608,7 +2611,6 @@ class TestXBlockInfo(ItemTest): self.assertEqual(xblock_info['format'], None) self.assertEqual(xblock_info['highlights'], self.chapter.highlights) self.assertFalse(xblock_info['highlights_enabled']) - self.assertFalse(xblock_info['highlights_enabled_for_messaging']) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info) diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 8bd50a4b20..78eaaddea2 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -165,6 +165,7 @@ function(Backbone, _, str, ModuleUtils) { */ highlights: [], highlights_enabled: false, + highlights_enabled_for_messaging: false, highlights_preview_only: true, highlights_doc_url: '' }, diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index 9800a3261b..1dbbbaa5b0 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -32,7 +32,9 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j children: [] }, user_partitions: [], - user_partition_info: {} + user_partition_info: {}, + highlights_enabled: true, + highlights_enabled_for_messaging: false }, options, {child_info: {children: children}}); }; @@ -262,7 +264,8 @@ 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', 'highlights-editor' + 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', + 'course-highlights-enable' ]); appendSetFixtures(mockOutlinePage); mockCourseJSON = createMockCourseJSON({}, [ @@ -529,20 +532,17 @@ 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, - expectHighlightLinkNumberToBe, expectHighlightsToBe, expectServerHandshakeWithHighlights, - expectHighlightsToUpdate, - maxNumHighlights = 5; + describe('Content Highlights', function() { + var createCourse, createCourseWithHighlights, createCourseWithHighlightsDisabled, + clickSaveOnModal, clickCancelOnModal; beforeEach(function() { setSelfPaced(); }); - createCourse = function(sectionOptions) { + createCourse = function(sectionOptions, courseOptions) { createCourseOutlinePage(this, - createMockCourseJSON({}, [ + createMockCourseJSON(courseOptions, [ createMockSectionJSON(sectionOptions) ]) ); @@ -553,142 +553,256 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j }; createCourseWithHighlightsDisabled = function() { - createCourse({highlights_enabled: false}); + var highlightsDisabled = {highlights_enabled: false}; + createCourse(highlightsDisabled, highlightsDisabled); }; - 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() { + clickSaveOnModal = 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(''); - } + clickCancelOnModal = function() { + $('.wrapper-modal-window .action-cancel').click(); }; - expectHighlightLinkNumberToBe = function(expectedNumber) { - var link = highlightsLink(); - expect(link).toContainText('Section Highlights'); - expect(link.find('.number-highlights')).toHaveHtml(expectedNumber); - }; + describe('Course Highlights Setting', function() { + var highlightsSetting, expectHighlightsEnabledToBe, expectServerHandshake, openHighlightsSettings; - expectHighlightsToBe = function(expectedHighlights) { - var highlights = highlightInputs(), - i; + highlightsSetting = function() { + return $('.course-highlights-setting'); + }; - 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 + expectHighlightsEnabledToBe = function(expectedEnabled) { + if (expectedEnabled) { + expect('.status-highlights-enabled-value.button').not.toExist(); + expect('.status-highlights-enabled-value.text').toExist(); + } else { + expect('.status-highlights-enabled-value.button').toExist(); + expect('.status-highlights-enabled-value.text').not.toExist(); } + } + + expectServerHandshake = function() { + // POST to update course + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-course', { + publish: 'republish', + metadata: { + highlights_enabled_for_messaging: true + } + }); + AjaxHelpers.respondWithJson(requests, {}); + + // GET updated course + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); + AjaxHelpers.respondWithJson( + requests, createMockCourseJSON({highlights_enabled_for_messaging: true}) + ); + }; + + openHighlightsSettings = function() { + $('button.status-highlights-enabled-value').click(); + }; + + it('does not display settings when disabled', function() { + createCourseWithHighlightsDisabled(); + expect(highlightsSetting()).not.toExist(); }); - AjaxHelpers.respondWithJson(requests, {}); - // GET updated section - AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); - AjaxHelpers.respondWithJson(requests, createMockSectionJSON({highlights: highlights})); - }; + it('displays settings when enabled', function() { + createCourseWithHighlights([]); + expect(highlightsSetting()).toExist(); + }); - expectHighlightsToUpdate = function(originalHighlights, updatedHighlights) { - createCourseWithHighlights(originalHighlights); + it('displays settings as not enabled for messaging', function() { + createCourse(); + expectHighlightsEnabledToBe(false); + }); - openHighlights(); - setHighlights(updatedHighlights); - saveHighlights(); + it('displays settings as enabled for messaging', function() { + createCourse({}, {highlights_enabled_for_messaging: true}); + expectHighlightsEnabledToBe(true); + }); - expectServerHandshakeWithHighlights(updatedHighlights); - expectHighlightLinkNumberToBe(updatedHighlights.length); + it('changes settings when enabled for messaging', function() { + createCourse(); + openHighlightsSettings(); + clickSaveOnModal(); + expectServerHandshake(); + expectHighlightsEnabledToBe(true); + }); - openHighlights(); - expectHighlightsToBe(updatedHighlights); - }; - - it('does not display a link when highlights is disabled', function() { - createCourseWithHighlightsDisabled(); - expect(highlightsLink()).toHaveLength(0); + it('does not change settings when enabling is cancelled', function() { + createCourse(); + openHighlightsSettings(); + clickCancelOnModal(); + expectHighlightsEnabledToBe(false); + }); }); - it('displays link when no highlights exist', function() { - createCourseWithHighlights([]); - expectHighlightLinkNumberToBe(0); - }); - it('displays link when highlights exist', function() { - var highlights = mockHighlightValues(2); - createCourseWithHighlights(highlights); - expectHighlightLinkNumberToBe(2); - }); + describe('Section Highlights', function() { + var mockHighlightValues, highlightsLink, highlightInputs, openHighlights, saveHighlights, + cancelHighlights, setHighlights, expectHighlightLinkNumberToBe, expectHighlightsToBe, + expectServerHandshakeWithHighlights, expectHighlightsToUpdate, + maxNumHighlights = 5; - it('can view when no highlights exist', function() { - createCourseWithHighlights([]); - openHighlights(); - expectHighlightsToBe([]); - }); + mockHighlightValues = function(numberOfHighlights) { + var highlights = [], + i; + for (i = 0; i < numberOfHighlights; i++) { + highlights.push('Highlight' + (i + 1)); + } + return highlights; + }; - it('can view existing highlights', function() { - var highlights = mockHighlightValues(2); - createCourseWithHighlights(highlights); - openHighlights(); - expectHighlightsToBe(highlights); - }); + highlightsLink = function() { + return outlinePage.$('.section-status >> .highlights-button'); + }; - it('can add highlights', function() { - expectHighlightsToUpdate( - mockHighlightValues(0), - mockHighlightValues(1) - ); - }); + highlightInputs = function() { + return $('.highlight-input-text'); + }; - it('can remove highlights', function() { - expectHighlightsToUpdate( - mockHighlightValues(5), - mockHighlightValues(3) - ); - }); + openHighlights = function() { + highlightsLink().click(); + }; - it('can edit highlights', function() { - var originalHighlights = mockHighlightValues(3), - editedHighlights = originalHighlights; - editedHighlights[2] = 'A New Value'; - expectHighlightsToUpdate(originalHighlights, editedHighlights); + saveHighlights = function() { + clickSaveOnModal(); + }; + + cancelHighlights = function() { + clickCancelOnModal(); + }; + + 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(''); + } + }; + + expectHighlightLinkNumberToBe = function(expectedNumber) { + var link = highlightsLink(); + expect(link).toContainText('Section Highlights'); + expect(link.find('.number-highlights')).toHaveHtml(expectedNumber); + }; + + 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); + expectHighlightLinkNumberToBe(updatedHighlights.length); + + openHighlights(); + expectHighlightsToBe(updatedHighlights); + }; + + it('does not display link when disabled', function() { + createCourseWithHighlightsDisabled(); + expect(highlightsLink()).not.toExist(); + }); + + it('displays link when no highlights exist', function() { + createCourseWithHighlights([]); + expectHighlightLinkNumberToBe(0); + }); + + it('displays link when highlights exist', function() { + var highlights = mockHighlightValues(2); + createCourseWithHighlights(highlights); + expectHighlightLinkNumberToBe(2); + }); + + 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('does not save highlights when cancelled', function() { + var originalHighlights = mockHighlightValues(2), + editedHighlights = originalHighlights; + editedHighlights[1] = 'A New Value'; + + createCourseWithHighlights(originalHighlights); + openHighlights(); + setHighlights(editedHighlights); + + cancelHighlights(); + AjaxHelpers.expectNoRequests(requests); + + openHighlights(); + expectHighlightsToBe(originalHighlights); + }); + + 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); + }); }); }); diff --git a/cms/static/js/views/course_highlights_enable.js b/cms/static/js/views/course_highlights_enable.js new file mode 100644 index 0000000000..8d2973e02a --- /dev/null +++ b/cms/static/js/views/course_highlights_enable.js @@ -0,0 +1,54 @@ +define([ + 'jquery', 'underscore', 'backbone', 'js/views/utils/xblock_utils', 'js/utils/templates', + 'js/views/modals/course_outline_modals', 'edx-ui-toolkit/js/utils/html-utils'], + function( + $, _, Backbone, XBlockViewUtils, TemplateUtils, CourseOutlineModalsFactory, HtmlUtils + ) { + 'use strict'; + var CourseHighlightsEnableView = Backbone.View.extend({ + events: { + 'click button.status-highlights-enabled-value': 'handleEnableButtonPress', + 'keypress button.status-highlights-enabled-value': 'handleEnableButtonPress' + }, + + initialize: function() { + this.template = TemplateUtils.loadTemplate('course-highlights-enable'); + }, + + handleEnableButtonPress: function(event) { + if (event.type === 'click' || event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.highlightsEnableXBlock(); + } + }, + + highlightsEnableXBlock: function() { + var modal = CourseOutlineModalsFactory.getModal('highlights_enable', this.model, { + onSave: this.refresh.bind(this), + xblockType: XBlockViewUtils.getXBlockType( + this.model.get('category') + ) + }); + + if (modal) { + window.analytics.track('edx.bi.highlights_enable.modal_open'); + modal.show(); + } + }, + + refresh: function() { + this.model.fetch({ + success: this.render.bind(this) + }); + }, + + render: function() { + var html = this.template(this.model.attributes); + HtmlUtils.setHtml(this.$el, HtmlUtils.HTML(html)); + return this; + } + }); + + return CourseHighlightsEnableView; + } +); diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js index bf34283ee9..e839429346 100644 --- a/cms/static/js/views/modals/course_outline_modals.js +++ b/cms/static/js/views/modals/course_outline_modals.js @@ -17,7 +17,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', AbstractEditor, BaseDateEditor, ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor, StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor, - AccessEditor, ShowCorrectnessEditor, HighlightsEditor; + AccessEditor, ShowCorrectnessEditor, HighlightsEditor, HighlightsEnableXBlockModal, HighlightsEnableEditor; CourseOutlineXBlockModal = BaseModal.extend({ events: _.extend({}, BaseModal.prototype.events, { @@ -242,7 +242,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', callAnalytics: function(event) { event.preventDefault(); window.analytics.track('edx.bi.highlights.' + event.target.innerText.toLowerCase()); - this.save(event); + if (event.target.className.indexOf('save') !== -1) { + this.save(event); + } else { + this.hide(); + } }, addActionButtons: function() { @@ -251,6 +255,44 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', } }); + HighlightsEnableXBlockModal = CourseOutlineXBlockModal.extend({ + + events: _.extend({}, CourseOutlineXBlockModal.prototype.events, { + 'click .action-save': 'callAnalytics', + 'click .action-cancel': 'callAnalytics' + }), + + initialize: function() { + CourseOutlineXBlockModal.prototype.initialize.call(this); + if (this.options.xblockType) { + this.options.modalName = 'highlights-enable-' + this.options.xblockType; + } + }, + + getTitle: function() { + return gettext('Enable Weekly Course Highlight Messages'); + }, + + getIntroductionMessage: function() { + return ''; + }, + + callAnalytics: function(event) { + event.preventDefault(); + window.analytics.track('edx.bi.highlights_enable.' + event.target.innerText.toLowerCase()); + if (event.target.className.indexOf('save') !== -1) { + this.save(event); + } else { + this.hide(); + } + }, + + addActionButtons: function() { + this.addActionButton('save', gettext('Enable'), true); + this.addActionButton('cancel', gettext('Not yet')); + } + }); + AbstractEditor = BaseView.extend({ tagName: 'section', templateName: null, @@ -933,6 +975,42 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', } }); + HighlightsEnableEditor = AbstractEditor.extend({ + templateName: 'highlights-enable-editor', + className: 'edit-enable-highlights', + + currentValue: function() { + return true; + }, + + hasChanges: function() { + return this.model.get('highlights_enabled_for_messaging') !== this.currentValue(); + }, + + getRequestData: function() { + if (this.hasChanges()) { + return { + publish: 'republish', + metadata: { + highlights_enabled_for_messaging: this.currentValue() + } + }; + } else { + return {}; + } + }, + getContext: function() { + return $.extend( + {}, + AbstractEditor.prototype.getContext.call(this), + { + highlights_enabled: this.model.get('highlights_enabled_for_messaging'), + highlights_doc_url: this.model.get('highlights_doc_url') + } + ); + } + }); + return { getModal: function(type, xblockInfo, options) { if (type === 'edit') { @@ -941,6 +1019,8 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', return this.getPublishModal(xblockInfo, options); } else if (type === 'highlights') { return this.getHighlightsModal(xblockInfo, options); + } else if (type === 'highlights_enable') { + return this.getHighlightsEnableModal(xblockInfo, options); } else { return null; } @@ -1018,6 +1098,13 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', editors: [HighlightsEditor], model: xblockInfo }, options)); + }, + + getHighlightsEnableModal: function(xblockInfo, options) { + return new HighlightsEnableXBlockModal($.extend({ + editors: [HighlightsEnableEditor], + model: xblockInfo + }, options)); } }; }); diff --git a/cms/static/js/views/pages/course_outline.js b/cms/static/js/views/pages/course_outline.js index 05fc02b20e..b4d35e348b 100644 --- a/cms/static/js/views/pages/course_outline.js +++ b/cms/static/js/views/pages/course_outline.js @@ -1,10 +1,14 @@ /** * This page is used to show the user an outline of the course. */ -define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'js/views/utils/xblock_utils', - 'js/views/course_outline', 'common/js/components/utils/view_utils', 'common/js/components/views/feedback_alert', - 'common/js/components/views/feedback_notification'], - function($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView, ViewUtils, AlertView, NoteView) { +define([ + 'jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'js/views/utils/xblock_utils', + 'js/views/course_outline', 'common/js/components/utils/view_utils', 'common/js/components/views/feedback_alert', + 'common/js/components/views/feedback_notification', 'js/views/course_highlights_enable'], + function($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView, ViewUtils, AlertView, NoteView, + CourseHighlightsEnableView + ) { + 'use strict'; var expandedLocators, CourseOutlinePage; CourseOutlinePage = BasePage.extend({ @@ -65,6 +69,15 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'js/views this.expandedLocators.addAll(this.initialState.expanded_locators); } + /* globals course */ + if (this.model.get('highlights_enabled') && course.get('self_paced')) { + this.highlightsEnableView = new CourseHighlightsEnableView({ + el: this.$('.status-highlights-enabled'), + model: this.model + }); + this.highlightsEnableView.render(); + } + this.outlineView = new CourseOutlineView({ el: this.$('.outline'), model: this.model, diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index d3674ce852..ac06a835b9 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -184,11 +184,13 @@ margin-bottom: $baseline; .status-release, - .status-pacing { + .status-pacing, + .status-highlights-enabled { @extend %t-copy-base; display: inline-block; color: $color-copy-base; + margin-right: $baseline; // STATE: hover &:hover { @@ -198,10 +200,17 @@ } } + .status-highlights-enabled { + margin-left: $baseline * 1.6; + } + .status-release-label, .status-release-value, .status-pacing-label, .status-pacing-value, + .status-highlights-enabled-label, + .status-highlights-enabled-value, + .status-highlights-enabled-info, .status-actions { display: inline-block; vertical-align: middle; @@ -209,13 +218,28 @@ } .status-release-label, - .status-pacing-label { + .status-pacing-label, + .status-highlights-enabled-label { margin-right: ($baseline/4); } .status-release-value, - .status-pacing-value { + .status-pacing-value, + .status-highlights-enabled-value { @extend %t-strong; + font-size: smaller; + } + + .status-highlights-enabled-info { + font-size: smaller; + margin-left: $baseline / 2; + } + + .status-highlights-enabled-value.button { + @extend %btn-primary-blue; + @extend %sizing; + padding: 5px 8px; + margin-top: 2px; } .status-actions { diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index 8073e7a814..20d7e7c959 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -26,7 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text <%block name="header_extras"> -% 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']: +% 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', 'highlights-enable-editor', 'course-highlights-enable']: @@ -152,7 +152,8 @@ from openedx.core.djangolib.markup import HTML, Text
-

${_("Course Start Date:")}

+

${_("Course Start Date")}

+

${course_release_date}

    @@ -166,14 +167,16 @@ from openedx.core.djangolib.markup import HTML, Text
% if SelfPacedConfiguration.current().enabled:
-

${_("Course Pacing:")}

+

${_("Course Pacing")}

+
% if context_course.self_paced:

${_("Self-Paced")}

% else:

${_("Instructor-Paced")}

% endif
- % endif + % endif +
+

+ <%- gettext('Weekly Highlight Emails') %> +

+
+<% if (highlights_enabled_for_messaging) { %> + <%- gettext('Enabled') %> +<% } else { %> + +<% } %> +Learn more +
diff --git a/cms/templates/js/highlights-editor.underscore b/cms/templates/js/highlights-editor.underscore index 2bf31f5ffe..be3588f45e 100644 --- a/cms/templates/js/highlights-editor.underscore +++ b/cms/templates/js/highlights-editor.underscore @@ -19,7 +19,10 @@ 'read our {linkStart}documentation{linkEnd}.' ), { - linkStart: edx.HtmlUtils.HTML(''), + linkStart: edx.HtmlUtils.interpolateHtml( + edx.HtmlUtils.HTML(''), + {highlightsDocUrl: highlights_doc_url} + ), linkEnd: edx.HtmlUtils.HTML('') } ) %> diff --git a/cms/templates/js/highlights-enable-editor.underscore b/cms/templates/js/highlights-enable-editor.underscore new file mode 100644 index 0000000000..9a21697b1a --- /dev/null +++ b/cms/templates/js/highlights-enable-editor.underscore @@ -0,0 +1,24 @@ +

+<%- gettext( + 'When you enable weekly course highlight messages, learners ' + + 'automatically receive weekly email messages for each section that ' + + 'has highlights. You cannot disable highlights after you start ' + + 'sending them.' +) %> +

+

+<% // xss-lint: disable=underscore-not-escaped %> +<%= edx.HtmlUtils.interpolateHtml( + gettext( + 'Are you sure you want to enable weekly course highlight messages? ' + + '{linkStart}Learn more.{linkEnd}' + ), + { + linkStart: edx.HtmlUtils.interpolateHtml( + edx.HtmlUtils.HTML(''), + {highlightsDocUrl: xblockInfo.attributes.highlights_doc_url} + ), + linkEnd: edx.HtmlUtils.HTML('') + } +) %> +

diff --git a/cms/templates/js/mock/mock-course-outline-page.underscore b/cms/templates/js/mock/mock-course-outline-page.underscore index 8cc7a725b9..6bdc4f718b 100644 --- a/cms/templates/js/mock/mock-course-outline-page.underscore +++ b/cms/templates/js/mock/mock-course-outline-page.underscore @@ -37,6 +37,9 @@
+
+
+
diff --git a/common/test/acceptance/tests/studio/test_studio_outline.py b/common/test/acceptance/tests/studio/test_studio_outline.py index 207f2878dd..bca3932a58 100644 --- a/common/test/acceptance/tests/studio/test_studio_outline.py +++ b/common/test/acceptance/tests/studio/test_studio_outline.py @@ -5,6 +5,7 @@ Acceptance tests for studio related to the outline page. import itertools import json from datetime import datetime, timedelta +from unittest import skip from nose.plugins.attrib import attr from pytz import UTC @@ -115,6 +116,7 @@ class CourseOutlineDragAndDropTest(CourseOutlineTest): expected_ordering ) + @skip("Fails in Firefox 45 but passes in Chrome") def test_drop_unit_in_collapsed_subsection(self): """ Drag vertical "1.1.2" from subsection "1.1" into collapsed subsection "1.2" which already diff --git a/openedx/core/djangoapps/schedules/content_highlights.py b/openedx/core/djangoapps/schedules/content_highlights.py index 076f4aa48f..53b2ba91c1 100644 --- a/openedx/core/djangoapps/schedules/content_highlights.py +++ b/openedx/core/djangoapps/schedules/content_highlights.py @@ -1,3 +1,9 @@ +""" +Contains methods for accessing weekly course highlights. Weekly highlights is a +schedule experience built on the Schedules app. +""" +import logging + from courseware.module_render import get_module_for_descriptor from courseware.model_data import FieldDataCache from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG @@ -6,6 +12,8 @@ from request_cache import get_request_or_stub from xmodule.modulestore.django import modulestore +log = logging.getLogger(__name__) + def course_has_highlights(course_key): """ @@ -13,35 +21,63 @@ def course_has_highlights(course_key): This ignores access checks, since highlights may be lurking in currently inaccessible content. """ - if not COURSE_UPDATE_WAFFLE_FLAG.is_enabled(course_key): + try: + course = _get_course_with_highlights(course_key) + + except CourseUpdateDoesNotExist: return False - course = modulestore().get_course(course_key, depth=1) - return any( - section.highlights - for section in course.get_children() - if not section.hide_from_toc - ) + else: + highlights_are_available = any( + section.highlights + for section in course.get_children() + if not section.hide_from_toc + ) + + if not highlights_are_available: + log.error( + "Course team enabled highlights and provided no highlights." + ) + + return highlights_are_available def get_week_highlights(user, course_key, week_num): """ Get highlights (list of unicode strings) for a given week. week_num starts at 1. - Raises CourseUpdateDoesNotExist if highlights do not exist for - the requested week_num. + + Raises: + CourseUpdateDoesNotExist: if highlights do not exist for + the requested week_num. """ + course_descriptor = _get_course_with_highlights(course_key) + course_module = _get_course_module(course_descriptor, user) + sections_with_highlights = _get_sections_with_highlights(course_module) + highlights = _get_highlights_for_week( + sections_with_highlights, + week_num, + course_key, + ) + return highlights + + +def _get_course_with_highlights(course_key): + # pylint: disable=missing-docstring if not COURSE_UPDATE_WAFFLE_FLAG.is_enabled(course_key): raise CourseUpdateDoesNotExist( - "%s does not have Course Updates enabled.", - course_key + "%s Course Update Messages waffle flag is disabled.", + course_key, ) course_descriptor = _get_course_descriptor(course_key) - course_module = _get_course_module(course_descriptor, user) - sections_with_highlights = _get_sections_with_highlights(course_module) - highlights = _get_highlights_for_week(sections_with_highlights, week_num, course_key) - return highlights + if not course_descriptor.highlights_enabled_for_messaging: + raise CourseUpdateDoesNotExist( + "%s Course Update Messages are disabled.", + course_key, + ) + + return course_descriptor def _get_course_descriptor(course_key): diff --git a/openedx/core/djangoapps/schedules/tests/test_content_highlights.py b/openedx/core/djangoapps/schedules/tests/test_content_highlights.py index 1186eb40bf..837aea5a85 100644 --- a/openedx/core/djangoapps/schedules/tests/test_content_highlights.py +++ b/openedx/core/djangoapps/schedules/tests/test_content_highlights.py @@ -21,7 +21,9 @@ class TestContentHighlights(ModuleStoreTestCase): self._setup_user() def _setup_course(self): - self.course = CourseFactory.create() + self.course = CourseFactory.create( + highlights_enabled_for_messaging=True + ) self.course_key = self.course.id def _setup_user(self): @@ -66,6 +68,23 @@ class TestContentHighlights(ModuleStoreTestCase): highlights, ) + @override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True) + def test_highlights_disabled_for_messaging(self): + highlights = [u'A test highlight.'] + with self.store.bulk_operations(self.course_key): + self._create_chapter(highlights=highlights) + self.course.highlights_enabled_for_messaging = False + self.store.update_item(self.course, self.user.id) + + self.assertFalse(course_has_highlights(self.course_key)) + + with self.assertRaises(CourseUpdateDoesNotExist): + get_week_highlights( + self.user, + self.course_key, + week_num=1, + ) + @override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True) def test_course_with_no_highlights(self): with self.store.bulk_operations(self.course_key): diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 61528c8cff..a97cf516b0 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -47,7 +47,7 @@ edx-lint==0.4.3 astroid==1.3.8 edx-django-oauth2-provider==1.2.5 edx-django-sites-extensions==2.3.0 -edx-enterprise==0.53.16 +edx-enterprise==0.53.18 edx-oauth2-provider==1.2.2 edx-opaque-keys==0.4.0 edx-organizations==0.4.8 @@ -204,3 +204,6 @@ py2neo==3.1.2 # Support for plugins web-fragments==0.2.2 xblock==1.0.0 + +# Redis version +redis==2.10.6