diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 692ec695ce..98a774fcc2 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -92,7 +92,7 @@ def press_the_notification_button(_step, name): # the "Save" button at the UI level. # Instead, we use JavaScript to reliably click # the button. - btn_css = 'div#page-notification a.action-%s' % name.lower() + btn_css = 'div#page-notification button.action-%s' % name.lower() world.trigger_event(btn_css, event='focus') world.browser.execute_script("$('{}').click()".format(btn_css)) world.wait_for_ajax_complete() @@ -284,7 +284,7 @@ def button_disabled(step, value): def _do_studio_prompt_action(intent, action): """ Wait for a studio prompt to appear and press the specified action button - See cms/static/js/views/feedback_prompt.js for implementation + See common/js/components/views/feedback_prompt.js for implementation """ assert intent in [ 'warning', @@ -299,7 +299,7 @@ def _do_studio_prompt_action(intent, action): world.wait_for_present('div.wrapper-prompt.is-shown#prompt-{}'.format(intent)) - action_css = 'li.nav-item > a.action-{}'.format(action) + action_css = 'li.nav-item > button.action-{}'.format(action) world.trigger_event(action_css, event='focus') world.browser.execute_script("$('{}').click()".format(action_css)) diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py index 1d3f7e3a47..c2f62e69e4 100644 --- a/cms/djangoapps/contentstore/features/component.py +++ b/cms/djangoapps/contentstore/features/component.py @@ -97,7 +97,7 @@ def delete_components(step, number): world.wait_for_xmodule() delete_btn_css = 'a.delete-button' prompt_css = 'div#prompt-warning' - btn_css = '{} a.button.action-primary'.format(prompt_css) + btn_css = '{} button.action-primary'.format(prompt_css) saving_mini_css = 'div#page-notification .wrapper-notification-mini' for _ in range(int(number)): world.css_click(delete_btn_css) diff --git a/cms/djangoapps/contentstore/features/course-export.py b/cms/djangoapps/contentstore/features/course-export.py index 38f2dc5c32..5a0867565d 100644 --- a/cms/djangoapps/contentstore/features/course-export.py +++ b/cms/djangoapps/contentstore/features/course-export.py @@ -49,8 +49,7 @@ def get_an_error_dialog(step): @step('I can click to go to the unit with the error$') def i_click_on_error_dialog(step): - world.wait_for_visible(".button.action-primary") - world.click_link_by_text('Correct failed component') + world.css_click("button.action-primary") problem_string = unicode(world.scenario_dict['COURSE'].id.make_usage_key("problem", 'ignore')) problem_string = u"Problem {}".format(problem_string[:problem_string.rfind('ignore')]) diff --git a/cms/djangoapps/contentstore/features/course-outline.py b/cms/djangoapps/contentstore/features/course-outline.py index 6ab7f85e81..702364b1a0 100644 --- a/cms/djangoapps/contentstore/features/course-outline.py +++ b/cms/djangoapps/contentstore/features/course-outline.py @@ -75,7 +75,7 @@ def i_press_the_section_delete_icon(step): @step(u'I will confirm all alerts') def i_confirm_all_alerts(step): - confirm_locator = '.prompt .nav-actions a.action-primary' + confirm_locator = '.prompt .nav-actions button.action-primary' world.css_click(confirm_locator) diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index 8bbae2219e..b211eb3fdf 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -220,7 +220,6 @@ define([ "coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec", "coffee/spec/models/upload_spec", - "coffee/spec/views/course_info_spec", "coffee/spec/views/feedback_spec", "coffee/spec/views/metadata_edit_spec", "coffee/spec/views/module_edit_spec", "coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec", @@ -254,8 +253,6 @@ define([ "js/spec/views/license_spec", "js/spec/views/paging_spec", - "js/spec/views/utils/view_utils_spec", - "js/spec/views/pages/container_spec", "js/spec/views/pages/container_subviews_spec", "js/spec/views/pages/group_configurations_spec", diff --git a/cms/static/coffee/spec/main_spec.coffee b/cms/static/coffee/spec/main_spec.coffee index 45cbd52bbc..66e8048889 100644 --- a/cms/static/coffee/spec/main_spec.coffee +++ b/cms/static/coffee/spec/main_spec.coffee @@ -21,10 +21,8 @@ require ["jquery", "backbone", "coffee/src/main", "common/js/spec_helpers/ajax_h expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken") describe "AJAX Errors", -> - tpl = readFixtures('system-feedback.underscore') beforeEach -> - setFixtures($(" - ## js templates - -
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %> diff --git a/cms/static/js/views/utils/view_utils.js b/common/static/common/js/components/utils/view_utils.js similarity index 97% rename from cms/static/js/views/utils/view_utils.js rename to common/static/common/js/components/utils/view_utils.js index efb3ae7f71..43761b2cb0 100644 --- a/cms/static/js/views/utils/view_utils.js +++ b/common/static/common/js/components/utils/view_utils.js @@ -1,7 +1,10 @@ /** * Provides useful utilities for views. */ -define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt"], +;(function (define) { + 'use strict'; + define(["jquery", "underscore", "gettext", "common/js/components/views/feedback_notification", + "common/js/components/views/feedback_prompt"], function ($, _, gettext, NotificationView, PromptView) { var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation, runOperationShowingMessage, disableElementWhileRunning, getScrollOffset, setScrollOffset, @@ -246,3 +249,4 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js 'checkTotalKeyLengthViolations': checkTotalKeyLengthViolations }; }); +}).call(this, define || RequireJS.define); diff --git a/common/static/common/js/components/views/feedback.js b/common/static/common/js/components/views/feedback.js new file mode 100644 index 0000000000..d6f4cde749 --- /dev/null +++ b/common/static/common/js/components/views/feedback.js @@ -0,0 +1,152 @@ +;(function (define) { + 'use strict'; + define(["jquery", + "underscore", + "underscore.string", + "backbone", + "text!common/templates/components/system-feedback.underscore"], + function($, _, str, Backbone, systemFeedbackTemplate) { + var SystemFeedback = Backbone.View.extend({ + options: { + title: "", + message: "", + intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc + type: null, // "alert", "notification", or "prompt": set by subclass + shown: true, // is this view currently being shown? + icon: true, // should we render an icon related to the message intent? + closeIcon: true, // should we render a close button in the top right corner? + minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds) + maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds) + + /* Could also have an "actions" hash: here is an example demonstrating + the expected structure. For each action, by default the framework + will call preventDefault on the click event before the function is + run; to make it not do that, just pass `preventDefault: false` in + the action object. + + actions: { + primary: { + "text": "Save", + "class": "action-save", + "click": function(view) { + // do something when Save is clicked + } + }, + secondary: [ + { + "text": "Cancel", + "class": "action-cancel", + "click": function(view) {} + }, { + "text": "Discard Changes", + "class": "action-discard", + "click": function(view) {} + } + ] + } + */ + }, + + initialize: function() { + if (!this.options.type) { + throw "SystemFeedback: type required (given " + + JSON.stringify(this.options) + ")"; + } + if (!this.options.intent) { + throw "SystemFeedback: intent required (given " + + JSON.stringify(this.options) + ")"; + } + this.setElement($("#page-" + this.options.type)); + // handle single "secondary" action + if (this.options.actions && this.options.actions.secondary && + !_.isArray(this.options.actions.secondary)) { + this.options.actions.secondary = [this.options.actions.secondary]; + } + return this; + }, + + // public API: show() and hide() + show: function() { + clearTimeout(this.hideTimeout); + this.options.shown = true; + this.shownAt = new Date(); + this.render(); + if ($.isNumeric(this.options.maxShown)) { + this.hideTimeout = setTimeout(_.bind(this.hide, this), + this.options.maxShown); + } + return this; + }, + + hide: function() { + if (this.shownAt && $.isNumeric(this.options.minShown) && + this.options.minShown > new Date() - this.shownAt) { + clearTimeout(this.hideTimeout); + this.hideTimeout = setTimeout(_.bind(this.hide, this), + this.options.minShown - (new Date() - this.shownAt)); + } else { + this.options.shown = false; + delete this.shownAt; + this.render(); + } + return this; + }, + + // the rest of the API should be considered semi-private + events: { + "click .action-close": "hide", + "click .action-primary": "primaryClick", + "click .action-secondary": "secondaryClick" + }, + + render: function() { + // there can be only one active view of a given type at a time: only + // one alert, only one notification, only one prompt. Therefore, we'll + // use a singleton approach. + var singleton = SystemFeedback["active_" + this.options.type]; + if (singleton && singleton !== this) { + singleton.stopListening(); + singleton.undelegateEvents(); + } + this.$el.html(_.template(systemFeedbackTemplate)(this.options)); + SystemFeedback["active_" + this.options.type] = this; + return this; + }, + + primaryClick: function(event) { + var actions, primary; + actions = this.options.actions; + if (!actions) { return; } + primary = actions.primary; + if (!primary) { return; } + if (primary.preventDefault !== false) { + event.preventDefault(); + } + if (primary.click) { + primary.click.call(event.target, this, event); + } + }, + + secondaryClick: function(event) { + var actions, secondaryList, secondary, i; + actions = this.options.actions; + if (!actions) { return; } + secondaryList = actions.secondary; + if (!secondaryList) { return; } + // which secondary action was clicked? + i = 0; // default to the first secondary action (easier for testing) + if (event && event.target) { + i = _.indexOf(this.$(".action-secondary"), event.target); + } + secondary = secondaryList[i]; + if (secondary.preventDefault !== false) { + event.preventDefault(); + } + if (secondary.click) { + secondary.click.call(event.target, this, event); + } + } + }); + return SystemFeedback; + }); +}).call(this, define || RequireJS.define); diff --git a/common/static/common/js/components/views/feedback_alert.js b/common/static/common/js/components/views/feedback_alert.js new file mode 100644 index 0000000000..8f3f716a58 --- /dev/null +++ b/common/static/common/js/components/views/feedback_alert.js @@ -0,0 +1,42 @@ +;(function (define) { + 'use strict'; + define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"], + function($, _, str, SystemFeedbackView) { + str = str || _.str; + var Alert = SystemFeedbackView.extend({ + options: $.extend({}, SystemFeedbackView.prototype.options, { + type: "alert" + }), + slide_speed: 900, + show: function() { + SystemFeedbackView.prototype.show.apply(this, arguments); + this.$el.hide(); + this.$el.slideDown(this.slide_speed); + return this; + }, + hide: function () { + this.$el.slideUp({ + duration: this.slide_speed + }); + setTimeout(_.bind(SystemFeedbackView.prototype.hide, this, arguments), + this.slideSpeed); + } + }); + + // create Alert.Warning, Alert.Confirmation, etc + var capitalCamel, intents; + capitalCamel = _.compose(str.capitalize, str.camelize); + intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"]; + _.each(intents, function(intent) { + var subclass; + subclass = Alert.extend({ + options: $.extend({}, Alert.prototype.options, { + intent: intent + }) + }); + Alert[capitalCamel(intent)] = subclass; + }); + + return Alert; + }); +}).call(this, define || RequireJS.define); diff --git a/common/static/common/js/components/views/feedback_notification.js b/common/static/common/js/components/views/feedback_notification.js new file mode 100644 index 0000000000..2fb74f691d --- /dev/null +++ b/common/static/common/js/components/views/feedback_notification.js @@ -0,0 +1,34 @@ +;(function (define) { + 'use strict'; + define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"], + function($, _, str, SystemFeedbackView) { + str = str || _.str; + var Notification = SystemFeedbackView.extend({ + options: $.extend({}, SystemFeedbackView.prototype.options, { + type: "notification", + closeIcon: false + }) + }); + + // create Notification.Warning, Notification.Confirmation, etc + var capitalCamel, intents; + capitalCamel = _.compose(str.capitalize, str.camelize); + intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"]; + _.each(intents, function(intent) { + var subclass; + subclass = Notification.extend({ + options: $.extend({}, Notification.prototype.options, { + intent: intent + }) + }); + Notification[capitalCamel(intent)] = subclass; + }); + + // set more sensible defaults for Notification.Mini views + var miniOptions = Notification.Mini.prototype.options; + miniOptions.minShown = 1250; + miniOptions.closeIcon = false; + + return Notification; + }); +}).call(this, define || RequireJS.define); diff --git a/common/static/common/js/components/views/feedback_prompt.js b/common/static/common/js/components/views/feedback_prompt.js new file mode 100644 index 0000000000..04a32da9c4 --- /dev/null +++ b/common/static/common/js/components/views/feedback_prompt.js @@ -0,0 +1,40 @@ +;(function (define) { + 'use strict'; + define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"], + function($, _, str, SystemFeedbackView) { + str = str || _.str; + var Prompt = SystemFeedbackView.extend({ + options: $.extend({}, SystemFeedbackView.prototype.options, { + type: "prompt", + closeIcon: false, + icon: false + }), + render: function() { + if(!window.$body) { window.$body = $(document.body); } + if(this.options.shown) { + $body.addClass('prompt-is-shown'); + } else { + $body.removeClass('prompt-is-shown'); + } + // super() in Javascript has awkward syntax :( + return SystemFeedbackView.prototype.render.apply(this, arguments); + } + }); + + // create Prompt.Warning, Prompt.Confirmation, etc + var capitalCamel, intents; + capitalCamel = _.compose(str.capitalize, str.camelize); + intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"]; + _.each(intents, function(intent) { + var subclass; + subclass = Prompt.extend({ + options: $.extend({}, Prompt.prototype.options, { + intent: intent + }) + }); + Prompt[capitalCamel(intent)] = subclass; + }); + + return Prompt; + }); +}).call(this, define || RequireJS.define); diff --git a/common/static/common/js/spec/components/feedback_spec.js b/common/static/common/js/spec/components/feedback_spec.js new file mode 100644 index 0000000000..5ef3600204 --- /dev/null +++ b/common/static/common/js/spec/components/feedback_spec.js @@ -0,0 +1,336 @@ +// Generated by CoffeeScript 1.6.1 +(function() { + + define(["jquery", "common/js/components/views/feedback", "common/js/components/views/feedback_notification", "common/js/components/views/feedback_alert", "common/js/components/views/feedback_prompt", "sinon"], function($, SystemFeedback, NotificationView, AlertView, PromptView, sinon) { + var tpl; + tpl = readFixtures('system-feedback.underscore'); + beforeEach(function() { + setFixtures(sandbox({ + id: "page-alert" + })); + appendSetFixtures(sandbox({ + id: "page-notification" + })); + appendSetFixtures(sandbox({ + id: "page-prompt" + })); + appendSetFixtures($("