Commit 9cf0b335 by Jose Antonio Gonzlez

Merge remote-tracking branch 'upstream/master' into…

Merge remote-tracking branch 'upstream/master' into proversity/add-recover-password-endpoint [ci skip]
parents 0decade2 c21895a8
......@@ -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'),
})
......
......@@ -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)
......
......@@ -165,6 +165,7 @@ function(Backbone, _, str, ModuleUtils) {
*/
highlights: [],
highlights_enabled: false,
highlights_enabled_for_messaging: false,
highlights_preview_only: true,
highlights_doc_url: ''
},
......
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;
}
);
......@@ -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));
}
};
});
/**
* 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,
......
......@@ -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 {
......
......@@ -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', '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']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
......@@ -152,7 +152,8 @@ from openedx.core.djangolib.markup import HTML, Text
<article class="content-primary" role="main">
<div class="course-status">
<div class="status-release">
<h2 class="status-release-label">${_("Course Start Date:")}</h2>
<h2 class="status-release-label">${_("Course Start Date")}</h2>
<br>
<p class="status-release-value">${course_release_date}</p>
<ul class="status-actions">
......@@ -166,14 +167,16 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
% if SelfPacedConfiguration.current().enabled:
<div class="status-pacing">
<h2 class=status-pacing-label>${_("Course Pacing:")}</h2>
<h2 class=status-pacing-label>${_("Course Pacing")}</h2>
<br>
% if context_course.self_paced:
<p class="status-pacing-value">${_("Self-Paced")}</p>
% else:
<p class="status-pacing-value">${_("Instructor-Paced")}</p>
% endif
</div>
% endif
% endif
<div class="status-highlights-enabled"></div>
</div>
<div class="wrapper-dnd"
% if getattr(context_course, 'language'):
......
<div class="course-highlights-setting">
<h2 id="highlights-enabled-label" class="status-highlights-enabled-label">
<%- gettext('Weekly Highlight Emails') %>
</h2>
<br>
<% if (highlights_enabled_for_messaging) { %>
<span class="status-highlights-enabled-value text"><%- gettext('Enabled') %></span>
<% } else { %>
<button class="status-highlights-enabled-value button" aria-labelledby="highlights-enabled-label"><%- gettext('Enable Now') %></button>
<% } %>
<a class="status-highlights-enabled-info" href="<%- highlights_doc_url %>">Learn more</a>
</div>
......@@ -19,7 +19,10 @@
'read our {linkStart}documentation{linkEnd}.'
),
{
linkStart: edx.HtmlUtils.HTML('<a href="' + highlights_doc_url + '">'),
linkStart: edx.HtmlUtils.interpolateHtml(
edx.HtmlUtils.HTML('<a href={highlightsDocUrl}">'),
{highlightsDocUrl: highlights_doc_url}
),
linkEnd: edx.HtmlUtils.HTML('</a>')
}
) %>
......
<p>
<%- 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.'
) %>
</p>
<p>
<% // 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('<a href="{highlightsDocUrl}" target="_blank">'),
{highlightsDocUrl: xblockInfo.attributes.highlights_doc_url}
),
linkEnd: edx.HtmlUtils.HTML('</a>')
}
) %>
</p>
......@@ -37,6 +37,9 @@
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<div class=course-status"">
<div class="status-highlights-enabled"></div>
</div>
<div class="wrapper-dnd">
<article class="outline outline-course" data-locator="mock-course" data-course-key="slashes:MockCourse">
<div class="no-content add-xblock-component">
......
......@@ -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
......
"""
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):
......
......@@ -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):
......@@ -67,6 +69,23 @@ class TestContentHighlights(ModuleStoreTestCase):
)
@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):
self._create_chapter(display_name=u"Week 1")
......
......@@ -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
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