diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 8b2bc885d0..93371a1934 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -1,8 +1,9 @@ define ["jquery", "jquery.ui", "gettext", "backbone", "js/views/feedback_notification", "js/views/feedback_prompt", - "coffee/src/views/module_edit", "js/models/module_info"], -($, ui, gettext, Backbone, NotificationView, PromptView, ModuleEditView, ModuleModel) -> - class UnitEditView extends Backbone.View + "coffee/src/views/module_edit", "js/models/module_info", + "js/views/baseview"], +($, ui, gettext, Backbone, NotificationView, PromptView, ModuleEditView, ModuleModel, BaseView) -> + class UnitEditView extends BaseView events: 'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates' 'click .new-component .new-component-type a.single-template': 'saveNewComponent' @@ -212,30 +213,35 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ) createDraft: (event) -> - @wait(true) + self = this + @disableElementWhileRunning($(event.target), -> + self.wait(true) + $.postJSON(self.model.url(), { + publish: 'create_draft' + }, => + analytics.track "Created Draft", + course: course_location_analytics + unit_id: unit_location_analytics - $.postJSON(@model.url(), { - publish: 'create_draft' - }, => - analytics.track "Created Draft", - course: course_location_analytics - unit_id: unit_location_analytics - - @model.set('state', 'draft') + self.model.set('state', 'draft') + ) ) publishDraft: (event) -> - @wait(true) - @saveDraft() + self = this + @disableElementWhileRunning($(event.target), -> + self.wait(true) + self.saveDraft() - $.postJSON(@model.url(), { - publish: 'make_public' - }, => - analytics.track "Published Draft", - course: course_location_analytics - unit_id: unit_location_analytics + $.postJSON(self.model.url(), { + publish: 'make_public' + }, => + analytics.track "Published Draft", + course: course_location_analytics + unit_id: unit_location_analytics - @model.set('state', 'public') + self.model.set('state', 'public') + ) ) setVisibility: (event) -> @@ -259,7 +265,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone", @model.set('state', @$('.visibility-select').val()) ) - class UnitEditView.NameEdit extends Backbone.View + class UnitEditView.NameEdit extends BaseView events: 'change .unit-display-name-input': 'saveName' @@ -293,14 +299,14 @@ define ["jquery", "jquery.ui", "gettext", "backbone", display_name: metadata.display_name - class UnitEditView.LocationState extends Backbone.View + class UnitEditView.LocationState extends BaseView initialize: => @model.on('change:state', @render) render: => @$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item") - class UnitEditView.Visibility extends Backbone.View + class UnitEditView.Visibility extends BaseView initialize: => @model.on('change:state', @render) @render() diff --git a/cms/static/js/spec/views/baseview_spec.js b/cms/static/js/spec/views/baseview_spec.js index 4d88992a3e..c26ba7b528 100644 --- a/cms/static/js/spec/views/baseview_spec.js +++ b/cms/static/js/spec/views/baseview_spec.js @@ -76,5 +76,24 @@ define(["jquery", "underscore", "js/views/baseview", "js/utils/handle_iframe_bin expect(view.$('.is-collapsible')).not.toHaveClass('collapsed'); }); }); + + describe("disabled element while running", function() { + it("adds 'is-disabled' class to element while action is running and removes it after", function() { + var viewWithLink, + link, + deferred = new $.Deferred(), + promise = deferred.promise(), + view = new BaseView(); + + setFixtures("ripe apples drop about my head"); + + link = $("#link"); + expect(link).not.toHaveClass("is-disabled"); + view.disableElementWhileRunning(link, function(){return promise}); + expect(link).toHaveClass("is-disabled"); + deferred.resolve(); + expect(link).not.toHaveClass("is-disabled"); + }); + }); }); }); diff --git a/cms/static/js/spec/views/unit_spec.js b/cms/static/js/spec/views/unit_spec.js index 498243f358..d912abc229 100644 --- a/cms/static/js/spec/views/unit_spec.js +++ b/cms/static/js/spec/views/unit_spec.js @@ -162,5 +162,79 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec_helpers/creat verifyComponents(unit, ['loc_1', 'loc_2']); }); }); + describe("Disabled edit/publish links during ajax call", function() { + var unit, + link, + draft_states = [ + { + state: "draft", + selector: ".publish-draft" + }, + { + state: "public", + selector: ".create-draft" + } + ], + editLinkFixture = + '
\ +
\ +

Unit Settings

\ +
\ +
\ +

\ + edit a draft \ +

\ +

\ + replace it with this draft \ +

\ +
\ +
\ +
\ +
'; + function test_link_disabled_during_ajax_call(draft_state) { + beforeEach(function () { + setFixtures(editLinkFixture); + unit = new UnitEditView({ + el: $('.main-wrapper'), + model: new ModuleModel({ + id: 'unit_locator', + state: draft_state['state'] + }) + }); + // needed to stub out the ajax + window.analytics = jasmine.createSpyObj('analytics', ['track']); + window.course_location_analytics = jasmine.createSpy('course_location_analytics'); + window.unit_location_analytics = jasmine.createSpy('unit_location_analytics'); + }); + + it("reenables the " + draft_state['selector'] + " link once the ajax call returns", function() { + runs(function(){ + spyOn($, "ajax").andCallThrough(); + spyOn($.fn, 'addClass').andCallThrough(); + spyOn($.fn, 'removeClass').andCallThrough(); + link = $(draft_state['selector']); + link.click(); + }); + waitsFor(function(){ + // wait for "is-disabled" to be removed as a class + return !($(draft_state['selector']).hasClass("is-disabled")); + }, 500); + runs(function(){ + // check that the `is-disabled` class was added and removed + expect($.fn.addClass).toHaveBeenCalledWith("is-disabled"); + expect($.fn.removeClass).toHaveBeenCalledWith("is-disabled"); + + // make sure the link finishes without the `is-disabled` class + expect(link).not.toHaveClass("is-disabled"); + + // affirm that ajax was called + expect($.ajax).toHaveBeenCalled(); + }); + }); + }; + for (var i = 0; i < draft_states.length; i++) { + test_link_disabled_during_ajax_call(draft_states[i]); + }; + }); } ); diff --git a/cms/static/js/views/baseview.js b/cms/static/js/views/baseview.js index ad47bb86eb..8072705d36 100644 --- a/cms/static/js/views/baseview.js +++ b/cms/static/js/views/baseview.js @@ -1,13 +1,13 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"], function ($, _, Backbone, IframeUtils) { /* - This view is extended from backbone to provide useful functionality for all Studio views. - This functionality includes: - - automatic expand and collapse of elements with the 'ui-toggle-expansion' class specified - - additional control of rendering by overriding 'beforeRender' or 'afterRender' + This view is extended from backbone to provide useful functionality for all Studio views. + This functionality includes: + - automatic expand and collapse of elements with the 'ui-toggle-expansion' class specified + - additional control of rendering by overriding 'beforeRender' or 'afterRender' - Note: the default 'afterRender' function calls a utility function 'iframeBinding' which modifies - iframe src urls on a page so that they are rendered as part of the DOM. + Note: the default 'afterRender' function calls a utility function 'iframeBinding' which modifies + iframe src urls on a page so that they are rendered as part of the DOM. */ var BaseView = Backbone.View.extend({ @@ -60,6 +60,20 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"], $('.ui-loading').hide(); }, + /** + * Disables a given element when a given operation is running. + * @param {jQuery} element: the element to be disabled. + * @param operation: the operation during whose duration the + * element should be disabled. The operation should return + * a jquery promise. + */ + disableElementWhileRunning: function(element, operation) { + element.addClass("is-disabled"); + operation().always(function() { + element.removeClass("is-disabled"); + }); + }, + /** * Loads the named template from the page, or logs an error if it fails. * @param name The name of the template.