diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 7d52124310e7..d357c8ae96c4 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -239,6 +239,17 @@ def save_button_disabled(step): assert world.css_has_class(button_css, disabled) +@step('I confirm the prompt') +def confirm_the_prompt(step): + prompt_css = 'a.button.action-primary' + world.css_click(prompt_css) + + +@step(u'I am shown a (.*)$') +def i_am_shown_a_notification(step, notification_type): + assert world.is_css_present('.wrapper-%s' % notification_type) + + def type_in_codemirror(index, text): world.css_click(".CodeMirror", index=index) g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea") diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature index 2291712f2da0..a30ce96ae62c 100644 --- a/cms/djangoapps/contentstore/features/component.feature +++ b/cms/djangoapps/contentstore/features/component.feature @@ -67,3 +67,21 @@ Feature: Component Adding When I will confirm all alerts And I delete all components Then I see no components + + Scenario: I see a prompt on delete + Given I have opened a new course in studio + And I am editing a new unit + And I add the following components: + | Component | + | Discussion | + And I delete a component + Then I am shown a prompt + + Scenario: I see a notification on save + Given I have opened a new course in studio + And I am editing a new unit + And I add the following components: + | Component | + | Discussion | + And I edit and save a component + Then I am shown a notification diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py index 217ad84591fb..15727dd99231 100644 --- a/cms/djangoapps/contentstore/features/component.py +++ b/cms/djangoapps/contentstore/features/component.py @@ -41,6 +41,17 @@ def see_no_components(steps): assert world.is_css_not_present('li.component') +@step(u'I delete a component') +def delete_one_component(step): + world.css_click('a.delete-button') + + +@step(u'I edit and save a component') +def edit_and_save_component(step): + world.css_click('.edit-button') + world.css_click('.save-button') + + def step_selector_list(data_type, path, index=1): selector_list = ['a[data-type="{}"]'.format(data_type)] if index != 1: diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/course-overview.feature similarity index 86% rename from cms/djangoapps/contentstore/features/studio-overview-togglesection.feature rename to cms/djangoapps/contentstore/features/course-overview.feature index e746f3629a85..b3041b9b189e 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/course-overview.feature @@ -1,7 +1,7 @@ -Feature: Overview Toggle Section - In order to quickly view the details of a course's section or to scan the inventory of sections +Feature: Course Overview + In order to quickly view the details of a course's section and set release dates and grading As a course author - I want to toggle the visibility of each section's subsection details in the overview listing + I want to use the course overview page Scenario: The default layout for the overview page is to show sections in expanded view Given I have a course with multiple sections @@ -57,3 +57,9 @@ Feature: Overview Toggle Section And I click the "Expand All Sections" link Then I see the "Collapse All Sections" link And all sections are expanded + + Scenario: Notification is shown on grading status changes + Given I have a course with 1 section + When I navigate to the course overview page + And I change an assignment's grading status + Then I am shown a notification diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/course-overview.py similarity index 95% rename from cms/djangoapps/contentstore/features/studio-overview-togglesection.py rename to cms/djangoapps/contentstore/features/course-overview.py index 41e39513ea95..10fa6453b254 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/course-overview.py @@ -118,3 +118,9 @@ def all_sections_are_collapsed(step): subsections = world.css_find(subsection_locator) for index in range(len(subsections)): assert_false(world.css_visible(subsection_locator, index=index)) + + +@step(u"I change an assignment's grading status") +def change_grading_status(step): + world.css_find('a.menu-toggle').click() + world.css_find('.menu li').first.click() diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 84a9bb991d8b..a08b490c6dc1 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -33,4 +33,5 @@ Feature: Create Section And I have added a new section When I will confirm all alerts And I press the "section" delete icon + And I confirm the prompt Then the section does not exist diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index a11467e3f92e..9f5793dbe763 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -38,4 +38,5 @@ Feature: Create Subsection And I see my subsection on the Courseware page When I will confirm all alerts And I press the "subsection" delete icon + And I confirm the prompt Then the subsection does not exist diff --git a/cms/static/coffee/spec/views/overview_spec.coffee b/cms/static/coffee/spec/views/overview_spec.coffee index ba9b5d891862..d900e4bfb1b2 100644 --- a/cms/static/coffee/spec/views/overview_spec.coffee +++ b/cms/static/coffee/spec/views/overview_spec.coffee @@ -40,17 +40,30 @@ describe "Course Overview", -> """#" + appendSetFixtures """ +
+ +
+ """#" + spyOn(window, 'saveSetSectionScheduleDate').andCallThrough() # Have to do this here, as it normally gets bound in document.ready() $('a.save-button').click(saveSetSectionScheduleDate) + $('a.delete-section-button').click(deleteSection) + @notificationSpy = spyOn(CMS.Views.Notification.Mini.prototype, 'show').andCallThrough() window.analytics = jasmine.createSpyObj('analytics', ['track']) window.course_location_analytics = jasmine.createSpy() - sinon.useFakeXMLHttpRequest() + @xhr = sinon.useFakeXMLHttpRequest() + requests = @requests = [] + @xhr.onCreate = (req) -> requests.push(req) afterEach -> delete window.analytics delete window.course_location_analytics + @notificationSpy.reset() it "should save model when save is clicked", -> $('a.edit-button').click() @@ -61,3 +74,21 @@ describe "Course Overview", -> $('a.edit-button').click() $('a.save-button').click() expect(@notificationSpy).toHaveBeenCalled() + + it "should delete model when delete is clicked", -> + deleteSpy = spyOn(window, '_deleteItem').andCallThrough() + $('a.delete-section-button').click() + $('a.action-primary').click() + expect(deleteSpy).toHaveBeenCalled() + expect(@requests[0].url).toEqual('/delete_item') + + it "should not delete model when cancel is clicked", -> + deleteSpy = spyOn(window, '_deleteItem').andCallThrough() + $('a.delete-section-button').click() + $('a.action-secondary').click() + expect(@requests.length).toEqual(0) + + it "should show a confirmation on delete", -> + $('a.delete-section-button').click() + $('a.action-primary').click() + expect(@notificationSpy).toHaveBeenCalled() diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 62083fa26d6d..c45feecd419e 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -84,11 +84,15 @@ class CMS.Views.ModuleEdit extends Backbone.View data.metadata = _.extend(data.metadata || {}, @changedMetadata()) @hideModal() + saving = new CMS.Views.Notification.Mini + title: gettext('Saving') + '…' + saving.show() @model.save(data).done( => # # showToastMessage("Your changes have been saved.", null, 3) @module = null @render() @$el.removeClass('editing') + saving.hide() ).fail( -> showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3) ) diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 774ef04f6d0a..14b64b7b9e63 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -67,8 +67,8 @@ class CMS.Views.UnitEdit extends Backbone.View type = $(event.currentTarget).data('type') @$newComponentTypePicker.slideUp(250) @$(".new-component-#{type}").slideDown(250) - $('html, body').animate({ - scrollTop: @$(".new-component-#{type}").offset().top + $('html, body').animate({ + scrollTop: @$(".new-component-#{type}").offset().top }, 500) closeNewComponent: (event) => @@ -115,27 +115,43 @@ class CMS.Views.UnitEdit extends Backbone.View @model.save() deleteComponent: (event) => - if not confirm 'Are you sure you want to delete this component? This action cannot be undone.' - return - $component = $(event.currentTarget).parents('.component') - $.post('/delete_item', { - id: $component.data('id') - }, => - analytics.track "Deleted a Component", - course: course_location_analytics - unit_id: unit_location_analytics - id: $component.data('id') - - $component.remove() - # b/c we don't vigilantly keep children up to date - # get rid of it before it hurts someone - # sorry for the js, i couldn't figure out the coffee equivalent - `_this.model.save({children: _this.components()}, - {success: function(model) { - model.unset('children'); - }} - );` + msg = new CMS.Views.Prompt.Warning( + title: gettext('Delete this component?'), + message: gettext('Deleting this component is permanent and cannot be undone.'), + actions: + primary: + text: gettext('Yes, delete this component'), + click: (view) => + view.hide() + deleting = new CMS.Views.Notification.Mini + title: gettext('Deleting') + '…', + deleting.show() + $component = $(event.currentTarget).parents('.component') + $.post('/delete_item', { + id: $component.data('id') + }, => + deleting.hide() + analytics.track "Deleted a Component", + course: course_location_analytics + unit_id: unit_location_analytics + id: $component.data('id') + + $component.remove() + # b/c we don't vigilantly keep children up to date + # get rid of it before it hurts someone + # sorry for the js, i couldn't figure out the coffee equivalent + `_this.model.save({children: _this.components()}, + {success: function(model) { + model.unset('children'); + }} + );` + ) + secondary: + text: gettext('Cancel'), + click: (view) -> + view.hide() ) + msg.show() deleteDraft: (event) -> @wait(true) @@ -236,7 +252,7 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View class CMS.Views.UnitEdit.LocationState extends Backbone.View initialize: => @model.on('change:state', @render) - + render: => @$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item") diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 329624ef4687..25c6c151f3d5 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -356,39 +356,61 @@ function createNewUnit(e) { function deleteUnit(e) { e.preventDefault(); - _deleteItem($(this).parents('li.leaf')); + _deleteItem($(this).parents('li.leaf'), 'Unit'); } function deleteSubsection(e) { e.preventDefault(); - _deleteItem($(this).parents('li.branch')); + _deleteItem($(this).parents('li.branch'), 'Subsection'); } function deleteSection(e) { e.preventDefault(); - _deleteItem($(this).parents('section.branch')); -} - -function _deleteItem($el) { - if (!confirm(gettext('Are you sure you wish to delete this item. It cannot be reversed!'))) return; - - var id = $el.data('id'); - - analytics.track('Deleted an Item', { - 'course': course_location_analytics, - 'id': id - }); - - - $.post('/delete_item', { - 'id': id, - 'delete_children': true, - 'delete_all_versions': true - }, - - function(data) { - $el.remove(); + _deleteItem($(this).parents('section.branch'), 'Section'); +} + +function _deleteItem($el, type) { + var confirm = new CMS.Views.Prompt.Warning({ + title: gettext('Delete this ' + type + '?'), + message: gettext('Deleting this ' + type + ' is permanent and cannot be undone.'), + actions: { + primary: { + text: gettext('Yes, delete this ' + type), + click: function(view) { + view.hide(); + + var id = $el.data('id'); + + analytics.track('Deleted an Item', { + 'course': course_location_analytics, + 'id': id + }); + + var deleting = new CMS.Views.Notification.Mini({ + title: gettext('Deleting') + '…' + }); + deleting.show(); + + $.post('/delete_item', + {'id': id, + 'delete_children': true, + 'delete_all_versions': true}, + function(data) { + $el.remove(); + deleting.hide(); + } + ); + } + }, + secondary: { + text: gettext('Cancel'), + click: function(view) { + view.hide(); + } + } + } }); + confirm.show(); } function markAsLoaded() { diff --git a/cms/static/js/views/grader-select-view.js b/cms/static/js/views/grader-select-view.js index b22e763710a8..a16f5fa1b791 100644 --- a/cms/static/js/views/grader-select-view.js +++ b/cms/static/js/views/grader-select-view.js @@ -81,9 +81,18 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({ this.removeMenu(e); + var saving = new CMS.Views.Notification.Mini({ + title: gettext('Saving') + '…' + }); + saving.show(); + // TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr // of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly) - this.assignmentGrade.save('graderType', $(e.target).text()); + this.assignmentGrade.save( + 'graderType', + $(e.target).text(), + {success: function () { saving.hide(); }} + ); this.render(); }