diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a68ad7530fec3d4d0ceeba90ba899ca8bcd1c905..b6390d58de3f674a4285483560d46feee0201770 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,12 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Support adding cohorts from the instructor dashboard. TNL-162 + +LMS: Support adding students to a cohort via the instructor dashboard. TNL-163 + +LMS: Show cohorts on the new instructor dashboard. TNL-161 + LMS: Mobile API available for courses that opt in using the Course Advanced Setting "Mobile Course Available" (only used in limited closed beta). diff --git a/cms/static/coffee/spec/main_spec.coffee b/cms/static/coffee/spec/main_spec.coffee index ee78d9f6f96bfcd88556eb72c7982a6adca06ede..db442b15ed8c5adee92b5f93a2d44bddf74b9b28 100644 --- a/cms/static/coffee/spec/main_spec.coffee +++ b/cms/static/coffee/spec/main_spec.coffee @@ -1,5 +1,5 @@ -require ["jquery", "backbone", "coffee/src/main", "js/spec_helpers/create_sinon", "jasmine-stealth", "jquery.cookie"], -($, Backbone, main, create_sinon) -> +require ["jquery", "backbone", "coffee/src/main", "js/common_helpers/ajax_helpers", "jasmine-stealth", "jquery.cookie"], +($, Backbone, main, AjaxHelpers) -> describe "CMS", -> it "should initialize URL", -> expect(window.CMS.URL).toBeDefined() @@ -28,7 +28,7 @@ require ["jquery", "backbone", "coffee/src/main", "js/spec_helpers/create_sinon" appendSetFixtures(sandbox({id: "page-notification"})) it "successful AJAX request does not pop an error notification", -> - server = create_sinon['server'](200, this) + server = AjaxHelpers['server'](200, this) expect($("#page-notification")).toBeEmpty() $.ajax("/test") @@ -37,7 +37,7 @@ require ["jquery", "backbone", "coffee/src/main", "js/spec_helpers/create_sinon" expect($("#page-notification")).toBeEmpty() it "AJAX request with error should pop an error notification", -> - server = create_sinon['server'](500, this) + server = AjaxHelpers['server'](500, this) $.ajax("/test") server.respond() @@ -45,7 +45,7 @@ require ["jquery", "backbone", "coffee/src/main", "js/spec_helpers/create_sinon" expect($("#page-notification")).toContain('div.wrapper-notification-error') it "can override AJAX request with error so it does not pop an error notification", -> - server = create_sinon['server'](500, this) + server = AjaxHelpers['server'](500, this) $.ajax url: "/test" diff --git a/cms/static/coffee/spec/models/section_spec.coffee b/cms/static/coffee/spec/models/section_spec.coffee index 4dfb66a7056ebc4191b5ddbb323812ef33ed3071..491cb340ec809f5d67d89c1e7c48704b6453edd2 100644 --- a/cms/static/coffee/spec/models/section_spec.coffee +++ b/cms/static/coffee/spec/models/section_spec.coffee @@ -1,4 +1,4 @@ -define ["js/models/section", "js/spec_helpers/create_sinon", "js/utils/module"], (Section, create_sinon, ModuleUtils) -> +define ["js/models/section", "js/common_helpers/ajax_helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) -> describe "Section", -> describe "basic", -> beforeEach -> @@ -34,7 +34,7 @@ define ["js/models/section", "js/spec_helpers/create_sinon", "js/utils/module"], }) it "show/hide a notification when it saves to the server", -> - server = create_sinon['server'](200, this) + server = AjaxHelpers['server'](200, this) @model.save() expect(Section.prototype.showNotification).toHaveBeenCalled() @@ -43,7 +43,7 @@ define ["js/models/section", "js/spec_helpers/create_sinon", "js/utils/module"], it "don't hide notification when saving fails", -> # this is handled by the global AJAX error handler - server = create_sinon['server'](500, this) + server = AjaxHelpers['server'](500, this) @model.save() server.respond() diff --git a/cms/static/coffee/spec/views/assets_spec.coffee b/cms/static/coffee/spec/views/assets_spec.coffee index 2a74b50c606f928360fbd60bdb3ce310823e00fc..ac893c86a29a214b056fb4d255cb5ce6dfc3f098 100644 --- a/cms/static/coffee/spec/views/assets_spec.coffee +++ b/cms/static/coffee/spec/views/assets_spec.coffee @@ -1,5 +1,5 @@ -define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], -($, jasmine, create_sinon, Squire) -> +define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], +($, jasmine, AjaxHelpers, Squire) -> feedbackTpl = readFixtures('system-feedback.underscore') assetLibraryTpl = readFixtures('asset-library.underscore') @@ -72,7 +72,7 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], describe "AJAX", -> it "should destroy itself on confirmation", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render().$(".remove-asset-button").click() ctorOptions = @promptSpies.constructor.mostRecentCall.args[0] @@ -92,7 +92,7 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], expect(@collection.contains(@model)).toBeFalsy() it "should not destroy itself if server errors", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render().$(".remove-asset-button").click() ctorOptions = @promptSpies.constructor.mostRecentCall.args[0] @@ -106,7 +106,7 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], expect(@collection.contains(@model)).toBeTruthy() it "should lock the asset on confirmation", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render().$(".lock-checkbox").click() # AJAX request has been sent, but not yet returned @@ -123,7 +123,7 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], expect(@model.get("locked")).toBeTruthy() it "should not lock the asset if server errors", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render().$(".lock-checkbox").click() # return an error response @@ -207,7 +207,7 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], thumbnail: null id: 'idx' @view.addAsset(model) - create_sinon.respondWithJson(requests, + AjaxHelpers.respondWithJson(requests, { assets: [ @mockAsset1, @mockAsset2, @@ -231,9 +231,9 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], describe "Basic", -> # Separate setup method to work-around mis-parenting of beforeEach methods setup = -> - requests = create_sinon.requests(this) + requests = AjaxHelpers.requests(this) @view.setPage(0) - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) return requests $.fn.fileupload = -> @@ -270,11 +270,11 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], expect($('.ui-loading').is(':visible')).toBe(false) it "should hide the status indicator if an error occurs while loading", -> - requests = create_sinon.requests(this) + requests = AjaxHelpers.requests(this) appendSetFixtures('<div class="ui-loading"/>') expect($('.ui-loading').is(':visible')).toBe(true) @view.setPage(0) - create_sinon.respondWithError(requests) + AjaxHelpers.respondWithError(requests) expect($('.ui-loading').is(':visible')).toBe(false) it "should render both assets", -> @@ -316,9 +316,9 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], describe "Sorting", -> # Separate setup method to work-around mis-parenting of beforeEach methods setup = -> - requests = create_sinon.requests(this) + requests = AjaxHelpers.requests(this) @view.setPage(0) - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) return requests it "should have the correct default sort order", -> @@ -331,30 +331,30 @@ define ["jquery", "jasmine", "js/spec_helpers/create_sinon", "squire"], expect(@view.sortDisplayName()).toBe("Date Added") expect(@view.collection.sortDirection).toBe("desc") @view.$("#js-asset-date-col").click() - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) expect(@view.sortDisplayName()).toBe("Date Added") expect(@view.collection.sortDirection).toBe("asc") @view.$("#js-asset-date-col").click() - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) expect(@view.sortDisplayName()).toBe("Date Added") expect(@view.collection.sortDirection).toBe("desc") it "should switch the sort order when clicking on a different column", -> requests = setup.call(this) @view.$("#js-asset-name-col").click() - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) expect(@view.sortDisplayName()).toBe("Name") expect(@view.collection.sortDirection).toBe("asc") @view.$("#js-asset-name-col").click() - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) expect(@view.sortDisplayName()).toBe("Name") expect(@view.collection.sortDirection).toBe("desc") it "should switch sort to most recent date added when a new asset is added", -> requests = setup.call(this) @view.$("#js-asset-name-col").click() - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) addMockAsset.call(this, requests) - create_sinon.respondWithJson(requests, @mockAssetsResponse) + AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) expect(@view.sortDisplayName()).toBe("Date Added") expect(@view.collection.sortDirection).toBe("desc") diff --git a/cms/static/coffee/spec/views/course_info_spec.coffee b/cms/static/coffee/spec/views/course_info_spec.coffee index aa2530185cf8805e0a30c881809ab0d3491d5d28..e9846b80d790a72b0ad4f74d3526e6e6b88809c2 100644 --- a/cms/static/coffee/spec/views/course_info_spec.coffee +++ b/cms/static/coffee/spec/views/course_info_spec.coffee @@ -1,5 +1,5 @@ -define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "js/spec_helpers/create_sinon"], -(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, create_sinon) -> +define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "js/common_helpers/ajax_helpers"], +(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, AjaxHelpers) -> describe "Course Updates and Handouts", -> courseInfoPage = """ @@ -101,7 +101,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model modalCover.click() it "does not rewrite links on save", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) # Create a new update, verifying that the model is created # in the collection and save is called. @@ -168,7 +168,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model @handoutsEdit.render() it "does not rewrite links on save", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) # Enter something in the handouts section, verifying that the model is saved # when "Save" is clicked. diff --git a/cms/static/coffee/spec/views/textbook_spec.coffee b/cms/static/coffee/spec/views/textbook_spec.coffee index bc067faac848d0c1268a110f6499b4095faa2aed..4bb5115fe85a8bcbe0d05ff91b82a000b5befa77 100644 --- a/cms/static/coffee/spec/views/textbook_spec.coffee +++ b/cms/static/coffee/spec/views/textbook_spec.coffee @@ -1,8 +1,8 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course", "js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks", "js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification", - "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers", "jasmine-stealth"], -(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, create_sinon, modal_helpers) -> + "js/common_helpers/ajax_helpers", "js/spec_helpers/modal_helpers", "jasmine-stealth"], +(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, AjaxHelpers, modal_helpers) -> feedbackTpl = readFixtures('system-feedback.underscore') beforeEach -> @@ -83,7 +83,7 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js delete CMS.URL.TEXTBOOKS it "should destroy itself on confirmation", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render().$(".delete").click() ctorOptions = @promptSpies.constructor.mostRecentCall.args[0] diff --git a/cms/static/coffee/spec/views/upload_spec.coffee b/cms/static/coffee/spec/views/upload_spec.coffee index a16b789a44c6276f02201d921364c211e88fd705..245f12f526401ccfb66ea64d6dc089dfdc9dc79c 100644 --- a/cms/static/coffee/spec/views/upload_spec.coffee +++ b/cms/static/coffee/spec/views/upload_spec.coffee @@ -1,4 +1,4 @@ -define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers"], (FileUpload, UploadDialog, Chapter, create_sinon, modal_helpers) -> +define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/common_helpers/ajax_helpers", "js/spec_helpers/modal_helpers"], (FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) -> feedbackTpl = readFixtures('system-feedback.underscore') @@ -98,7 +98,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec_h @clock.restore() it "can upload correctly", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render() @view.upload() @@ -115,7 +115,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec_h expect(@dialogResponse.pop()).toEqual("dummy_response") it "can handle upload errors", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render() @view.upload() @@ -124,7 +124,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec_h expect(@view.remove).not.toHaveBeenCalled() it "removes itself after two seconds on successful upload", -> - requests = create_sinon["requests"](this) + requests = AjaxHelpers["requests"](this) @view.render() @view.upload() diff --git a/cms/static/js/common_helpers b/cms/static/js/common_helpers new file mode 120000 index 0000000000000000000000000000000000000000..ac288058f722957ed34587908c5d2214d3d2c10a --- /dev/null +++ b/cms/static/js/common_helpers @@ -0,0 +1 @@ +../../../common/static/js/spec_helpers \ No newline at end of file diff --git a/cms/static/js/spec/utils/drag_and_drop_spec.js b/cms/static/js/spec/utils/drag_and_drop_spec.js index 1eea1f920a6ffc29b292affdf67fae2373e84e33..2599008b30364fdc485533e9e4e37c10f2589e95 100644 --- a/cms/static/js/spec/utils/drag_and_drop_spec.js +++ b/cms/static/js/spec/utils/drag_and_drop_spec.js @@ -1,5 +1,5 @@ -define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_helpers/create_sinon", "jquery", "underscore"], - function (ContentDragger, Notification, create_sinon, $, _) { +define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/common_helpers/ajax_helpers", "jquery", "underscore"], + function (ContentDragger, Notification, AjaxHelpers, $, _) { describe("Overview drag and drop functionality", function () { beforeEach(function () { setFixtures(readFixtures('mock/mock-outline.underscore')); @@ -310,7 +310,7 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel }); it("should send an update on reorder from one parent to another", function () { var requests, savingOptions; - requests = create_sinon["requests"](this); + requests = AjaxHelpers["requests"](this); ContentDragger.dragState.dropDestination = $('#unit-4'); ContentDragger.dragState.attachMethod = "after"; ContentDragger.dragState.parentList = $('#subsection-2'); @@ -341,7 +341,7 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel expect($('#subsection-2').data('refresh')).toHaveBeenCalled(); }); it("should send an update on reorder within the same parent", function () { - var requests = create_sinon["requests"](this); + var requests = AjaxHelpers["requests"](this); ContentDragger.dragState.dropDestination = $('#unit-2'); ContentDragger.dragState.attachMethod = "after"; ContentDragger.dragState.parentList = $('#subsection-1'); diff --git a/cms/static/js/spec/video/file_uploader_editor_spec.js b/cms/static/js/spec/video/file_uploader_editor_spec.js index 95b0913d070eb61ab848923dd3e4d86858685aa0..948ae84eb7e32d4b37dc83668396981f752e931d 100644 --- a/cms/static/js/spec/video/file_uploader_editor_spec.js +++ b/cms/static/js/spec/video/file_uploader_editor_spec.js @@ -1,8 +1,8 @@ define( [ - 'jquery', 'underscore', 'js/spec_helpers/create_sinon', 'squire' + 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'squire' ], -function ($, _, create_sinon, Squire) { +function ($, _, AjaxHelpers, Squire) { 'use strict'; describe('FileUploader', function () { var FileUploaderTemplate = readFixtures( diff --git a/cms/static/js/spec/video/translations_editor_spec.js b/cms/static/js/spec/video/translations_editor_spec.js index 632e20477322947ea1a248145fd17714853dbb13..ef5cf607e525389cc116b9aa06bfcf58606fae87 100644 --- a/cms/static/js/spec/video/translations_editor_spec.js +++ b/cms/static/js/spec/video/translations_editor_spec.js @@ -1,8 +1,8 @@ define( [ - 'jquery', 'underscore', 'js/spec_helpers/create_sinon', 'squire' + 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'squire' ], -function ($, _, create_sinon, Squire) { +function ($, _, AjaxHelpers, Squire) { 'use strict'; // TODO: fix BLD-1100 Disabled due to intermittent failure on master and in PR builds xdescribe('VideoTranslations', function () { diff --git a/cms/static/js/spec/views/assets_spec.js b/cms/static/js/spec/views/assets_spec.js index 61c24a63c9432444176010f217732ee33b0393a2..3e3bb7d050ef3bab323bf0951c9d132186342873 100644 --- a/cms/static/js/spec/views/assets_spec.js +++ b/cms/static/js/spec/views/assets_spec.js @@ -1,6 +1,6 @@ -define([ "jquery", "js/spec_helpers/create_sinon", "js/views/asset", "js/views/assets", +define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views/assets", "js/models/asset", "js/collections/asset", "js/spec_helpers/view_helpers" ], - function ($, create_sinon, AssetView, AssetsView, AssetModel, AssetCollection, view_helpers) { + function ($, AjaxHelpers, AssetView, AssetsView, AssetModel, AssetCollection, ViewHelpers) { describe("Assets", function() { var assetsView, mockEmptyAssetsResponse, mockAssetUploadResponse, @@ -64,18 +64,18 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/views/asset", "js/views/a var setup; setup = function() { var requests; - requests = create_sinon.requests(this); + requests = AjaxHelpers.requests(this); assetsView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyAssetsResponse); + AjaxHelpers.respondWithJson(requests, mockEmptyAssetsResponse); return requests; }; beforeEach(function () { - view_helpers.installMockAnalytics(); + ViewHelpers.installMockAnalytics(); }); afterEach(function () { - view_helpers.removeMockAnalytics(); + ViewHelpers.removeMockAnalytics(); }); it('shows the upload modal when clicked on "Upload your first asset" button', function () { diff --git a/cms/static/js/spec/views/baseview_spec.js b/cms/static/js/spec/views/baseview_spec.js index ee24c9f12180d69cb8ae98d6d2dbf4ffbc2d5bb3..4d88992a3ea145101a8a4ce5a3fb0c0461e273b9 100644 --- a/cms/static/js/spec/views/baseview_spec.js +++ b/cms/static/js/spec/views/baseview_spec.js @@ -1,6 +1,5 @@ -define(["jquery", "underscore", "js/views/baseview", "js/utils/handle_iframe_binding", "sinon", - "js/spec_helpers/edit_helpers"], - function ($, _, BaseView, IframeBinding, sinon, view_helpers) { +define(["jquery", "underscore", "js/views/baseview", "js/utils/handle_iframe_binding", "sinon"], + function ($, _, BaseView, IframeBinding, sinon) { describe("BaseView", function() { var baseViewPrototype; diff --git a/cms/static/js/spec/views/container_spec.js b/cms/static/js/spec/views/container_spec.js index 7f57eb1e91d773917900bab3ca662a48d62a2bc9..7b9398152fbe9852afbac1eb7c46d2658a17d445 100644 --- a/cms/static/js/spec/views/container_spec.js +++ b/cms/static/js/spec/views/container_spec.js @@ -1,7 +1,7 @@ -define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", +define([ "jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/edit_helpers", "js/views/container", "js/models/xblock_info", "jquery.simulate", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], - function ($, create_sinon, edit_helpers, ContainerView, XBlockInfo) { + function ($, AjaxHelpers, EditHelpers, ContainerView, XBlockInfo) { describe("Container View", function () { @@ -30,14 +30,14 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers respondWithMockXBlockFragment = function (requests, response) { var requestIndex = requests.length - 1; - create_sinon.respondWithJson(requests, response, requestIndex); + AjaxHelpers.respondWithJson(requests, response, requestIndex); }; beforeEach(function () { - edit_helpers.installMockXBlock(); - edit_helpers.installViewTemplates(); + EditHelpers.installMockXBlock(); + EditHelpers.installViewTemplates(); appendSetFixtures('<div class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="' + rootLocator + '"></div>'); - notificationSpy = edit_helpers.createNotificationSpy(); + notificationSpy = EditHelpers.createNotificationSpy(); model = new XBlockInfo({ id: rootLocator, display_name: 'Test AB Test', @@ -52,12 +52,12 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers }); afterEach(function () { - edit_helpers.uninstallMockXBlock(); + EditHelpers.uninstallMockXBlock(); containerView.remove(); }); init = function (caller) { - var requests = create_sinon.requests(caller); + var requests = AjaxHelpers.requests(caller); containerView.render(); respondWithMockXBlockFragment(requests, { @@ -188,11 +188,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers // Drag the first component in Group B to the first group. dragComponentAbove(groupBComponent1, groupAComponent1); - edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); respondToRequest(requests, 0, 200); - edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); respondToRequest(requests, 1, 200); - edit_helpers.verifyNotificationHidden(notificationSpy); + EditHelpers.verifyNotificationHidden(notificationSpy); }); it('does not hide saving message if failure', function () { @@ -200,9 +200,9 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers // Drag the first component in Group B to the first group. dragComponentAbove(groupBComponent1, groupAComponent1); - edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); respondToRequest(requests, 0, 500); - edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); // Since the first reorder call failed, the removal will not be called. verifyNumReorderCalls(requests, 1); diff --git a/cms/static/js/spec/views/group_configuration_spec.js b/cms/static/js/spec/views/group_configuration_spec.js index c9fa5e792560302317e0b2297e40a2733a076707..9bc0ea3c9e9f5365853f2c21411d88f595381d79 100644 --- a/cms/static/js/spec/views/group_configuration_spec.js +++ b/cms/static/js/spec/views/group_configuration_spec.js @@ -5,13 +5,13 @@ define([ 'js/views/group_configurations_list', 'js/views/group_configuration_edit', 'js/views/group_configuration_item', 'js/models/group', 'js/collections/group', 'js/views/group_edit', - 'js/views/feedback_notification', 'js/spec_helpers/create_sinon', - 'js/spec_helpers/edit_helpers', 'jasmine-stealth' + 'js/views/feedback_notification', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', + 'js/spec_helpers/view_helpers', 'jasmine-stealth' ], function( _, Course, GroupConfigurationModel, GroupConfigurationCollection, GroupConfigurationDetails, GroupConfigurationsList, GroupConfigurationEdit, GroupConfigurationItem, GroupModel, GroupCollection, GroupEdit, - Notification, create_sinon, view_helpers + Notification, AjaxHelpers, TemplateHelpers, ViewHelpers ) { 'use strict'; var SELECTORS = { @@ -92,7 +92,7 @@ define([ describe('GroupConfigurationDetails', function() { beforeEach(function() { - view_helpers.installTemplate('group-configuration-details', true); + TemplateHelpers.installTemplate('group-configuration-details', true); this.model = new GroupConfigurationModel({ name: 'Configuration', @@ -270,8 +270,8 @@ define([ }; beforeEach(function() { - view_helpers.installViewTemplates(); - view_helpers.installTemplates([ + ViewHelpers.installViewTemplates(); + TemplateHelpers.installTemplates([ 'group-configuration-edit', 'group-edit' ]); @@ -304,8 +304,8 @@ define([ }); it('should save properly', function() { - var requests = create_sinon.requests(this), - notificationSpy = view_helpers.createNotificationSpy(), + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy(), groups; this.view.$('.action-add-group').click(); @@ -315,9 +315,9 @@ define([ }); this.view.$('form').submit(); - view_helpers.verifyNotificationShowing(notificationSpy, /Saving/); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Saving/); requests[0].respond(200); - view_helpers.verifyNotificationHidden(notificationSpy); + ViewHelpers.verifyNotificationHidden(notificationSpy); expect(this.model).toBeCorrectValuesInModel({ name: 'New Configuration', @@ -331,14 +331,14 @@ define([ }); it('does not hide saving message if failure', function() { - var requests = create_sinon.requests(this), - notificationSpy = view_helpers.createNotificationSpy(); + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy(); setValuesToInputs(this.view, { inputName: 'New Configuration' }); this.view.$('form').submit(); - view_helpers.verifyNotificationShowing(notificationSpy, /Saving/); - create_sinon.respondWithError(requests); - view_helpers.verifyNotificationShowing(notificationSpy, /Saving/); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Saving/); + AjaxHelpers.respondWithError(requests); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Saving/); }); it('does not save on cancel', function() { @@ -373,7 +373,7 @@ define([ }); it('should be possible to correct validation errors', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); // Set incorrect value setValuesToInputs(this.view, { inputName: '' }); @@ -494,7 +494,7 @@ define([ var emptyMessage = 'You haven\'t created any group configurations yet.'; beforeEach(function() { - view_helpers.installTemplate('no-group-configurations', true); + TemplateHelpers.installTemplate('no-group-configurations', true); this.model = new GroupConfigurationModel({ id: 0 }); this.collection = new GroupConfigurationCollection(); @@ -533,7 +533,7 @@ define([ var clickDeleteItem; beforeEach(function() { - view_helpers.installTemplates([ + TemplateHelpers.installTemplates([ 'group-configuration-edit', 'group-configuration-details' ], true); this.model = new GroupConfigurationModel({ id: 0 }); @@ -547,9 +547,9 @@ define([ clickDeleteItem = function (view, promptSpy) { view.$('.delete').click(); - view_helpers.verifyPromptShowing(promptSpy, /Delete this Group Configuration/); - view_helpers.confirmPrompt(promptSpy); - view_helpers.verifyPromptHidden(promptSpy); + ViewHelpers.verifyPromptShowing(promptSpy, /Delete this Group Configuration/); + ViewHelpers.confirmPrompt(promptSpy); + ViewHelpers.verifyPromptHidden(promptSpy); }; it('should render properly', function() { @@ -564,43 +564,43 @@ define([ }); it('should destroy itself on confirmation of deleting', function () { - var requests = create_sinon.requests(this), - promptSpy = view_helpers.createPromptSpy(), - notificationSpy = view_helpers.createNotificationSpy(); + var requests = AjaxHelpers.requests(this), + promptSpy = ViewHelpers.createPromptSpy(), + notificationSpy = ViewHelpers.createNotificationSpy(); clickDeleteItem(this.view, promptSpy); // Backbone.emulateHTTP is enabled in our system, so setting this // option will fake PUT, PATCH and DELETE requests with a HTTP POST, // setting the X-HTTP-Method-Override header with the true method. - create_sinon.expectJsonRequest(requests, 'POST', '/group_configurations/0'); + AjaxHelpers.expectJsonRequest(requests, 'POST', '/group_configurations/0'); expect(_.last(requests).requestHeaders['X-HTTP-Method-Override']).toBe('DELETE'); - view_helpers.verifyNotificationShowing(notificationSpy, /Deleting/); - create_sinon.respondToDelete(requests); - view_helpers.verifyNotificationHidden(notificationSpy); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); + AjaxHelpers.respondToDelete(requests); + ViewHelpers.verifyNotificationHidden(notificationSpy); expect($(SELECTORS.itemView)).not.toExist(); }); it('does not hide deleting message if failure', function() { - var requests = create_sinon.requests(this), - promptSpy = view_helpers.createPromptSpy(), - notificationSpy = view_helpers.createNotificationSpy(); + var requests = AjaxHelpers.requests(this), + promptSpy = ViewHelpers.createPromptSpy(), + notificationSpy = ViewHelpers.createNotificationSpy(); clickDeleteItem(this.view, promptSpy); // Backbone.emulateHTTP is enabled in our system, so setting this // option will fake PUT, PATCH and DELETE requests with a HTTP POST, // setting the X-HTTP-Method-Override header with the true method. - create_sinon.expectJsonRequest(requests, 'POST', '/group_configurations/0'); + AjaxHelpers.expectJsonRequest(requests, 'POST', '/group_configurations/0'); expect(_.last(requests).requestHeaders['X-HTTP-Method-Override']).toBe('DELETE'); - view_helpers.verifyNotificationShowing(notificationSpy, /Deleting/); - create_sinon.respondWithError(requests); - view_helpers.verifyNotificationShowing(notificationSpy, /Deleting/); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); + AjaxHelpers.respondWithError(requests); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); expect($(SELECTORS.itemView)).toExist(); }); }); describe('GroupEdit', function() { beforeEach(function() { - view_helpers.installTemplate('group-edit', true); + TemplateHelpers.installTemplate('group-edit', true); this.model = new GroupModel({ name: 'Group A' diff --git a/cms/static/js/spec/views/modals/base_modal_spec.js b/cms/static/js/spec/views/modals/base_modal_spec.js index 88a594483600200ba7a2328129fc6492f1b7fb84..2d8d312bbe3675b0e7b68d4f96e0ea6dd51d0ab9 100644 --- a/cms/static/js/spec/views/modals/base_modal_spec.js +++ b/cms/static/js/spec/views/modals/base_modal_spec.js @@ -1,5 +1,5 @@ define(["jquery", "underscore", "js/views/modals/base_modal", "js/spec_helpers/modal_helpers"], - function ($, _, BaseModal, modal_helpers) { + function ($, _, BaseModal, ModelHelpers) { describe("BaseModal", function() { var MockModal, modal, showMockModal; @@ -18,29 +18,29 @@ define(["jquery", "underscore", "js/views/modals/base_modal", "js/spec_helpers/m }; beforeEach(function () { - modal_helpers.installModalTemplates(); + ModelHelpers.installModalTemplates(); }); afterEach(function() { - modal_helpers.hideModalIfShowing(modal); + ModelHelpers.hideModalIfShowing(modal); }); describe("Single Modal", function() { it('is visible after show is called', function () { showMockModal(); - expect(modal_helpers.isShowingModal(modal)).toBeTruthy(); + expect(ModelHelpers.isShowingModal(modal)).toBeTruthy(); }); it('is removed after hide is called', function () { showMockModal(); modal.hide(); - expect(modal_helpers.isShowingModal(modal)).toBeFalsy(); + expect(ModelHelpers.isShowingModal(modal)).toBeFalsy(); }); it('is removed after cancel is clicked', function () { showMockModal(); - modal_helpers.cancelModal(modal); - expect(modal_helpers.isShowingModal(modal)).toBeFalsy(); + ModelHelpers.cancelModal(modal); + expect(ModelHelpers.isShowingModal(modal)).toBeFalsy(); }); }); @@ -57,32 +57,32 @@ define(["jquery", "underscore", "js/views/modals/base_modal", "js/spec_helpers/m }; afterEach(function() { - if (nestedModal && modal_helpers.isShowingModal(nestedModal)) { + if (nestedModal && ModelHelpers.isShowingModal(nestedModal)) { nestedModal.hide(); } }); it('is visible after show is called', function () { showNestedModal(); - expect(modal_helpers.isShowingModal(nestedModal)).toBeTruthy(); + expect(ModelHelpers.isShowingModal(nestedModal)).toBeTruthy(); }); it('is removed after hide is called', function () { showNestedModal(); nestedModal.hide(); - expect(modal_helpers.isShowingModal(nestedModal)).toBeFalsy(); + expect(ModelHelpers.isShowingModal(nestedModal)).toBeFalsy(); // Verify that the parent modal is still showing - expect(modal_helpers.isShowingModal(modal)).toBeTruthy(); + expect(ModelHelpers.isShowingModal(modal)).toBeTruthy(); }); it('is removed after cancel is clicked', function () { showNestedModal(); - modal_helpers.cancelModal(nestedModal); - expect(modal_helpers.isShowingModal(nestedModal)).toBeFalsy(); + ModelHelpers.cancelModal(nestedModal); + expect(ModelHelpers.isShowingModal(nestedModal)).toBeFalsy(); // Verify that the parent modal is still showing - expect(modal_helpers.isShowingModal(modal)).toBeTruthy(); + expect(ModelHelpers.isShowingModal(modal)).toBeTruthy(); }); }); }); diff --git a/cms/static/js/spec/views/modals/edit_xblock_spec.js b/cms/static/js/spec/views/modals/edit_xblock_spec.js index 301c1b8a441d539c389bd35a36599e28a4644091..99de1404c87edb27a305ba6d7e9974bde7200a0d 100644 --- a/cms/static/js/spec/views/modals/edit_xblock_spec.js +++ b/cms/static/js/spec/views/modals/edit_xblock_spec.js @@ -1,17 +1,17 @@ -define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", +define(["jquery", "underscore", "js/common_helpers/ajax_helpers", "js/spec_helpers/edit_helpers", "js/views/modals/edit_xblock", "js/models/xblock_info"], - function ($, _, create_sinon, edit_helpers, EditXBlockModal, XBlockInfo) { + function ($, _, AjaxHelpers, EditHelpers, EditXBlockModal, XBlockInfo) { describe("EditXBlockModal", function() { var model, modal, showModal; showModal = function(requests, mockHtml, options) { var xblockElement = $('.xblock'); - return edit_helpers.showEditModal(requests, xblockElement, model, mockHtml, options); + return EditHelpers.showEditModal(requests, xblockElement, model, mockHtml, options); }; beforeEach(function () { - edit_helpers.installEditTemplates(); + EditHelpers.installEditTemplates(); appendSetFixtures('<div class="xblock" data-locator="mock-xblock"></div>'); model = new XBlockInfo({ id: 'testCourse/branch/draft/block/verticalFFF', @@ -21,7 +21,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers }); afterEach(function() { - edit_helpers.cancelModalIfShowing(); + EditHelpers.cancelModalIfShowing(); }); describe("XBlock Editor", function() { @@ -30,42 +30,42 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); beforeEach(function () { - edit_helpers.installMockXBlock(); + EditHelpers.installMockXBlock(); }); afterEach(function() { - edit_helpers.uninstallMockXBlock(); + EditHelpers.uninstallMockXBlock(); }); it('can show itself', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXBlockEditorHtml); - expect(edit_helpers.isShowingModal(modal)).toBeTruthy(); - edit_helpers.cancelModal(modal); - expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + EditHelpers.cancelModal(modal); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); }); it('does not show the "Save" button', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXBlockEditorHtml); expect(modal.$('.action-save')).not.toBeVisible(); expect(modal.$('.action-cancel').text()).toBe('OK'); }); it('shows the correct title', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXBlockEditorHtml); expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); }); it('does not show any editor mode buttons', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXBlockEditorHtml); expect(modal.$('.editor-modes a').length).toBe(0); }); it('hides itself and refreshes after save notification', function() { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), refreshed = false, refresh = function() { refreshed = true; @@ -73,19 +73,19 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh }); modal.editorView.notifyRuntime('save', { state: 'start' }); modal.editorView.notifyRuntime('save', { state: 'end' }); - expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); expect(refreshed).toBeTruthy(); }); it('hides itself and does not refresh after cancel notification', function() { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), refreshed = false, refresh = function() { refreshed = true; }; modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh }); modal.editorView.notifyRuntime('cancel'); - expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); expect(refreshed).toBeFalsy(); }); @@ -95,7 +95,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers mockCustomButtonsHtml = readFixtures('mock/mock-xblock-editor-with-custom-buttons.underscore'); it('hides the modal\'s button bar', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockCustomButtonsHtml); expect(modal.$('.modal-actions')).toBeHidden(); }); @@ -108,29 +108,29 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore'); beforeEach(function() { - edit_helpers.installMockXModule(); + EditHelpers.installMockXModule(); }); afterEach(function () { - edit_helpers.uninstallMockXModule(); + EditHelpers.uninstallMockXModule(); }); it('can render itself', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXModuleEditorHtml); - expect(edit_helpers.isShowingModal(modal)).toBeTruthy(); - edit_helpers.cancelModal(modal); - expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + EditHelpers.cancelModal(modal); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); }); it('shows the correct title', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXModuleEditorHtml); expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); }); it('shows the correct default buttons', function() { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), editorButton, settingsButton; modal = showModal(requests, mockXModuleEditorHtml); @@ -144,7 +144,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers }); it('can switch tabs', function() { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), editorButton, settingsButton; modal = showModal(requests, mockXModuleEditorHtml); @@ -164,13 +164,13 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers mockCustomTabsHtml = readFixtures('mock/mock-xmodule-editor-with-custom-tabs.underscore'); it('hides the modal\'s header', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockCustomTabsHtml); expect(modal.$('.modal-header')).toBeHidden(); }); it('shows the correct title', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockCustomTabsHtml); expect(modal.$('.component-name').text()).toBe('Editing: Component'); }); @@ -183,23 +183,23 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore'); beforeEach(function() { - edit_helpers.installMockXModule(); + EditHelpers.installMockXModule(); }); afterEach(function () { - edit_helpers.uninstallMockXModule(); + EditHelpers.uninstallMockXModule(); }); it('can render itself', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXModuleEditorHtml); - expect(edit_helpers.isShowingModal(modal)).toBeTruthy(); - edit_helpers.cancelModal(modal); - expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + EditHelpers.cancelModal(modal); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); }); it('does not show any mode buttons', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); modal = showModal(requests, mockXModuleEditorHtml); expect(modal.$('.editor-modes li').length).toBe(0); }); diff --git a/cms/static/js/spec/views/modals/validation_error_modal_spec.js b/cms/static/js/spec/views/modals/validation_error_modal_spec.js index bc23303bcee1607a76d5be90c21d415521bb151d..7a722fd9849c0589e1b9248c806d784d028b4415 100644 --- a/cms/static/js/spec/views/modals/validation_error_modal_spec.js +++ b/cms/static/js/spec/views/modals/validation_error_modal_spec.js @@ -1,5 +1,5 @@ define(['jquery', 'underscore', 'js/spec_helpers/validation_helpers', 'js/views/modals/validation_error_modal'], - function ($, _, validation_helpers, ValidationErrorModal) { + function ($, _, ValidationHelpers, ValidationErrorModal) { describe('ValidationErrorModal', function() { var modal, showModal; @@ -14,24 +14,24 @@ define(['jquery', 'underscore', 'js/spec_helpers/validation_helpers', 'js/views/ /* Before each, install templates required for the base modal and validation error modal. */ beforeEach(function () { - validation_helpers.installValidationTemplates(); + ValidationHelpers.installValidationTemplates(); }); afterEach(function() { - validation_helpers.hideModalIfShowing(modal); + ValidationHelpers.hideModalIfShowing(modal); }); it('is visible after show is called', function () { showModal([]); - expect(validation_helpers.isShowingModal(modal)).toBeTruthy(); + expect(ValidationHelpers.isShowingModal(modal)).toBeTruthy(); }); it('displays none if no error given', function () { var errorObjects = []; showModal(errorObjects); - expect(validation_helpers.isShowingModal(modal)).toBeTruthy(); - validation_helpers.checkErrorContents(modal, errorObjects); + expect(ValidationHelpers.isShowingModal(modal)).toBeTruthy(); + ValidationHelpers.checkErrorContents(modal, errorObjects); }); it('correctly displays json error message objects', function () { @@ -47,8 +47,8 @@ define(['jquery', 'underscore', 'js/spec_helpers/validation_helpers', 'js/views/ ]; showModal(errorObjects); - expect(validation_helpers.isShowingModal(modal)).toBeTruthy(); - validation_helpers.checkErrorContents(modal, errorObjects); + expect(ValidationHelpers.isShowingModal(modal)).toBeTruthy(); + ValidationHelpers.checkErrorContents(modal, errorObjects); }); it('run callback when undo changes button is clicked', function () { @@ -69,8 +69,8 @@ define(['jquery', 'underscore', 'js/spec_helpers/validation_helpers', 'js/views/ // Show Modal and click undo changes showModal(errorObjects, callback); - expect(validation_helpers.isShowingModal(modal)).toBeTruthy(); - validation_helpers.undoChanges(modal); + expect(ValidationHelpers.isShowingModal(modal)).toBeTruthy(); + ValidationHelpers.undoChanges(modal); // Wait for the callback to be fired waitsFor(function () { @@ -79,7 +79,7 @@ define(['jquery', 'underscore', 'js/spec_helpers/validation_helpers', 'js/views/ // After checking callback fire, check modal hide runs(function () { - expect(validation_helpers.isShowingModal(modal)).toBe(false); + expect(ValidationHelpers.isShowingModal(modal)).toBe(false); }); }); }); diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index 1089ec47a66f8275d503a0eeee0744fb3e9f87cc..8077c0ed7afad5338238ab832bae563c18d155d0 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -1,6 +1,7 @@ -define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", +define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_helpers", + "js/common_helpers/template_helpers", "js/spec_helpers/edit_helpers", "js/views/pages/container", "js/models/xblock_info", "jquery.simulate"], - function ($, _, str, create_sinon, edit_helpers, ContainerPage, XBlockInfo) { + function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, XBlockInfo) { describe("ContainerPage", function() { var lastRequest, renderContainerPage, expectComponents, respondWithHtml, @@ -15,12 +16,12 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin beforeEach(function () { var newDisplayName = 'New Display Name'; - edit_helpers.installEditTemplates(); - edit_helpers.installTemplate('xblock-string-field-editor'); - edit_helpers.installTemplate('container-message'); + EditHelpers.installEditTemplates(); + TemplateHelpers.installTemplate('xblock-string-field-editor'); + TemplateHelpers.installTemplate('container-message'); appendSetFixtures(mockContainerPage); - edit_helpers.installMockXBlock({ + EditHelpers.installMockXBlock({ data: "<p>Some HTML</p>", metadata: { display_name: newDisplayName @@ -37,14 +38,14 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); afterEach(function() { - edit_helpers.uninstallMockXBlock(); + EditHelpers.uninstallMockXBlock(); }); lastRequest = function() { return requests[requests.length - 1]; }; respondWithHtml = function(html) { var requestIndex = requests.length - 1; - create_sinon.respondWithJson( + AjaxHelpers.respondWithJson( requests, { html: html, "resources": [] }, requestIndex @@ -52,10 +53,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }; renderContainerPage = function(test, html, options) { - requests = create_sinon.requests(test); + requests = AjaxHelpers.requests(test); containerPage = new ContainerPage(_.extend(options || {}, { model: model, - templates: edit_helpers.mockComponentTemplates, + templates: EditHelpers.mockComponentTemplates, el: $('#content') })); containerPage.render(); @@ -79,7 +80,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); it('shows a loading indicator', function() { - requests = create_sinon.requests(this); + requests = AjaxHelpers.requests(this); containerPage.render(); expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden'); respondWithHtml(mockContainerXBlockHtml); @@ -113,7 +114,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin getDisplayNameWrapper; afterEach(function() { - edit_helpers.cancelModalIfShowing(); + EditHelpers.cancelModalIfShowing(); }); getDisplayNameWrapper = function() { @@ -131,30 +132,30 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // Expect a request to be made to show the studio view for the container expect(str.startsWith(lastRequest().url, '/xblock/locator-container/studio_view')).toBeTruthy(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockContainerXBlockHtml, resources: [] }); - expect(edit_helpers.isShowingModal()).toBeTruthy(); + expect(EditHelpers.isShowingModal()).toBeTruthy(); // Expect the correct title to be shown - expect(edit_helpers.getModalTitle()).toBe('Editing: Test Container'); + expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container'); // Press the save button and respond with a success message to the save - edit_helpers.pressModalButton('.action-save'); - create_sinon.respondWithJson(requests, { }); - expect(edit_helpers.isShowingModal()).toBeFalsy(); + EditHelpers.pressModalButton('.action-save'); + AjaxHelpers.respondWithJson(requests, { }); + expect(EditHelpers.isShowingModal()).toBeFalsy(); // Expect the last request be to refresh the container page expect(str.startsWith(lastRequest().url, '/xblock/locator-container/container_preview')).toBeTruthy(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockUpdatedContainerXBlockHtml, resources: [] }); // Respond to the subsequent xblock info fetch request. - create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName}); + AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName}); // Expect the title to have been updated expect(displayNameElement.text().trim()).toBe(updatedDisplayName); @@ -164,20 +165,20 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin var displayNameInput, displayNameWrapper; renderContainerPage(this, mockContainerXBlockHtml); displayNameWrapper = getDisplayNameWrapper(); - displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName); + displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName); displayNameInput.change(); // This is the response for the change operation. - create_sinon.respondWithJson(requests, { }); + AjaxHelpers.respondWithJson(requests, { }); // This is the response for the subsequent fetch operation. - create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName}); - edit_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); + AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName}); + EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); expect(containerPage.model.get('display_name')).toBe(updatedDisplayName); }); }); describe("Editing an xblock", function() { afterEach(function() { - edit_helpers.cancelModalIfShowing(); + EditHelpers.cancelModalIfShowing(); }); it('can show an edit modal for a child xblock', function() { @@ -189,11 +190,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin editButtons[0].click(); // Make sure that the correct xblock is requested to be edited expect(str.startsWith(lastRequest().url, '/xblock/locator-component-A1/studio_view')).toBeTruthy(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockXBlockEditorHtml, resources: [] }); - expect(edit_helpers.isShowingModal()).toBeTruthy(); + expect(EditHelpers.isShowingModal()).toBeTruthy(); }); it('can show an edit modal for a child xblock with broken JavaScript', function() { @@ -201,11 +202,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin renderContainerPage(this, mockBadContainerXBlockHtml); editButtons = containerPage.$('.wrapper-xblock .edit-button'); editButtons[0].click(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockXBlockEditorHtml, resources: [] }); - expect(edit_helpers.isShowingModal()).toBeTruthy(); + expect(EditHelpers.isShowingModal()).toBeTruthy(); }); }); @@ -214,7 +215,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin newDisplayName = 'New Display Name'; beforeEach(function () { - edit_helpers.installMockXModule({ + EditHelpers.installMockXModule({ data: "<p>Some HTML</p>", metadata: { display_name: newDisplayName @@ -223,8 +224,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); afterEach(function() { - edit_helpers.uninstallMockXModule(); - edit_helpers.cancelModalIfShowing(); + EditHelpers.uninstallMockXModule(); + EditHelpers.cancelModalIfShowing(); }); it('can save changes to settings', function() { @@ -235,7 +236,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // The container should have rendered six mock xblocks expect(editButtons.length).toBe(6); editButtons[0].click(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockXModuleEditor, resources: [] }); @@ -249,7 +250,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // Press the save button modal.find('.action-save').click(); // Respond to the save - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { id: model.id }); @@ -278,7 +279,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin promptSpy; beforeEach(function() { - promptSpy = edit_helpers.createPromptSpy(); + promptSpy = EditHelpers.createPromptSpy(); }); clickDelete = function(componentIndex, clickNo) { @@ -291,20 +292,20 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin deleteButtons[componentIndex].click(); // click the 'yes' or 'no' button in the prompt - edit_helpers.confirmPrompt(promptSpy, clickNo); + EditHelpers.confirmPrompt(promptSpy, clickNo); }; deleteComponent = function(componentIndex) { clickDelete(componentIndex); - create_sinon.respondWithJson(requests, {}); + AjaxHelpers.respondWithJson(requests, {}); // second to last request contains given component's id (to delete the component) - create_sinon.expectJsonRequest(requests, 'DELETE', + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1), null, requests.length - 2); // final request to refresh the xblock info - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); }; deleteComponentWithSuccess = function(componentIndex) { @@ -335,13 +336,13 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it("can delete an xblock with broken JavaScript", function() { renderContainerPage(this, mockBadContainerXBlockHtml); containerPage.$('.delete-button').first().click(); - edit_helpers.confirmPrompt(promptSpy); - create_sinon.respondWithJson(requests, {}); + EditHelpers.confirmPrompt(promptSpy); + AjaxHelpers.respondWithJson(requests, {}); // expect the second to last request to be a delete of the xblock - create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript', + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript', null, requests.length - 2); // expect the last request to be a fetch of the xblock info for the parent container - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); }); it('does not delete when clicking No in prompt', function () { @@ -361,21 +362,21 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); it('shows a notification during the delete operation', function() { - var notificationSpy = edit_helpers.createNotificationSpy(); + var notificationSpy = EditHelpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); clickDelete(0); - edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/); - create_sinon.respondWithJson(requests, {}); - edit_helpers.verifyNotificationHidden(notificationSpy); + EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); + AjaxHelpers.respondWithJson(requests, {}); + EditHelpers.verifyNotificationHidden(notificationSpy); }); it('does not delete an xblock upon failure', function () { - var notificationSpy = edit_helpers.createNotificationSpy(); + var notificationSpy = EditHelpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); clickDelete(0); - edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/); - create_sinon.respondWithError(requests); - edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/); + EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); + AjaxHelpers.respondWithError(requests); + EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); expectComponents(getGroupElement(), allComponentsInGroup); }); }); @@ -400,13 +401,13 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin clickDuplicate(componentIndex); // verify content of request - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'duplicate_source_locator': 'locator-component-' + GROUP_TO_TEST + (componentIndex + 1), 'parent_locator': 'locator-group-' + GROUP_TO_TEST }); // send the response - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { 'locator': 'locator-duplicated-component' }); @@ -432,31 +433,31 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it("can duplicate an xblock with broken JavaScript", function() { renderContainerPage(this, mockBadContainerXBlockHtml); containerPage.$('.duplicate-button').first().click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'duplicate_source_locator': 'locator-broken-javascript', 'parent_locator': 'locator-container' }); }); it('shows a notification when duplicating', function () { - var notificationSpy = edit_helpers.createNotificationSpy(); + var notificationSpy = EditHelpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); clickDuplicate(0); - edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/); - create_sinon.respondWithJson(requests, {"locator": "new_item"}); - edit_helpers.verifyNotificationHidden(notificationSpy); + EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); + AjaxHelpers.respondWithJson(requests, {"locator": "new_item"}); + EditHelpers.verifyNotificationHidden(notificationSpy); }); it('does not duplicate an xblock upon failure', function () { - var notificationSpy = edit_helpers.createNotificationSpy(); + var notificationSpy = EditHelpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); refreshXBlockSpies = spyOn(containerPage, "refreshXBlock"); clickDuplicate(0); - edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/); - create_sinon.respondWithError(requests); + EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); + AjaxHelpers.respondWithError(requests); expectComponents(getGroupElement(), allComponentsInGroup); expect(refreshXBlockSpies).not.toHaveBeenCalled(); - edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/); + EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); }); }); @@ -470,7 +471,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('sends the correct JSON to the server', function () { renderContainerPage(this, mockContainerXBlockHtml); clickNewComponent(0); - edit_helpers.verifyXBlockRequest(requests, { + EditHelpers.verifyXBlockRequest(requests, { "category": "discussion", "type": "discussion", "parent_locator": "locator-group-A" @@ -478,12 +479,12 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); it('shows a notification while creating', function () { - var notificationSpy = edit_helpers.createNotificationSpy(); + var notificationSpy = EditHelpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); clickNewComponent(0); - edit_helpers.verifyNotificationShowing(notificationSpy, /Adding/); - create_sinon.respondWithJson(requests, { }); - edit_helpers.verifyNotificationHidden(notificationSpy); + EditHelpers.verifyNotificationShowing(notificationSpy, /Adding/); + AjaxHelpers.respondWithJson(requests, { }); + EditHelpers.verifyNotificationHidden(notificationSpy); }); it('does not insert component upon failure', function () { @@ -491,7 +492,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin renderContainerPage(this, mockContainerXBlockHtml); clickNewComponent(0); requestCount = requests.length; - create_sinon.respondWithError(requests); + AjaxHelpers.respondWithError(requests); // No new requests should be made to refresh the view expect(requests.length).toBe(requestCount); expectComponents(getGroupElement(), allComponentsInGroup); @@ -511,8 +512,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin showTemplatePicker(); xblockCount = containerPage.$('.studio-xblock-wrapper').length; containerPage.$('.new-component-html a')[templateIndex].click(); - edit_helpers.verifyXBlockRequest(requests, expectedRequest); - create_sinon.respondWithJson(requests, {"locator": "new_item"}); + EditHelpers.verifyXBlockRequest(requests, expectedRequest); + AjaxHelpers.respondWithJson(requests, {"locator": "new_item"}); respondWithHtml(mockXBlockHtml); expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1); }; diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js index fca5939de0d0d43a2ea35ba8a5571c27c3c5079b..50653efcc7268619428a3c137c7b89d90feb6967 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -1,7 +1,8 @@ -define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", +define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_helpers", + "js/common_helpers/template_helpers", "js/spec_helpers/edit_helpers", "js/views/feedback_prompt", "js/views/pages/container", "js/views/pages/container_subviews", "js/models/xblock_info", "js/views/utils/xblock_utils"], - function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, ContainerSubviews, + function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, Prompt, ContainerPage, ContainerSubviews, XBlockInfo, XBlockUtils) { var VisibilityState = XBlockUtils.VisibilityState; @@ -13,11 +14,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin mockContainerXBlockHtml = readFixtures('mock/mock-empty-container-xblock.underscore'); beforeEach(function () { - edit_helpers.installTemplate('xblock-string-field-editor'); - edit_helpers.installTemplate('publish-xblock'); - edit_helpers.installTemplate('publish-history'); - edit_helpers.installTemplate('unit-outline'); - edit_helpers.installTemplate('container-message'); + TemplateHelpers.installTemplate('xblock-string-field-editor'); + TemplateHelpers.installTemplate('publish-xblock'); + TemplateHelpers.installTemplate('publish-history'); + TemplateHelpers.installTemplate('unit-outline'); + TemplateHelpers.installTemplate('container-message'); appendSetFixtures(mockContainerPage); }); @@ -38,11 +39,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }; createContainerPage = function (test, options) { - requests = create_sinon.requests(test); + requests = AjaxHelpers.requests(test); model = new XBlockInfo(createXBlockInfo(options), { parse: true }); containerPage = new ContainerPage({ model: model, - templates: edit_helpers.mockComponentTemplates, + templates: EditHelpers.mockComponentTemplates, el: $('#content'), isUnitPage: true }); @@ -56,7 +57,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin respondWithHtml = function(html) { var requestIndex = requests.length - 1; - create_sinon.respondWithJson( + AjaxHelpers.respondWithJson( requests, { html: html, "resources": [] }, requestIndex @@ -64,7 +65,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }; respondWithJson = function(json, requestIndex) { - create_sinon.respondWithJson( + AjaxHelpers.respondWithJson( requests, json, requestIndex @@ -142,7 +143,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin expect(promptSpies.constructor).toHaveBeenCalled(); promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(promptSpies); - create_sinon.expectJsonRequest(requests, "POST", "/xblock/locator-container", + AjaxHelpers.expectJsonRequest(requests, "POST", "/xblock/locator-container", {"publish": "discard_changes"} ); }; @@ -219,7 +220,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); it('can publish private content', function () { - var notificationSpy = edit_helpers.createNotificationSpy(); + var notificationSpy = EditHelpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass); expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); @@ -227,17 +228,17 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // Click publish containerPage.$(publishButtonCss).click(); - edit_helpers.verifyNotificationShowing(notificationSpy, /Publishing/); + EditHelpers.verifyNotificationShowing(notificationSpy, /Publishing/); - create_sinon.expectJsonRequest(requests, "POST", "/xblock/locator-container", + AjaxHelpers.expectJsonRequest(requests, "POST", "/xblock/locator-container", {"publish": "make_public"} ); // Response to publish call respondWithJson({"id": "locator-container", "data": null, "metadata":{}}); - edit_helpers.verifyNotificationHidden(notificationSpy); + EditHelpers.verifyNotificationHidden(notificationSpy); - create_sinon.expectJsonRequest(requests, "GET", "/xblock/locator-container"); + AjaxHelpers.expectJsonRequest(requests, "GET", "/xblock/locator-container"); // Response to fetch respondWithJson(createXBlockInfo({ published: true, has_changes: false, visibility_state: VisibilityState.ready @@ -258,7 +259,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin var numRequests = requests.length; // Respond with failure - create_sinon.respondWithError(requests); + AjaxHelpers.respondWithError(requests); expect(requests.length).toEqual(numRequests); @@ -271,7 +272,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('can discard changes', function () { var notificationSpy, renderPageSpy, numRequests; createContainerPage(this); - notificationSpy = edit_helpers.createNotificationSpy(); + notificationSpy = EditHelpers.createNotificationSpy(); renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').andCallThrough(); sendDiscardChangesToServer(); @@ -279,7 +280,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // Respond with success. respondWithJson({"id": "locator-container"}); - edit_helpers.verifyNotificationHidden(notificationSpy); + EditHelpers.verifyNotificationHidden(notificationSpy); // Verify other requests are sent to the server to update page state. // Response to fetch, specifying the very next request (as multiple requests will be sent to server) @@ -297,7 +298,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin numRequests = requests.length; // Respond with failure - create_sinon.respondWithError(requests); + AjaxHelpers.respondWithError(requests); expect(requests.length).toEqual(numRequests); expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled'); @@ -393,14 +394,14 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin // If removing explicit staff lock with no implicit staff lock, click 'Yes' to confirm if (!isStaffOnly && !containerPage.model.get('ancestor_has_staff_lock')) { - edit_helpers.confirmPrompt(promptSpy); + EditHelpers.confirmPrompt(promptSpy); } - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/locator-container', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/locator-container', { publish: 'republish', metadata: { visible_to_staff_only: isStaffOnly ? true : null } }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { data: null, id: "locator-container", metadata: { @@ -408,13 +409,13 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin } }); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); if (isStaffOnly || containerPage.model.get('ancestor_has_staff_lock')) { newVisibilityState = VisibilityState.staffOnly; } else { newVisibilityState = VisibilityState.live; } - create_sinon.respondWithJson(requests, createXBlockInfo({ + AjaxHelpers.respondWithJson(requests, createXBlockInfo({ published: containerPage.model.get('published'), has_explicit_staff_lock: isStaffOnly, visibility_state: newVisibilityState, @@ -423,11 +424,12 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }; verifyStaffOnly = function(isStaffOnly) { + var visibilityCopy = containerPage.$('.wrapper-visibility .copy').text().trim(); if (isStaffOnly) { - expect(containerPage.$('.wrapper-visibility .copy').text()).toContain('Staff Only'); + expect(visibilityCopy).toContain('Staff Only'); expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyClass); } else { - expect(containerPage.$('.wrapper-visibility .copy').text().trim()).toBe('Staff and Students'); + expect(visibilityCopy).toBe('Staff and Students'); expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass); verifyExplicitStaffOnly(false); verifyImplicitStaffOnly(false); @@ -506,7 +508,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); it("can remove explicit staff only setting without having implicit staff only", function() { - promptSpy = edit_helpers.createPromptSpy(); + promptSpy = EditHelpers.createPromptSpy(); renderContainerPage(this, mockContainerXBlockHtml, { visibility_state: VisibilityState.staffOnly, has_explicit_staff_lock: true, @@ -517,7 +519,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); it("can remove explicit staff only setting while having implicit staff only", function() { - promptSpy = edit_helpers.createPromptSpy(); + promptSpy = EditHelpers.createPromptSpy(); renderContainerPage(this, mockContainerXBlockHtml, { visibility_state: VisibilityState.staffOnly, ancestor_has_staff_lock: true, @@ -532,7 +534,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it("does not refresh if removing staff only is canceled", function() { var requestCount; - promptSpy = edit_helpers.createPromptSpy(); + promptSpy = EditHelpers.createPromptSpy(); renderContainerPage(this, mockContainerXBlockHtml, { visibility_state: VisibilityState.staffOnly, has_explicit_staff_lock: true, @@ -540,7 +542,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); requestCount = requests.length; containerPage.$('.action-staff-lock').click(); - edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel + EditHelpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel expect(requests.length).toBe(requestCount); verifyExplicitStaffOnly(true); verifyStaffOnly(true); @@ -551,7 +553,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin renderContainerPage(this, mockContainerXBlockHtml); containerPage.$('.lock-checkbox').click(); requestCount = requests.length; - create_sinon.respondWithError(requests); + AjaxHelpers.respondWithError(requests); expect(requests.length).toBe(requestCount); verifyStaffOnly(false); }); @@ -588,7 +590,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("Message Area", function() { var messageSelector = '.container-message .warning', - warningMessage = 'Caution: The last published version of this unit is live. By publishing changes you will change the student experience.'; + warningMessage = 'Caution: The last published version of this unit is live. ' + + 'By publishing changes you will change the student experience.'; it('is empty for a unit that is not currently visible to students', function() { renderContainerPage(this, mockContainerXBlockHtml, { 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 a13537051d3333450fa66f5a8ac8467add8137d8..77462d11379e90593bcf7ae74d89fe66476f5bbe 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -1,12 +1,14 @@ -define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/views/utils/view_utils", - "js/views/pages/course_outline", "js/models/xblock_outline_info", "js/utils/date_utils", "js/spec_helpers/edit_helpers"], - function ($, create_sinon, view_helpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils, edit_helpers) { +define(["jquery", "js/common_helpers/ajax_helpers", "js/views/utils/view_utils", "js/views/pages/course_outline", + "js/models/xblock_outline_info", "js/utils/date_utils", "js/spec_helpers/edit_helpers", + "js/common_helpers/template_helpers"], + function($, AjaxHelpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils, EditHelpers, TemplateHelpers) { describe("CourseOutlinePage", function() { var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, - getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, - createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable, - mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON, + getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, + collapseItemsAndVerifyState, createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, + verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, + createMockVerticalJSON, mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'), mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore'); @@ -114,7 +116,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; createCourseOutlinePage = function(test, courseJSON, createOnly) { - requests = create_sinon.requests(test); + requests = AjaxHelpers.requests(test); model = new XBlockOutlineInfo(courseJSON, { parse: true }); outlinePage = new CourseOutlinePage({ model: model, @@ -148,12 +150,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createCourseOutlinePageAndShowUnit(this, mockCourseJSON); getItemHeaders(type).find('.publish-button').click(); $(".wrapper-modal-window .action-publish").click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-' + type, { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-' + type, { publish : 'make_public' }); expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH'); - create_sinon.respondWithJson(requests, {}); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); + AjaxHelpers.respondWithJson(requests, {}); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); }); it('should show publish button if it is not published and not changed', function() { @@ -191,9 +193,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; beforeEach(function () { - view_helpers.installMockAnalytics(); - view_helpers.installViewTemplates(); - view_helpers.installTemplates([ + EditHelpers.installMockAnalytics(); + EditHelpers.installViewTemplates(); + TemplateHelpers.installTemplates([ 'course-outline', 'xblock-string-field-editor', 'modal-button', 'basic-modal', 'course-outline-modal', 'release-date-editor', 'due-date-editor', 'grading-editor', 'publish-editor', @@ -214,8 +216,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); afterEach(function () { - view_helpers.removeMockAnalytics(); - edit_helpers.cancelModalIfShowing(); + EditHelpers.removeMockAnalytics(); + EditHelpers.cancelModalIfShowing(); // Clean up after the $.datepicker $("#start_date").datepicker( "destroy" ); $("#due_date").datepicker( "destroy" ); @@ -250,8 +252,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createCourseOutlinePage(this, mockEmptyCourseJSON); expect($('.wrapper-alert-announcement')).not.toHaveClass('is-hidden'); $('.dismiss-button').click(); - create_sinon.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url'); - create_sinon.respondToDelete(requests); + AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url'); + AjaxHelpers.respondToDelete(requests); expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden'); }); }); @@ -260,17 +262,17 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can add a section', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); outlinePage.$('.nav-actions .button-new').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', 'parent_locator': 'mock-course' }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { "locator": 'mock-section', "courseKey": 'slashes:MockCourse' }); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); - create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); + AjaxHelpers.respondWithJson(requests, mockSingleSectionCourseJSON); expect(outlinePage.$('.no-content')).not.toExist(); expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section'); }); @@ -279,18 +281,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var sectionElements; createCourseOutlinePage(this, mockSingleSectionCourseJSON); outlinePage.$('.nav-actions .button-new').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', 'parent_locator': 'mock-course' }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { "locator": 'mock-section-2', "courseKey": 'slashes:MockCourse' }); // Expect the UI to just fetch the new section and repaint it - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2'); - create_sinon.respondWithJson(requests, + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2'); + AjaxHelpers.respondWithJson(requests, createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})); sectionElements = getItemsOfType('section'); expect(sectionElements.length).toBe(2); @@ -318,17 +320,17 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can add a section', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); $('.no-content .button-new').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', 'parent_locator': 'mock-course' }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { "locator": "mock-section", "courseKey": "slashes:MockCourse" }); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); - create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); + AjaxHelpers.respondWithJson(requests, mockSingleSectionCourseJSON); expect(outlinePage.$('.no-content')).not.toExist(); expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section'); }); @@ -337,13 +339,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var requestCount; createCourseOutlinePage(this, mockEmptyCourseJSON); $('.no-content .button-new').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', 'parent_locator': 'mock-course' }); requestCount = requests.length; - create_sinon.respondWithError(requests); + AjaxHelpers.respondWithError(requests); expect(requests.length).toBe(requestCount); // No additional requests should be made expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); expect(outlinePage.$('.no-content .button-new')).toExist(); @@ -358,43 +360,43 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; it('can be deleted', function() { - var promptSpy = view_helpers.createPromptSpy(), requestCount; + var promptSpy = EditHelpers.createPromptSpy(), requestCount; createCourseOutlinePage(this, createMockCourseJSON({}, [ createMockSectionJSON(), createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'}) ])); getItemHeaders('section').find('.delete-button').first().click(); - view_helpers.confirmPrompt(promptSpy); + EditHelpers.confirmPrompt(promptSpy); requestCount = requests.length; - create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); - create_sinon.respondWithJson(requests, {}); + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); + AjaxHelpers.respondWithJson(requests, {}); expect(requests.length).toBe(requestCount); // No fetch should be performed expect(outlinePage.$('[data-locator="mock-section"]')).not.toExist(); expect(outlinePage.$('[data-locator="mock-section-2"]')).toExist(); }); it('can be deleted if it is the only section', function() { - var promptSpy = view_helpers.createPromptSpy(); + var promptSpy = EditHelpers.createPromptSpy(); createCourseOutlinePage(this, mockSingleSectionCourseJSON); getItemHeaders('section').find('.delete-button').click(); - view_helpers.confirmPrompt(promptSpy); - create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); - create_sinon.respondWithJson(requests, {}); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); - create_sinon.respondWithJson(requests, mockEmptyCourseJSON); + EditHelpers.confirmPrompt(promptSpy); + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); + AjaxHelpers.respondWithJson(requests, {}); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); + AjaxHelpers.respondWithJson(requests, mockEmptyCourseJSON); expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); expect(outlinePage.$('.no-content .button-new')).toExist(); }); it('remains visible if its deletion fails', function() { - var promptSpy = view_helpers.createPromptSpy(), + var promptSpy = EditHelpers.createPromptSpy(), requestCount; createCourseOutlinePage(this, mockSingleSectionCourseJSON); getItemHeaders('section').find('.delete-button').click(); - view_helpers.confirmPrompt(promptSpy); - create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); + EditHelpers.confirmPrompt(promptSpy); + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); requestCount = requests.length; - create_sinon.respondWithError(requests); + AjaxHelpers.respondWithError(requests); expect(requests.length).toBe(requestCount); // No additional requests should be made expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section'); }); @@ -402,18 +404,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can add a subsection', function() { createCourseOutlinePage(this, mockCourseJSON); getItemsOfType('section').find('> .outline-content > .add-subsection .button-new').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'sequential', 'display_name': 'Subsection', 'parent_locator': 'mock-section' }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { "locator": "new-mock-subsection", "courseKey": "slashes:MockCourse" }); // Note: verification of the server response and the UI's handling of it // is handled in the acceptance tests. - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); }); @@ -423,13 +425,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" sectionModel; createCourseOutlinePage(this, mockCourseJSON); displayNameWrapper = getDisplayNameWrapper(); - displayNameInput = view_helpers.inlineEdit(displayNameWrapper, updatedDisplayName); + displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName); displayNameInput.change(); // This is the response for the change operation. - create_sinon.respondWithJson(requests, { }); + AjaxHelpers.respondWithJson(requests, { }); // This is the response for the subsequent fetch operation. - create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName}); - view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); + AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName}); + EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); sectionModel = outlinePage.model.get('child_info').children[0]; expect(sectionModel.get('display_name')).toBe(updatedDisplayName); }); @@ -455,7 +457,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" // Staff lock controls are always visible expect($("#staff_lock")).toExist(); $(".wrapper-modal-window .action-save").click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', { "metadata":{ "start":"2015-01-02T00:00:00.000Z" } @@ -463,7 +465,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH'); // This is the response for the change operation. - create_sinon.respondWithJson(requests, {}); + AjaxHelpers.respondWithJson(requests, {}); var mockResponseSectionJSON = createMockSectionJSON({ release_date: 'Jan 02, 2015 at 00:00 UTC' }, [ @@ -474,10 +476,10 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }) ]) ]); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section') + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); expect(requests.length).toBe(2); // This is the response for the subsequent fetch operation for the section. - create_sinon.respondWithJson(requests, mockResponseSectionJSON); + AjaxHelpers.respondWithJson(requests, mockResponseSectionJSON); expect($(".outline-section .status-release-value")).toContainText("Jan 02, 2015 at 00:00 UTC"); }); @@ -507,7 +509,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ]), createMockSectionJSON({has_changes: true}, [ createMockSubsectionJSON({has_changes: true}, [ - createMockVerticalJSON({has_changes: true}), + createMockVerticalJSON({has_changes: true}) ]) ]) ]), modalWindow; @@ -518,7 +520,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" expect(modalWindow.find('.outline-unit').length).toBe(3); expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual( ['Unit 100', 'Unit 50', 'Unit 1'] - ) + ); expect(modalWindow.find('.outline-subsection').length).toBe(2); }); }); @@ -538,7 +540,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; // Contains hard-coded dates because dates are presented in different formats. - var mockServerValuesJson = createMockSectionJSON({ + mockServerValuesJson = createMockSectionJSON({ release_date: 'Jan 01, 2970 at 05:00 UTC' }, [ createMockSubsectionJSON({ @@ -559,15 +561,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ]); it('can be deleted', function() { - var promptSpy = view_helpers.createPromptSpy(); + var promptSpy = EditHelpers.createPromptSpy(); createCourseOutlinePage(this, mockCourseJSON); getItemHeaders('subsection').find('.delete-button').click(); - view_helpers.confirmPrompt(promptSpy); - create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection'); - create_sinon.respondWithJson(requests, {}); + EditHelpers.confirmPrompt(promptSpy); + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection'); + AjaxHelpers.respondWithJson(requests, {}); // Note: verification of the server response and the UI's handling of it // is handled in the acceptance tests. - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); }); it('can add a unit', function() { @@ -575,12 +577,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createCourseOutlinePage(this, mockCourseJSON); redirectSpy = spyOn(ViewUtils, 'redirect'); getItemsOfType('subsection').find('> .outline-content > .add-unit .button-new').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'vertical', 'display_name': 'Unit', 'parent_locator': 'mock-subsection' }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { "locator": "new-mock-unit", "courseKey": "slashes:MockCourse" }); @@ -593,12 +595,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" subsectionModel; createCourseOutlinePage(this, mockCourseJSON); displayNameWrapper = getDisplayNameWrapper(); - displayNameInput = view_helpers.inlineEdit(displayNameWrapper, updatedDisplayName); + displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName); displayNameInput.change(); // This is the response for the change operation. - create_sinon.respondWithJson(requests, { }); + AjaxHelpers.respondWithJson(requests, { }); // This is the response for the subsequent fetch operation for the section. - create_sinon.respondWithJson(requests, + AjaxHelpers.respondWithJson(requests, createMockSectionJSON({}, [ createMockSubsectionJSON({ display_name: updatedDisplayName @@ -607,7 +609,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ); // Find the display name again in the refreshed DOM and verify it displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field'); - view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); + EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); subsectionModel = outlinePage.model.get('child_info').children[0].get('child_info').children[0]; expect(subsectionModel.get('display_name')).toBe(updatedDisplayName); }); @@ -625,7 +627,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" outlinePage.$('.outline-subsection .configure-button').click(); setEditModalValues("7/9/2014", "7/10/2014", "Lab", true); $(".wrapper-modal-window .action-save").click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', { "graderType":"Lab", "publish": "republish", "metadata":{ @@ -637,16 +639,24 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH'); // This is the response for the change operation. - create_sinon.respondWithJson(requests, {}); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section') + AjaxHelpers.respondWithJson(requests, {}); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); expect(requests.length).toBe(2); // This is the response for the subsequent fetch operation for the section. - create_sinon.respondWithJson(requests, mockServerValuesJson); + AjaxHelpers.respondWithJson(requests, mockServerValuesJson); - expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC"); - expect($(".outline-subsection .status-grading-date")).toContainText("Due: Jul 10, 2014 at 00:00 UTC"); - expect($(".outline-subsection .status-grading-value")).toContainText("Lab"); - expect($(".outline-subsection .status-message-copy")).toContainText("Contains staff only content"); + expect($(".outline-subsection .status-release-value")).toContainText( + "Jul 09, 2014 at 00:00 UTC" + ); + expect($(".outline-subsection .status-grading-date")).toContainText( + "Due: Jul 10, 2014 at 00:00 UTC" + ); + expect($(".outline-subsection .status-grading-value")).toContainText( + "Lab" + ); + expect($(".outline-subsection .status-message-copy")).toContainText( + "Contains staff only content" + ); expect($(".outline-item .outline-subsection .status-grading-value")).toContainText("Lab"); outlinePage.$('.outline-item .outline-subsection .configure-button').click(); @@ -663,14 +673,22 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" $(".wrapper-modal-window .action-save").click(); // This is the response for the change operation. - create_sinon.respondWithJson(requests, {}); + AjaxHelpers.respondWithJson(requests, {}); // This is the response for the subsequent fetch operation. - create_sinon.respondWithJson(requests, mockServerValuesJson); + AjaxHelpers.respondWithJson(requests, mockServerValuesJson); - expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC"); - expect($(".outline-subsection .status-grading-date")).toContainText("Due: Jul 10, 2014 at 00:00 UTC"); - expect($(".outline-subsection .status-grading-value")).toContainText("Lab"); - expect($(".outline-subsection .status-message-copy")).toContainText("Contains staff only content"); + expect($(".outline-subsection .status-release-value")).toContainText( + "Jul 09, 2014 at 00:00 UTC" + ); + expect($(".outline-subsection .status-grading-date")).toContainText( + "Due: Jul 10, 2014 at 00:00 UTC" + ); + expect($(".outline-subsection .status-grading-value")).toContainText( + "Lab" + ); + expect($(".outline-subsection .status-message-copy")).toContainText( + "Contains staff only content" + ); outlinePage.$('.outline-subsection .configure-button').click(); expect($("#start_date").val()).toBe('7/9/2014'); @@ -689,15 +707,19 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" $(".wrapper-modal-window .action-save").click(); // This is the response for the change operation. - create_sinon.respondWithJson(requests, {}); + AjaxHelpers.respondWithJson(requests, {}); // This is the response for the subsequent fetch operation. - create_sinon.respondWithJson(requests, + AjaxHelpers.respondWithJson(requests, createMockSectionJSON({}, [createMockSubsectionJSON()]) ); - expect($(".outline-subsection .status-release-value")).not.toContainText("Jul 09, 2014 at 00:00 UTC"); + expect($(".outline-subsection .status-release-value")).not.toContainText( + "Jul 09, 2014 at 00:00 UTC" + ); expect($(".outline-subsection .status-grading-date")).not.toExist(); expect($(".outline-subsection .status-grading-value")).not.toExist(); - expect($(".outline-subsection .status-message-copy")).not.toContainText("Contains staff only content"); + expect($(".outline-subsection .status-message-copy")).not.toContainText( + "Contains staff only content" + ); }); verifyTypePublishable('subsection', function (options) { @@ -731,7 +753,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" expect(modalWindow.find('.outline-unit').length).toBe(2); expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual( ['Unit 100', 'Unit 50'] - ) + ); expect(modalWindow.find('.outline-subsection')).not.toExist(); }); }); @@ -739,16 +761,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" // Note: most tests for units can be found in Bok Choy describe("Unit", function() { it('can be deleted', function() { - var promptSpy = view_helpers.createPromptSpy(); + var promptSpy = EditHelpers.createPromptSpy(); createCourseOutlinePage(this, mockCourseJSON); expandItemsAndVerifyState('subsection'); getItemHeaders('unit').find('.delete-button').click(); - view_helpers.confirmPrompt(promptSpy); - create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-unit'); - create_sinon.respondWithJson(requests, {}); + EditHelpers.confirmPrompt(promptSpy); + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-unit'); + AjaxHelpers.respondWithJson(requests, {}); // Note: verification of the server response and the UI's handling of it // is handled in the acceptance tests. - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section'); }); it('has a link to the unit page', function() { diff --git a/cms/static/js/spec/views/pages/course_rerun_spec.js b/cms/static/js/spec/views/pages/course_rerun_spec.js index 5a9eb0b87ba2feed367fc2234410e761b77c7caf..bcf881b98685de8fe407181de42aad997b62cfd1 100644 --- a/cms/static/js/spec/views/pages/course_rerun_spec.js +++ b/cms/static/js/spec/views/pages/course_rerun_spec.js @@ -1,6 +1,6 @@ -define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/views/course_rerun", +define(["jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helpers", "js/views/course_rerun", "js/views/utils/create_course_utils", "js/views/utils/view_utils", "jquery.simulate"], - function ($, create_sinon, view_helpers, CourseRerunUtils, CreateCourseUtilsFactory, ViewUtils) { + function ($, AjaxHelpers, ViewHelpers, CourseRerunUtils, CreateCourseUtilsFactory, ViewUtils) { describe("Create course rerun page", function () { var selectors = { org: '.rerun-course-org', @@ -36,14 +36,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; beforeEach(function () { - view_helpers.installMockAnalytics(); + ViewHelpers.installMockAnalytics(); window.source_course_key = 'test_course_key'; appendSetFixtures(mockCreateCourseRerunHTML); CourseRerunUtils.onReady(); }); afterEach(function () { - view_helpers.removeMockAnalytics(); + ViewHelpers.removeMockAnalytics(); delete window.source_course_key; }); @@ -156,11 +156,11 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); it("saves course reruns", function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); var redirectSpy = spyOn(ViewUtils, 'redirect') fillInFields('DemoX', 'DM101', '2014', 'Demo course'); $(selectors.save).click(); - create_sinon.expectJsonRequest(requests, 'POST', '/course/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/course/', { source_course_key: 'test_course_key', org: 'DemoX', number: 'DM101', @@ -170,17 +170,17 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" expect($(selectors.save)).toHaveClass(classes.disabled); expect($(selectors.save)).toHaveClass(classes.processing); expect($(selectors.cancel)).toHaveClass(classes.hidden); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { url: 'dummy_test_url' }); expect(redirectSpy).toHaveBeenCalledWith('dummy_test_url'); }); it("displays an error when saving fails", function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); fillInFields('DemoX', 'DM101', '2014', 'Demo course'); $(selectors.save).click(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { ErrMsg: 'error message' }); expect($(selectors.errorWrapper)).not.toHaveClass(classes.hidden); @@ -190,7 +190,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); it("does not save if there are validation errors", function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); fillInFields('DemoX', 'DM101', '', 'Demo course'); $(selectors.save).click(); expect(requests.length).toBe(0); diff --git a/cms/static/js/spec/views/pages/group_configurations_spec.js b/cms/static/js/spec/views/pages/group_configurations_spec.js index 015e89edff94e891a37b5501a2bfe889627a2c10..f5eec76c6e1caf3fc22ad6f081ebc9f4ecce5421 100644 --- a/cms/static/js/spec/views/pages/group_configurations_spec.js +++ b/cms/static/js/spec/views/pages/group_configurations_spec.js @@ -1,7 +1,7 @@ define([ 'jquery', 'underscore', 'js/views/pages/group_configurations', - 'js/collections/group_configuration', 'js/models/group_configuration', 'js/spec_helpers/edit_helpers' -], function ($, _, GroupConfigurationsPage, GroupConfigurationCollection, GroupConfigurationModel, view_helpers) { + 'js/collections/group_configuration', 'js/common_helpers/template_helpers' +], function ($, _, GroupConfigurationsPage, GroupConfigurationCollection, TemplateHelpers) { 'use strict'; describe('GroupConfigurationsPage', function() { var mockGroupConfigurationsPage = readFixtures( @@ -35,7 +35,7 @@ define([ beforeEach(function () { setFixtures(mockGroupConfigurationsPage); - view_helpers.installTemplates([ + TemplateHelpers.installTemplates([ 'no-group-configurations', 'group-configuration-edit', 'group-configuration-details' ]); @@ -83,7 +83,7 @@ define([ describe('Check that Group Configuration will focus and expand depending on content of url hash', function() { beforeEach(function () { spyOn($.fn, 'focus'); - view_helpers.installTemplate('group-configuration-details'); + TemplateHelpers.installTemplate('group-configuration-details'); this.view = initializePage(true); }); diff --git a/cms/static/js/spec/views/pages/index_spec.js b/cms/static/js/spec/views/pages/index_spec.js index fab5b9653e1acb4156f7c089d169f518523e76e2..2a7974cd591d979e4f6ad44660dada86a8601ee2 100644 --- a/cms/static/js/spec/views/pages/index_spec.js +++ b/cms/static/js/spec/views/pages/index_spec.js @@ -1,6 +1,6 @@ -define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/index", +define(["jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helpers", "js/index", "js/views/utils/view_utils"], - function ($, create_sinon, view_helpers, IndexUtils, ViewUtils) { + function ($, AjaxHelpers, ViewHelpers, IndexUtils, ViewUtils) { describe("Course listing page", function () { var mockIndexPageHTML = readFixtures('mock/mock-index-page.underscore'), fillInFields; @@ -12,49 +12,49 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; beforeEach(function () { - view_helpers.installMockAnalytics(); + ViewHelpers.installMockAnalytics(); appendSetFixtures(mockIndexPageHTML); IndexUtils.onReady(); }); afterEach(function () { - view_helpers.removeMockAnalytics(); + ViewHelpers.removeMockAnalytics(); delete window.source_course_key; }); it("can dismiss notifications", function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); var reloadSpy = spyOn(ViewUtils, 'reload'); $('.dismiss-button').click(); - create_sinon.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url'); - create_sinon.respondToDelete(requests); + AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url'); + AjaxHelpers.respondToDelete(requests); expect(reloadSpy).toHaveBeenCalled(); }); it("saves new courses", function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); var redirectSpy = spyOn(ViewUtils, 'redirect'); $('.new-course-button').click() fillInFields('DemoX', 'DM101', '2014', 'Demo course'); $('.new-course-save').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/course/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/course/', { org: 'DemoX', number: 'DM101', run: '2014', display_name: 'Demo course' }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { url: 'dummy_test_url' }); expect(redirectSpy).toHaveBeenCalledWith('dummy_test_url'); }); it("displays an error when saving fails", function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); $('.new-course-button').click(); fillInFields('DemoX', 'DM101', '2014', 'Demo course'); $('.new-course-save').click(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { ErrMsg: 'error message' }); expect($('.wrap-error')).toHaveClass('is-shown'); diff --git a/cms/static/js/spec/views/paging_spec.js b/cms/static/js/spec/views/paging_spec.js index 5deac8e999441b4a8f4e914c61f5638c2b80d126..444e87a2add25c7a2f50921cd965b304596d8081 100644 --- a/cms/static/js/spec/views/paging_spec.js +++ b/cms/static/js/spec/views/paging_spec.js @@ -1,7 +1,7 @@ -define([ "jquery", "js/spec_helpers/create_sinon", "URI", +define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/paging", "js/views/paging_header", "js/views/paging_footer", "js/models/asset", "js/collections/asset" ], - function ($, create_sinon, URI, PagingView, PagingHeader, PagingFooter, AssetModel, AssetCollection) { + function ($, AjaxHelpers, URI, PagingView, PagingHeader, PagingFooter, AssetModel, AssetCollection) { var createMockAsset = function(index) { var id = 'asset_' + index; @@ -50,7 +50,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value var page = queryParameters.page; var response = page === "0" ? mockFirstPage : mockSecondPage; - create_sinon.respondWithJson(requests, response, requestIndex); + AjaxHelpers.respondWithJson(requests, response, requestIndex); }; var MockPagingView = PagingView.extend({ @@ -77,7 +77,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", describe("PagingView", function () { describe("setPage", function () { it('can set the current page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingView.collection.currentPage).toBe(0); @@ -87,7 +87,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('should not change page after a server error', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingView.setPage(1); @@ -98,7 +98,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", describe("nextPage", function () { it('does not move forward after a server error', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingView.nextPage(); @@ -107,7 +107,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('can move to the next page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingView.nextPage(); @@ -116,7 +116,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('can not move forward from the final page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); pagingView.nextPage(); @@ -127,7 +127,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", describe("previousPage", function () { it('can move back a page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); pagingView.previousPage(); @@ -136,7 +136,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('can not move back from the first page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingView.previousPage(); @@ -144,7 +144,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('does not move back after a server error', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); pagingView.previousPage(); @@ -156,7 +156,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", describe("toggleSortOrder", function () { it('can toggle direction of the current sort', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); expect(pagingView.collection.sortDirection).toBe('desc'); pagingView.toggleSortOrder('date-col'); respondWithMockAssets(requests); @@ -167,7 +167,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('sets the correct default sort direction for a column', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.toggleSortOrder('name-col'); respondWithMockAssets(requests); expect(pagingView.sortDisplayName()).toBe('Name'); @@ -214,7 +214,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('does not move forward if a server error occurs', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingHeader.$('.next-page-link').click(); @@ -223,7 +223,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('can move to the next page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingHeader.$('.next-page-link').click(); @@ -232,23 +232,23 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('should be enabled when there is at least one more page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingHeader.$('.next-page-link')).not.toHaveClass('is-disabled'); }); it('should be disabled on the final page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); expect(pagingHeader.$('.next-page-link')).toHaveClass('is-disabled'); }); it('should be disabled on an empty page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingHeader.$('.next-page-link')).toHaveClass('is-disabled'); }); }); @@ -261,7 +261,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('does not move back if a server error occurs', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); pagingHeader.$('.previous-page-link').click(); @@ -270,7 +270,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('can go back a page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); pagingHeader.$('.previous-page-link').click(); @@ -279,30 +279,30 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('should be disabled on the first page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled'); }); it('should be enabled on the second page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); expect(pagingHeader.$('.previous-page-link')).not.toHaveClass('is-disabled'); }); it('should be disabled for an empty page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled'); }); }); describe("Page metadata section", function() { it('shows the correct metadata for the current page', function () { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), message; pagingView.setPage(0); respondWithMockAssets(requests); @@ -313,7 +313,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('shows the correct metadata when sorted ascending', function () { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), message; pagingView.setPage(0); pagingView.toggleSortOrder('name-col'); @@ -327,60 +327,60 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", describe("Asset count label", function () { it('should show correct count on first page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingHeader.$('.count-current-shown')).toHaveHtml('1-3'); }); it('should show correct count on second page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); expect(pagingHeader.$('.count-current-shown')).toHaveHtml('4-4'); }); it('should show correct count for an empty collection', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingHeader.$('.count-current-shown')).toHaveHtml('0-0'); }); }); describe("Asset total label", function () { it('should show correct total on the first page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingHeader.$('.count-total')).toHaveText('4 total'); }); it('should show correct total on the second page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); expect(pagingHeader.$('.count-total')).toHaveText('4 total'); }); it('should show zero total for an empty collection', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingHeader.$('.count-total')).toHaveText('0 total'); }); }); describe("Sort order label", function () { it('should show correct initial sort order', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingHeader.$('.sort-order')).toHaveText('Date'); }); it('should show updated sort order', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.toggleSortOrder('name-col'); respondWithMockAssets(requests); expect(pagingHeader.$('.sort-order')).toHaveText('Name'); @@ -405,7 +405,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('does not move forward if a server error occurs', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingFooter.$('.next-page-link').click(); @@ -414,7 +414,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('can move to the next page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingFooter.$('.next-page-link').click(); @@ -423,23 +423,23 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('should be enabled when there is at least one more page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingFooter.$('.next-page-link')).not.toHaveClass('is-disabled'); }); it('should be disabled on the final page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); expect(pagingFooter.$('.next-page-link')).toHaveClass('is-disabled'); }); it('should be disabled on an empty page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingFooter.$('.next-page-link')).toHaveClass('is-disabled'); }); }); @@ -452,7 +452,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('does not move back if a server error occurs', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); pagingFooter.$('.previous-page-link').click(); @@ -461,7 +461,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('can go back a page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); pagingFooter.$('.previous-page-link').click(); @@ -470,62 +470,62 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('should be disabled on the first page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled'); }); it('should be enabled on the second page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); expect(pagingFooter.$('.previous-page-link')).not.toHaveClass('is-disabled'); }); it('should be disabled for an empty page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled'); }); }); describe("Current page label", function () { it('should show 1 on the first page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingFooter.$('.current-page')).toHaveText('1'); }); it('should show 2 on the second page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(1); respondWithMockAssets(requests); expect(pagingFooter.$('.current-page')).toHaveText('2'); }); it('should show 1 for an empty collection', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingFooter.$('.current-page')).toHaveText('1'); }); }); describe("Page total label", function () { it('should show the correct value with more than one page', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingFooter.$('.total-pages')).toHaveText('2'); }); it('should show page 1 when there are no assets', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); - create_sinon.respondWithJson(requests, mockEmptyPage); + AjaxHelpers.respondWithJson(requests, mockEmptyPage); expect(pagingFooter.$('.total-pages')).toHaveText('1'); }); }); @@ -538,14 +538,14 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('should initially have a blank page input', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); expect(pagingFooter.$('.page-number-input')).toHaveValue(''); }); it('should handle invalid page requests', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingFooter.$('.page-number-input').val('abc'); @@ -555,18 +555,18 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", }); it('should switch pages via the input field', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingFooter.$('.page-number-input').val('2'); pagingFooter.$('.page-number-input').trigger('change'); - create_sinon.respondWithJson(requests, mockSecondPage); + AjaxHelpers.respondWithJson(requests, mockSecondPage); expect(pagingView.collection.currentPage).toBe(1); expect(pagingFooter.$('.page-number-input')).toHaveValue(''); }); it('should handle AJAX failures when switching pages via the input field', function () { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); pagingView.setPage(0); respondWithMockAssets(requests); pagingFooter.$('.page-number-input').val('2'); diff --git a/cms/static/js/spec/views/unit_outline_spec.js b/cms/static/js/spec/views/unit_outline_spec.js index 117964751aa0ae569906014885e1be8aaf40a9bd..085cb36202812d88c8cde7e212293f16971e3964 100644 --- a/cms/static/js/spec/views/unit_outline_spec.js +++ b/cms/static/js/spec/views/unit_outline_spec.js @@ -1,13 +1,13 @@ -define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/views/utils/view_utils", - "js/views/unit_outline", "js/models/xblock_info"], - function ($, create_sinon, view_helpers, ViewUtils, UnitOutlineView, XBlockInfo) { +define(["jquery", "js/common_helpers/ajax_helpers", "js/common_helpers/template_helpers", + "js/spec_helpers/view_helpers", "js/views/utils/view_utils", "js/views/unit_outline", "js/models/xblock_info"], + function ($, AjaxHelpers, TemplateHelpers, ViewHelpers, ViewUtils, UnitOutlineView, XBlockInfo) { describe("UnitOutlineView", function() { var createUnitOutlineView, createMockXBlockInfo, requests, model, unitOutlineView; createUnitOutlineView = function(test, unitJSON, createOnly) { - requests = create_sinon.requests(test); + requests = AjaxHelpers.requests(test); model = new XBlockInfo(unitJSON, { parse: true }); unitOutlineView = new UnitOutlineView({ model: model, @@ -71,14 +71,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; beforeEach(function () { - view_helpers.installMockAnalytics(); - view_helpers.installViewTemplates(); - view_helpers.installTemplate('unit-outline'); + ViewHelpers.installMockAnalytics(); + ViewHelpers.installViewTemplates(); + TemplateHelpers.installTemplate('unit-outline'); appendSetFixtures('<div class="wrapper-unit-overview"></div>'); }); afterEach(function () { - view_helpers.removeMockAnalytics(); + ViewHelpers.removeMockAnalytics(); }); it('can render itself', function() { @@ -93,12 +93,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); redirectSpy = spyOn(ViewUtils, 'redirect'); unitOutlineView.$('.outline-subsection > .outline-content > .add-unit .button-new').click(); - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { category: 'vertical', display_name: 'Unit', parent_locator: 'mock-subsection' }); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { locator: "new-mock-unit", courseKey: "slashes:MockCourse" }); @@ -106,11 +106,11 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); it('refreshes when the XBlockInfo model syncs', function() { - var updatedDisplayName = 'Mock Unit Updated', unitHeader; + var updatedDisplayName = 'Mock Unit Updated'; createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); unitOutlineView.refresh(); - create_sinon.expectJsonRequest(requests, 'GET', '/xblock/mock-unit'); - create_sinon.respondWithJson(requests, + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/mock-unit'); + AjaxHelpers.respondWithJson(requests, createMockXBlockInfo(updatedDisplayName)); expect(unitOutlineView.$('.outline-unit .unit-title').first().text().trim()).toBe(updatedDisplayName); }); diff --git a/cms/static/js/spec/views/utils/view_utils_spec.js b/cms/static/js/spec/views/utils/view_utils_spec.js index dade50ad4ae4b7c0c39820d1c02827cf2f63c928..114b3b01d42c5a692a178391825043b7bedfd17b 100644 --- a/cms/static/js/spec/views/utils/view_utils_spec.js +++ b/cms/static/js/spec/views/utils/view_utils_spec.js @@ -1,5 +1,5 @@ define(["jquery", "underscore", "js/views/baseview", "js/views/utils/view_utils", "js/spec_helpers/edit_helpers"], - function ($, _, BaseView, ViewUtils, view_helpers) { + function ($, _, BaseView, ViewUtils, ViewHelpers) { describe("ViewUtils", function() { describe("disabled element while running", function() { @@ -22,22 +22,22 @@ define(["jquery", "underscore", "js/views/baseview", "js/views/utils/view_utils" var testMessage = "Testing...", deferred = new $.Deferred(), promise = deferred.promise(), - notificationSpy = view_helpers.createNotificationSpy(); + notificationSpy = ViewHelpers.createNotificationSpy(); ViewUtils.runOperationShowingMessage(testMessage, function() { return promise; }); - view_helpers.verifyNotificationShowing(notificationSpy, /Testing/); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Testing/); deferred.resolve(); - view_helpers.verifyNotificationHidden(notificationSpy); + ViewHelpers.verifyNotificationHidden(notificationSpy); }); it("shows progress notification and leaves it showing upon failure", function() { var testMessage = "Testing...", deferred = new $.Deferred(), promise = deferred.promise(), - notificationSpy = view_helpers.createNotificationSpy(); + notificationSpy = ViewHelpers.createNotificationSpy(); ViewUtils.runOperationShowingMessage(testMessage, function() { return promise; }); - view_helpers.verifyNotificationShowing(notificationSpy, /Testing/); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Testing/); deferred.fail(); - view_helpers.verifyNotificationShowing(notificationSpy, /Testing/); + ViewHelpers.verifyNotificationShowing(notificationSpy, /Testing/); }); }); }); diff --git a/cms/static/js/spec/views/xblock_editor_spec.js b/cms/static/js/spec/views/xblock_editor_spec.js index 8b771e2fb4cf66da03ad8507f0d859e4dd2fd724..59116c603bbec69cadf7b10addb1660b5f4f8ff2 100644 --- a/cms/static/js/spec/views/xblock_editor_spec.js +++ b/cms/static/js/spec/views/xblock_editor_spec.js @@ -1,6 +1,6 @@ -define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", +define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "js/spec_helpers/edit_helpers", "js/views/xblock_editor", "js/models/xblock_info"], - function ($, _, create_sinon, edit_helpers, XBlockEditorView, XBlockInfo) { + function ($, _, AjaxHelpers, EditHelpers, XBlockEditorView, XBlockInfo) { describe("XBlockEditorView", function() { var model, editor, testDisplayName, mockSaveResponse; @@ -14,7 +14,7 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper }; beforeEach(function () { - edit_helpers.installEditTemplates(); + EditHelpers.installEditTemplates(); model = new XBlockInfo({ id: 'testCourse/branch/draft/block/verticalFFF', display_name: 'Test Unit', @@ -29,19 +29,19 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper var mockXBlockEditorHtml; beforeEach(function () { - edit_helpers.installMockXBlock(); + EditHelpers.installMockXBlock(); }); afterEach(function() { - edit_helpers.uninstallMockXBlock(); + EditHelpers.uninstallMockXBlock(); }); mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); it('can render itself', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); editor.render(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockXBlockEditorHtml, resources: [] }); @@ -57,17 +57,17 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore'); beforeEach(function() { - edit_helpers.installMockXModule(mockSaveResponse); + EditHelpers.installMockXModule(mockSaveResponse); }); afterEach(function () { - edit_helpers.uninstallMockXModule(); + EditHelpers.uninstallMockXModule(); }); it('can render itself', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); editor.render(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockXModuleEditorHtml, resources: [] }); @@ -77,9 +77,9 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper }); it('saves any custom metadata', function() { - var requests = create_sinon.requests(this), request, response; + var requests = AjaxHelpers.requests(this), request, response; editor.render(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockXModuleEditorHtml, resources: [] }); @@ -93,11 +93,11 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper }); it('can render a module with only settings', function() { - var requests = create_sinon.requests(this), mockXModuleEditorHtml; + var requests = AjaxHelpers.requests(this), mockXModuleEditorHtml; mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore'); editor.render(); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockXModuleEditorHtml, resources: [] }); diff --git a/cms/static/js/spec/views/xblock_spec.js b/cms/static/js/spec/views/xblock_spec.js index 2da1e7866a3463ce23665df792bb929f696462ce..64d8e31392d00d11dd3bfde7e2378211704e703a 100644 --- a/cms/static/js/spec/views/xblock_spec.js +++ b/cms/static/js/spec/views/xblock_spec.js @@ -1,6 +1,6 @@ -define([ "jquery", "js/spec_helpers/create_sinon", "URI", "js/views/xblock", "js/models/xblock_info", +define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/xblock", "js/models/xblock_info", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], - function ($, create_sinon, URI, XBlockView, XBlockInfo) { + function ($, AjaxHelpers, URI, XBlockView, XBlockInfo) { describe("XBlockView", function() { var model, xblockView, mockXBlockHtml, respondWithMockXBlockFragment; @@ -20,11 +20,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", "js/views/xblock", "js respondWithMockXBlockFragment = function(requests, response) { var requestIndex = requests.length - 1; - create_sinon.respondWithJson(requests, response, requestIndex); + AjaxHelpers.respondWithJson(requests, response, requestIndex); }; it('can render a nested xblock', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); xblockView.render(); respondWithMockXBlockFragment(requests, { html: mockXBlockHtml, @@ -57,12 +57,12 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", "js/views/xblock", "js }; it('can render an xblock with no CSS or JavaScript', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); postXBlockRequest(requests, []); }); it('can render an xblock with required CSS', function() { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), mockCssText = "// Just a comment", mockCssUrl = "mock.css", headHtml; @@ -76,7 +76,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", "js/views/xblock", "js }); it('can render an xblock with required JavaScript', function() { - var requests = create_sinon.requests(this); + var requests = AjaxHelpers.requests(this); postXBlockRequest(requests, [ ["hash3", { mimetype: "application/javascript", kind: "text", data: "window.test = 100;" }] ]); @@ -84,7 +84,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", "js/views/xblock", "js }); it('can render an xblock with required HTML', function() { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), mockHeadTag = "<title>Test Title</title>"; postXBlockRequest(requests, [ ["hash4", { mimetype: "text/html", placement: "head", data: mockHeadTag }] @@ -93,7 +93,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI", "js/views/xblock", "js }); it('aborts rendering when a dependent script fails to load', function() { - var requests = create_sinon.requests(this), + var requests = AjaxHelpers.requests(this), mockJavaScriptUrl = "mock.js", promise; spyOn($, 'getScript').andReturn($.Deferred().reject().promise()); diff --git a/cms/static/js/spec/views/xblock_string_field_editor_spec.js b/cms/static/js/spec/views/xblock_string_field_editor_spec.js index 1b7e9c8d0181d00ee2e099db8906a6cca4c0f3fa..d36573ffc9f36f14cf5fb9795358f7d03df7eec9 100644 --- a/cms/static/js/spec/views/xblock_string_field_editor_spec.js +++ b/cms/static/js/spec/views/xblock_string_field_editor_spec.js @@ -1,5 +1,6 @@ -define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/spec_helpers/edit_helpers", "js/models/xblock_info", "js/views/xblock_string_field_editor"], - function ($, create_sinon, view_helpers, edit_helpers, XBlockInfo, XBlockStringFieldEditor) { +define(["jquery", "js/common_helpers/ajax_helpers", "js/common_helpers/template_helpers", + "js/spec_helpers/edit_helpers", "js/models/xblock_info", "js/views/xblock_string_field_editor"], + function ($, AjaxHelpers, TemplateHelpers, EditHelpers, XBlockInfo, XBlockStringFieldEditor) { describe("XBlockStringFieldEditorView", function () { var initialDisplayName, updatedDisplayName, getXBlockInfo, getFieldEditorView; @@ -26,11 +27,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" beforeEach(function () { initialDisplayName = "Default Display Name"; updatedDisplayName = "Updated Display Name"; - view_helpers.installTemplate('xblock-string-field-editor'); + TemplateHelpers.installTemplate('xblock-string-field-editor'); appendSetFixtures( '<div class="wrapper-xblock-field incontext-editor is-editable"' + 'data-field="display_name" data-field-display-name="Display Name">' + - '<h1 class="page-header-title xblock-field-value incontext-editor-value"><span class="title-value">' + initialDisplayName + '</span></h1>' + + '<h1 class="page-header-title xblock-field-value incontext-editor-value">' + + '<span class="title-value">' + initialDisplayName + '</span>' + + '</h1>' + '</div>' ); }); @@ -39,7 +42,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var expectPostedNewDisplayName, expectEditCanceled; expectPostedNewDisplayName = function (requests, displayName) { - create_sinon.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', { + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', { metadata: { display_name: displayName } @@ -48,9 +51,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" expectEditCanceled = function (test, fieldEditorView, options) { var requests, initialRequests, displayNameInput; - requests = create_sinon.requests(test); + requests = AjaxHelpers.requests(test); initialRequests = requests.length; - displayNameInput = edit_helpers.inlineEdit(fieldEditorView.$el, options.newTitle); + displayNameInput = EditHelpers.inlineEdit(fieldEditorView.$el, options.newTitle); if (options.pressEscape) { displayNameInput.simulate("keydown", { keyCode: $.simulate.keyCode.ESCAPE }); displayNameInput.simulate("keyup", { keyCode: $.simulate.keyCode.ESCAPE }); @@ -61,51 +64,51 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" } // No requests should be made when the edit is cancelled client-side expect(initialRequests).toBe(requests.length); - edit_helpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName); + EditHelpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName); expect(fieldEditorView.model.get('display_name')).toBe(initialDisplayName); }; it('can inline edit the display name', function () { var requests, fieldEditorView; - requests = create_sinon.requests(this); + requests = AjaxHelpers.requests(this); fieldEditorView = getFieldEditorView().render(); - edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); + EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); fieldEditorView.$('button[name=submit]').click(); expectPostedNewDisplayName(requests, updatedDisplayName); // This is the response for the change operation. - create_sinon.respondWithJson(requests, { }); + AjaxHelpers.respondWithJson(requests, { }); // This is the response for the subsequent fetch operation. - create_sinon.respondWithJson(requests, {display_name: updatedDisplayName}); - edit_helpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName); + AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); + EditHelpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName); }); it('does not change the title when a display name update fails', function () { var requests, fieldEditorView, initialRequests; - requests = create_sinon.requests(this); + requests = AjaxHelpers.requests(this); initialRequests = requests.length; fieldEditorView = getFieldEditorView().render(); - edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); + EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); fieldEditorView.$('button[name=submit]').click(); expectPostedNewDisplayName(requests, updatedDisplayName); - create_sinon.respondWithError(requests); + AjaxHelpers.respondWithError(requests); // No fetch operation should occur. expect(initialRequests + 1).toBe(requests.length); - edit_helpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName, updatedDisplayName); + EditHelpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName, updatedDisplayName); }); it('trims whitespace from the display name', function () { var requests, fieldEditorView; - requests = create_sinon.requests(this); + requests = AjaxHelpers.requests(this); fieldEditorView = getFieldEditorView().render(); updatedDisplayName += ' '; - edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); + EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); fieldEditorView.$('button[name=submit]').click(); expectPostedNewDisplayName(requests, updatedDisplayName.trim()); // This is the response for the change operation. - create_sinon.respondWithJson(requests, { }); + AjaxHelpers.respondWithJson(requests, { }); // This is the response for the subsequent fetch operation. - create_sinon.respondWithJson(requests, {display_name: updatedDisplayName.trim()}); - edit_helpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName.trim()); + AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName.trim()}); + EditHelpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName.trim()); }); it('does not change the title when input is the empty string', function () { diff --git a/cms/static/js/spec/xblock/cms.runtime.v1_spec.js b/cms/static/js/spec/xblock/cms.runtime.v1_spec.js index 1eb5fa1d1b8359b1e61ca38c2a5c45bca73e7c21..c508c21548fb81b1872b2c4bfa171958e0645b4f 100644 --- a/cms/static/js/spec/xblock/cms.runtime.v1_spec.js +++ b/cms/static/js/spec/xblock/cms.runtime.v1_spec.js @@ -1,11 +1,11 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cms.runtime.v1"], - function (edit_helpers, BaseModal) { + function (EditHelpers, BaseModal) { describe("Studio Runtime v1", function() { var runtime; beforeEach(function () { - edit_helpers.installEditTemplates(); + EditHelpers.installEditTemplates(); runtime = new window.StudioRuntime.v1(); }); @@ -21,27 +21,27 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cm it('shows save notifications', function() { var title = "Mock saving...", - notificationSpy = edit_helpers.createNotificationSpy(); + notificationSpy = EditHelpers.createNotificationSpy(); runtime.notify('save', { state: 'start', message: title }); - edit_helpers.verifyNotificationShowing(notificationSpy, title); + EditHelpers.verifyNotificationShowing(notificationSpy, title); runtime.notify('save', { state: 'end' }); - edit_helpers.verifyNotificationHidden(notificationSpy); + EditHelpers.verifyNotificationHidden(notificationSpy); }); it('shows error messages', function() { var title = "Mock Error", message = "This is a mock error.", - notificationSpy = edit_helpers.createNotificationSpy("Error"); + notificationSpy = EditHelpers.createNotificationSpy("Error"); runtime.notify('error', { title: title, message: message }); - edit_helpers.verifyNotificationShowing(notificationSpy, title); + EditHelpers.verifyNotificationShowing(notificationSpy, title); }); describe("Modal Dialogs", function() { @@ -61,19 +61,19 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cm }; beforeEach(function () { - edit_helpers.installEditTemplates(); + EditHelpers.installEditTemplates(); }); afterEach(function() { - edit_helpers.hideModalIfShowing(modal); + EditHelpers.hideModalIfShowing(modal); }); it('cancels a modal dialog', function () { showMockModal(); runtime.notify('modal-shown', modal); - expect(edit_helpers.isShowingModal(modal)).toBeTruthy(); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); runtime.notify('cancel'); - expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); }); }); }); diff --git a/cms/static/js/spec_helpers/edit_helpers.js b/cms/static/js/spec_helpers/edit_helpers.js index d59abb3c11c0bad98e2d5c01e511aaadc148d6d8..f6a0d63078d79348c46953675783b8ccb576dc0d 100644 --- a/cms/static/js/spec_helpers/edit_helpers.js +++ b/cms/static/js/spec_helpers/edit_helpers.js @@ -1,10 +1,10 @@ /** * Provides helper methods for invoking Studio editors in Jasmine tests. */ -define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers", - "js/views/modals/edit_xblock", "js/collections/component_template", - "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], - function($, _, create_sinon, modal_helpers, EditXBlockModal, ComponentTemplates) { +define(["jquery", "underscore", "js/common_helpers/ajax_helpers", "js/common_helpers/template_helpers", + "js/spec_helpers/modal_helpers", "js/views/modals/edit_xblock", "js/collections/component_template", + "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], + function($, _, AjaxHelpers, TemplateHelpers, modal_helpers, EditXBlockModal, ComponentTemplates) { var installMockXBlock, uninstallMockXBlock, installMockXModule, uninstallMockXModule, mockComponentTemplates, installEditTemplates, showEditModal, verifyXBlockRequest; @@ -72,25 +72,25 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers modal_helpers.installModalTemplates(append); // Add templates needed by the add XBlock menu - modal_helpers.installTemplate('add-xblock-component'); - modal_helpers.installTemplate('add-xblock-component-button'); - modal_helpers.installTemplate('add-xblock-component-menu'); - modal_helpers.installTemplate('add-xblock-component-menu-problem'); + TemplateHelpers.installTemplate('add-xblock-component'); + TemplateHelpers.installTemplate('add-xblock-component-button'); + TemplateHelpers.installTemplate('add-xblock-component-menu'); + TemplateHelpers.installTemplate('add-xblock-component-menu-problem'); // Add templates needed by the edit XBlock modal - modal_helpers.installTemplate('edit-xblock-modal'); - modal_helpers.installTemplate('editor-mode-button'); + TemplateHelpers.installTemplate('edit-xblock-modal'); + TemplateHelpers.installTemplate('editor-mode-button'); // Add templates needed by the settings editor - modal_helpers.installTemplate('metadata-editor'); - modal_helpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry'); - modal_helpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry'); + TemplateHelpers.installTemplate('metadata-editor'); + TemplateHelpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry'); + TemplateHelpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry'); }; showEditModal = function(requests, xblockElement, model, mockHtml, options) { var modal = new EditXBlockModal({}); modal.edit(xblockElement, model, options); - create_sinon.respondWithJson(requests, { + AjaxHelpers.respondWithJson(requests, { html: mockHtml, "resources": [] }); diff --git a/cms/static/js/spec_helpers/modal_helpers.js b/cms/static/js/spec_helpers/modal_helpers.js index 9e6f6b65a3743eddd8732500a60fb7bfd458f530..461cf1e9446821b17a9de91895acadf10a76e74c 100644 --- a/cms/static/js/spec_helpers/modal_helpers.js +++ b/cms/static/js/spec_helpers/modal_helpers.js @@ -1,15 +1,15 @@ /** * Provides helper methods for invoking Studio modal windows in Jasmine tests. */ -define(["jquery", "js/spec_helpers/view_helpers"], - function($, view_helpers) { +define(["jquery", "js/common_helpers/template_helpers", "js/spec_helpers/view_helpers"], + function($, TemplateHelpers, ViewHelpers) { var installModalTemplates, getModalElement, getModalTitle, isShowingModal, hideModalIfShowing, pressModalButton, cancelModal, cancelModalIfShowing; installModalTemplates = function(append) { - view_helpers.installViewTemplates(append); - view_helpers.installTemplate('basic-modal'); - view_helpers.installTemplate('modal-button'); + ViewHelpers.installViewTemplates(append); + TemplateHelpers.installTemplate('basic-modal'); + TemplateHelpers.installTemplate('modal-button'); }; getModalElement = function(modal) { @@ -56,7 +56,7 @@ define(["jquery", "js/spec_helpers/view_helpers"], } }; - return $.extend(view_helpers, { + return $.extend(ViewHelpers, { 'getModalElement': getModalElement, 'getModalTitle': getModalTitle, 'installModalTemplates': installModalTemplates, diff --git a/cms/static/js/spec_helpers/validation_helpers.js b/cms/static/js/spec_helpers/validation_helpers.js index f9d5efd5190254291b3420f577fa59b377858b81..2854b016012191897f4dfc38c09fb11964085ae7 100644 --- a/cms/static/js/spec_helpers/validation_helpers.js +++ b/cms/static/js/spec_helpers/validation_helpers.js @@ -1,13 +1,13 @@ /** * Provides helper methods for invoking Validation modal in Jasmine tests. */ -define(['jquery', 'js/spec_helpers/modal_helpers', 'js/spec_helpers/view_helpers'], - function($, modal_helpers, view_helpers) { +define(['jquery', 'js/spec_helpers/modal_helpers', 'js/common_helpers/template_helpers'], + function($, ModalHelpers, TemplateHelpers) { var installValidationTemplates, checkErrorContents, undoChanges; installValidationTemplates = function () { - modal_helpers.installModalTemplates(); - view_helpers.installTemplate('validation-error-modal'); + ModalHelpers.installModalTemplates(); + TemplateHelpers.installTemplate('validation-error-modal'); }; checkErrorContents = function(validationModal, errorObjects) { @@ -23,10 +23,10 @@ define(['jquery', 'js/spec_helpers/modal_helpers', 'js/spec_helpers/view_helpers }; undoChanges = function(validationModal) { - modal_helpers.pressModalButton('.action-undo', validationModal); + ModalHelpers.pressModalButton('.action-undo', validationModal); }; - return $.extend(modal_helpers, { + return $.extend(ModalHelpers, { 'installValidationTemplates': installValidationTemplates, 'checkErrorContents': checkErrorContents, 'undoChanges': undoChanges, diff --git a/cms/static/js/spec_helpers/view_helpers.js b/cms/static/js/spec_helpers/view_helpers.js index 6eeea44ac011a422f551f934adf18179f4100c81..c0a319bdc1d1853ad3c22943e4fc4d03a8863354 100644 --- a/cms/static/js/spec_helpers/view_helpers.js +++ b/cms/static/js/spec_helpers/view_helpers.js @@ -1,41 +1,15 @@ /** * Provides helper methods for invoking Studio modal windows in Jasmine tests. */ -define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt"], - function($, NotificationView, Prompt) { - var installTemplate, installTemplates, installViewTemplates, createFeedbackSpy, verifyFeedbackShowing, +define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt", "js/common_helpers/template_helpers"], + function($, NotificationView, Prompt, TemplateHelpers) { + var installViewTemplates, createFeedbackSpy, verifyFeedbackShowing, verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing, verifyNotificationHidden, createPromptSpy, confirmPrompt, inlineEdit, verifyInlineEditChange, installMockAnalytics, removeMockAnalytics, verifyPromptShowing, verifyPromptHidden; - installTemplate = function(templateName, isFirst, templateId) { - var template = readFixtures(templateName + '.underscore'); - if (!templateId) { - templateId = templateName + '-tpl'; - } - - if (isFirst) { - setFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template)); - } else { - appendSetFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template)); - } - }; - - installTemplates = function(templateNames, isFirst) { - if (!$.isArray(templateNames)) { - templateNames = [templateNames]; - } - - $.each(templateNames, function(index, templateName) { - installTemplate(templateName, isFirst); - if (isFirst) { - isFirst = false; - } - }); - }; - installViewTemplates = function(append) { - installTemplate('system-feedback', !append); + TemplateHelpers.installTemplate('system-feedback', !append); appendSetFixtures('<div id="page-notification"></div>'); }; @@ -69,11 +43,11 @@ define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt"], verifyNotificationHidden = function(notificationSpy) { verifyFeedbackHidden.apply(this, arguments); }; - + createPromptSpy = function(type) { return createFeedbackSpy(Prompt, type || 'Warning'); }; - + confirmPrompt = function(promptSpy, pressSecondaryButton) { expect(promptSpy.constructor).toHaveBeenCalled(); if (pressSecondaryButton) { @@ -90,7 +64,7 @@ define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt"], verifyPromptHidden = function(promptSpy) { verifyFeedbackHidden.apply(this, arguments); }; - + installMockAnalytics = function() { window.analytics = jasmine.createSpyObj('analytics', ['track']); window.course_location_analytics = jasmine.createSpy(); @@ -121,8 +95,6 @@ define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt"], }; return { - 'installTemplate': installTemplate, - 'installTemplates': installTemplates, 'installViewTemplates': installViewTemplates, 'createNotificationSpy': createNotificationSpy, 'verifyNotificationShowing': verifyNotificationShowing, diff --git a/cms/static/js_test.yml b/cms/static/js_test.yml index c5d3bff04ffbe32bb2ab901ae63310b092b98673..339d05001533f00a5bb9e94924778ad2202facce 100644 --- a/cms/static/js_test.yml +++ b/cms/static/js_test.yml @@ -67,6 +67,7 @@ lib_paths: src_paths: - coffee/src - js + - js/common_helpers # Paths to spec (test) JavaScript files spec_paths: diff --git a/cms/static/js_test_squire.yml b/cms/static/js_test_squire.yml index 9f844f6680e6ddfe499be5ac04834eebfcf79c5d..e7f43ddd59771c8a5b2c43d88a003a9db9e459e4 100644 --- a/cms/static/js_test_squire.yml +++ b/cms/static/js_test_squire.yml @@ -62,11 +62,13 @@ lib_paths: src_paths: - coffee/src - js + - js/common_helpers # Paths to spec (test) JavaScript files spec_paths: - coffee/spec/main.js - coffee/spec + - js/spec # Paths to fixture files (optional) # The fixture path will be set automatically when using jasmine-jquery. diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py index 782ce16b47bf83a666418dfedcba3347095f09eb..9a6340c2bfe1629b43cdb2dcdc15807313d42aa3 100644 --- a/common/djangoapps/course_groups/cohorts.py +++ b/common/djangoapps/course_groups/cohorts.py @@ -3,11 +3,12 @@ This file contains the logic for cohort groups, as exposed internally to the forums, and to the cohort admin views. """ -from django.http import Http404 - import logging import random +from django.http import Http404 +from django.utils.translation import ugettext as _ + from courseware import courses from student.models import get_user_by_username_or_email from .models import CourseUserGroup @@ -15,11 +16,48 @@ from .models import CourseUserGroup log = logging.getLogger(__name__) +# A 'default cohort' is an auto-cohort that is automatically created for a course if no auto_cohort_groups have been +# specified. It is intended to be used in a cohorted-course for users who have yet to be assigned to a cohort. +# Note 1: If an administrator chooses to configure a cohort with the same name, the said cohort will be used as +# the "default cohort". +# Note 2: If auto_cohort_groups are configured after the 'default cohort' has been created and populated, the +# stagnant 'default cohort' will still remain (now as a manual cohort) with its previously assigned students. +# Translation Note: We are NOT translating this string since it is the constant identifier for the "default group" +# and needed across product boundaries. +DEFAULT_COHORT_NAME = "Default Group" + + +class CohortAssignmentType(object): + """ + The various types of rule-based cohorts + """ + # No automatic rules are applied to this cohort; users must be manually added. + NONE = "none" + + # One of (possibly) multiple cohort groups to which users are randomly assigned. + # Note: The 'default cohort' group is included in this category iff it exists and + # there are no other random groups. (Also see Note 2 above.) + RANDOM = "random" + + @staticmethod + def get(cohort, course): + """ + Returns the assignment type of the given cohort for the given course + """ + if cohort.name in course.auto_cohort_groups: + return CohortAssignmentType.RANDOM + elif len(course.auto_cohort_groups) == 0 and cohort.name == DEFAULT_COHORT_NAME: + return CohortAssignmentType.RANDOM + else: + return CohortAssignmentType.NONE + + # tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even # if and when that's fixed, it's a good idea to have a local generator to avoid any other # code that messes with the global random module. _local_random = None + def local_random(): """ Get the local random number generator. In a function so that we don't run @@ -103,7 +141,7 @@ def get_cohorted_commentables(course_key): def get_cohort(user, course_key): """ - Given a django User and a CourseKey, return the user's cohort in that + Given a Django user and a CourseKey, return the user's cohort in that cohort. Arguments: @@ -135,27 +173,19 @@ def get_cohort(user, course_key): # Didn't find the group. We'll go on to create one if needed. pass - if not course.auto_cohort: - return None - choices = course.auto_cohort_groups - n = len(choices) - if n == 0: - # Nowhere to put user - log.warning("Course %s is auto-cohorted, but there are no" - " auto_cohort_groups specified", - course_key) - return None - - # Put user in a random group, creating it if needed - group_name = local_random().choice(choices) + if len(choices) > 0: + # Randomly choose one of the auto_cohort_groups, creating it if needed. + group_name = local_random().choice(choices) + else: + # Use the "default cohort". + group_name = DEFAULT_COHORT_NAME - group, created = CourseUserGroup.objects.get_or_create( + group, __ = CourseUserGroup.objects.get_or_create( course_id=course_key, group_type=CourseUserGroup.COHORT, name=group_name ) - user.course_groups.add(group) return group @@ -172,15 +202,13 @@ def get_course_cohorts(course): A list of CourseUserGroup objects. Empty if there are no cohorts. Does not check whether the course is cohorted. """ - # TODO: remove auto_cohort check with TNL-160 - if course.auto_cohort: - # Ensure all auto cohorts are created. - for group_name in course.auto_cohort_groups: - CourseUserGroup.objects.get_or_create( - course_id=course.location.course_key, - group_type=CourseUserGroup.COHORT, - name=group_name - ) + # Ensure all auto cohorts are created. + for group_name in course.auto_cohort_groups: + CourseUserGroup.objects.get_or_create( + course_id=course.location.course_key, + group_type=CourseUserGroup.COHORT, + name=group_name + ) return list(CourseUserGroup.objects.filter( course_id=course.location.course_key, @@ -223,7 +251,7 @@ def add_cohort(course_key, name): if CourseUserGroup.objects.filter(course_id=course_key, group_type=CourseUserGroup.COHORT, name=name).exists(): - raise ValueError("Can't create two cohorts with the same name") + raise ValueError(_("You cannot create two cohorts with the same name")) try: course = courses.get_course_by_id(course_key) @@ -269,9 +297,10 @@ def add_user_to_cohort(cohort, username_or_email): ) if course_cohorts.exists(): if course_cohorts[0] == cohort: - raise ValueError("User {0} already present in cohort {1}".format( - user.username, - cohort.name)) + raise ValueError("User {user_name} already present in cohort {cohort_name}".format( + user_name=user.username, + cohort_name=cohort.name + )) else: previous_cohort = course_cohorts[0].name course_cohorts[0].users.remove(user) @@ -286,8 +315,9 @@ def delete_empty_cohort(course_key, name): """ cohort = get_cohort_by_name(course_key, name) if cohort.users.exists(): - raise ValueError( - "Can't delete non-empty cohort {0} in course {1}".format( - name, course_key)) + raise ValueError(_("You cannot delete non-empty cohort {cohort_name} in course {course_key}").format( + cohort_name=name, + course_key=course_key + )) cohort.delete() diff --git a/common/djangoapps/course_groups/tests/helpers.py b/common/djangoapps/course_groups/tests/helpers.py index 321f05a3415ec1ba9e4bfcc3d683fd10396aa0a5..8b19efa37990014e6191de21a47ecdc56effc731 100644 --- a/common/djangoapps/course_groups/tests/helpers.py +++ b/common/djangoapps/course_groups/tests/helpers.py @@ -1,10 +1,32 @@ """ Helper methods for testing cohorts. """ +from factory import post_generation, Sequence +from factory.django import DjangoModelFactory +from course_groups.models import CourseUserGroup from xmodule.modulestore.django import modulestore from xmodule.modulestore import ModuleStoreEnum +class CohortFactory(DjangoModelFactory): + """ + Factory for constructing mock cohorts. + """ + FACTORY_FOR = CourseUserGroup + + name = Sequence("cohort{}".format) + course_id = "dummy_id" + group_type = CourseUserGroup.COHORT + + @post_generation + def users(self, create, extracted, **kwargs): # pylint: disable=W0613 + """ + Returns the users associated with the cohort. + """ + if extracted: + self.users.add(*extracted) + + def topic_name_to_id(course, name): """ Given a discussion topic name, return an id for that name (includes @@ -17,11 +39,13 @@ def topic_name_to_id(course, name): ) -def config_course_cohorts(course, discussions, - cohorted, - cohorted_discussions=None, - auto_cohort=None, - auto_cohort_groups=None): +def config_course_cohorts( + course, + discussions, + cohorted, + cohorted_discussions=None, + auto_cohort_groups=None +): """ Given a course with no discussion set up, add the discussions and set the cohort config appropriately. @@ -33,7 +57,6 @@ def config_course_cohorts(course, discussions, cohorted: bool. cohorted_discussions: optional list of topic names. If specified, converts them to use the same ids as topic names. - auto_cohort: optional bool. auto_cohort_groups: optional list of strings (names of groups to put students into). @@ -54,8 +77,6 @@ def config_course_cohorts(course, discussions, d["cohorted_discussions"] = [to_id(name) for name in cohorted_discussions] - if auto_cohort is not None: - d["auto_cohort"] = auto_cohort if auto_cohort_groups is not None: d["auto_cohort_groups"] = auto_cohort_groups diff --git a/common/djangoapps/course_groups/tests/test_cohorts.py b/common/djangoapps/course_groups/tests/test_cohorts.py index 032b10bbf232559c1e1c92a84a6573f2bbccc74d..16d971dded35ae8ce43687da709f2234e81d24dc 100644 --- a/common/djangoapps/course_groups/tests/test_cohorts.py +++ b/common/djangoapps/course_groups/tests/test_cohorts.py @@ -6,9 +6,10 @@ from django.http import Http404 from django.test.utils import override_settings from student.models import CourseEnrollment +from student.tests.factories import UserFactory from course_groups.models import CourseUserGroup from course_groups import cohorts -from course_groups.tests.helpers import topic_name_to_id, config_course_cohorts +from course_groups.tests.helpers import topic_name_to_id, config_course_cohorts, CohortFactory from xmodule.modulestore.django import modulestore, clear_existing_modulestores from opaque_keys.edx.locations import SlashSeparatedCourseKey @@ -59,13 +60,11 @@ class TestCohorts(django.test.TestCase): course = modulestore().get_course(self.toy_course_key) self.assertFalse(course.is_cohorted) - user = User.objects.create(username="test", email="a@b.com") + user = UserFactory(username="test", email="a@b.com") self.assertIsNone(cohorts.get_cohort_id(user, course.id)) - config_course_cohorts(course, [], cohorted=True) - cohort = CourseUserGroup.objects.create(name="TestCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT) + config_course_cohorts(course, discussions=[], cohorted=True) + cohort = CohortFactory(course_id=course.id, name="TestCohort") cohort.users.add(user) self.assertEqual(cohorts.get_cohort_id(user, course.id), cohort.id) @@ -82,81 +81,98 @@ class TestCohorts(django.test.TestCase): self.assertEqual(course.id, self.toy_course_key) self.assertFalse(course.is_cohorted) - user = User.objects.create(username="test", email="a@b.com") - other_user = User.objects.create(username="test2", email="a2@b.com") + user = UserFactory(username="test", email="a@b.com") + other_user = UserFactory(username="test2", email="a2@b.com") self.assertIsNone(cohorts.get_cohort(user, course.id), "No cohort created yet") - cohort = CourseUserGroup.objects.create(name="TestCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT) - + cohort = CohortFactory(course_id=course.id, name="TestCohort") cohort.users.add(user) - self.assertIsNone(cohorts.get_cohort(user, course.id), - "Course isn't cohorted, so shouldn't have a cohort") + self.assertIsNone( + cohorts.get_cohort(user, course.id), + "Course isn't cohorted, so shouldn't have a cohort" + ) # Make the course cohorted... - config_course_cohorts(course, [], cohorted=True) - - self.assertEquals(cohorts.get_cohort(user, course.id).id, cohort.id, - "Should find the right cohort") + config_course_cohorts(course, discussions=[], cohorted=True) - self.assertEquals(cohorts.get_cohort(other_user, course.id), None, - "other_user shouldn't have a cohort") + self.assertEquals( + cohorts.get_cohort(user, course.id).id, + cohort.id, + "user should be assigned to the correct cohort" + ) + self.assertEquals( + cohorts.get_cohort(other_user, course.id).id, + cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).id, + "other_user should be assigned to the default cohort" + ) def test_auto_cohorting(self): """ - Make sure cohorts.get_cohort() does the right thing when the course is auto_cohorted + Make sure cohorts.get_cohort() does the right thing with auto_cohort_groups """ course = modulestore().get_course(self.toy_course_key) self.assertFalse(course.is_cohorted) - user1 = User.objects.create(username="test", email="a@b.com") - user2 = User.objects.create(username="test2", email="a2@b.com") - user3 = User.objects.create(username="test3", email="a3@b.com") + user1 = UserFactory(username="test", email="a@b.com") + user2 = UserFactory(username="test2", email="a2@b.com") + user3 = UserFactory(username="test3", email="a3@b.com") + user4 = UserFactory(username="test4", email="a4@b.com") - cohort = CourseUserGroup.objects.create(name="TestCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT) + cohort = CohortFactory(course_id=course.id, name="TestCohort") # user1 manually added to a cohort cohort.users.add(user1) - # Make the course auto cohorted... + # Add an auto_cohort_group to the course... config_course_cohorts( - course, [], cohorted=True, - auto_cohort=True, + course, + discussions=[], + cohorted=True, auto_cohort_groups=["AutoGroup"] ) - self.assertEquals(cohorts.get_cohort(user1, course.id).id, cohort.id, - "user1 should stay put") + self.assertEquals(cohorts.get_cohort(user1, course.id).id, cohort.id, "user1 should stay put") - self.assertEquals(cohorts.get_cohort(user2, course.id).name, "AutoGroup", - "user2 should be auto-cohorted") + self.assertEquals(cohorts.get_cohort(user2, course.id).name, "AutoGroup", "user2 should be auto-cohorted") - # Now make the group list empty + # Now make the auto_cohort_group list empty config_course_cohorts( - course, [], cohorted=True, - auto_cohort=True, + course, + discussions=[], + cohorted=True, auto_cohort_groups=[] ) - self.assertEquals(cohorts.get_cohort(user3, course.id), None, - "No groups->no auto-cohorting") + self.assertEquals( + cohorts.get_cohort(user3, course.id).id, + cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).id, + "No groups->default cohort" + ) - # Now make it different + # Now set the auto_cohort_group to something different config_course_cohorts( - course, [], cohorted=True, - auto_cohort=True, + course, + discussions=[], + cohorted=True, auto_cohort_groups=["OtherGroup"] ) - self.assertEquals(cohorts.get_cohort(user3, course.id).name, "OtherGroup", - "New list->new group") - self.assertEquals(cohorts.get_cohort(user2, course.id).name, "AutoGroup", - "user2 should still be in originally placed cohort") + self.assertEquals( + cohorts.get_cohort(user4, course.id).name, "OtherGroup", "New list->new group" + ) + self.assertEquals( + cohorts.get_cohort(user1, course.id).name, "TestCohort", "user1 should still be in originally placed cohort" + ) + self.assertEquals( + cohorts.get_cohort(user2, course.id).name, "AutoGroup", "user2 should still be in originally placed cohort" + ) + self.assertEquals( + cohorts.get_cohort(user3, course.id).name, + cohorts.get_cohort_by_name(course.id, cohorts.DEFAULT_COHORT_NAME).name, + "user3 should still be in the default cohort" + ) def test_auto_cohorting_randomization(self): """ @@ -167,15 +183,15 @@ class TestCohorts(django.test.TestCase): groups = ["group_{0}".format(n) for n in range(5)] config_course_cohorts( - course, [], cohorted=True, - auto_cohort=True, - auto_cohort_groups=groups + course, discussions=[], cohorted=True, auto_cohort_groups=groups ) # Assign 100 users to cohorts for i in range(100): - user = User.objects.create(username="test_{0}".format(i), - email="a@b{0}.com".format(i)) + user = UserFactory( + username="test_{0}".format(i), + email="a@b{0}.com".format(i) + ) cohorts.get_cohort(user, course.id) # Now make sure that the assignment was at least vaguely random: @@ -196,45 +212,22 @@ class TestCohorts(django.test.TestCase): config_course_cohorts(course, [], cohorted=True) self.assertEqual([], cohorts.get_course_cohorts(course)) - def _verify_course_cohorts(self, auto_cohort, expected_cohort_set): + def test_get_course_cohorts(self): """ - Helper method for testing get_course_cohorts with both manual and auto cohorts. + Tests that get_course_cohorts returns all cohorts, including auto cohorts. """ course = modulestore().get_course(self.toy_course_key) config_course_cohorts( - course, [], cohorted=True, auto_cohort=auto_cohort, + course, [], cohorted=True, auto_cohort_groups=["AutoGroup1", "AutoGroup2"] ) # add manual cohorts to course 1 - CourseUserGroup.objects.create( - name="ManualCohort", - course_id=course.location.course_key, - group_type=CourseUserGroup.COHORT - ) - - CourseUserGroup.objects.create( - name="ManualCohort2", - course_id=course.location.course_key, - group_type=CourseUserGroup.COHORT - ) + CohortFactory(course_id=course.id, name="ManualCohort") + CohortFactory(course_id=course.id, name="ManualCohort2") cohort_set = {c.name for c in cohorts.get_course_cohorts(course)} - self.assertEqual(cohort_set, expected_cohort_set) - - def test_get_course_cohorts_auto_cohort_enabled(self): - """ - Tests that get_course_cohorts returns all cohorts, including auto cohorts, - when auto_cohort is True. - """ - self._verify_course_cohorts(True, {"AutoGroup1", "AutoGroup2", "ManualCohort", "ManualCohort2"}) - - # TODO: Update test case with TNL-160 (auto cohorts WILL be returned). - def test_get_course_cohorts_auto_cohort_disabled(self): - """ - Tests that get_course_cohorts does not return auto cohorts if auto_cohort is False. - """ - self._verify_course_cohorts(False, {"ManualCohort", "ManualCohort2"}) + self.assertEqual(cohort_set, {"AutoGroup1", "AutoGroup2", "ManualCohort", "ManualCohort2"}) def test_is_commentable_cohorted(self): course = modulestore().get_course(self.toy_course_key) @@ -244,25 +237,31 @@ class TestCohorts(django.test.TestCase): return topic_name_to_id(course, name) # no topics - self.assertFalse(cohorts.is_commentable_cohorted(course.id, to_id("General")), - "Course doesn't even have a 'General' topic") + self.assertFalse( + cohorts.is_commentable_cohorted(course.id, to_id("General")), + "Course doesn't even have a 'General' topic" + ) # not cohorted config_course_cohorts(course, ["General", "Feedback"], cohorted=False) - self.assertFalse(cohorts.is_commentable_cohorted(course.id, to_id("General")), - "Course isn't cohorted") + self.assertFalse( + cohorts.is_commentable_cohorted(course.id, to_id("General")), + "Course isn't cohorted" + ) # cohorted, but top level topics aren't config_course_cohorts(course, ["General", "Feedback"], cohorted=True) self.assertTrue(course.is_cohorted) - self.assertFalse(cohorts.is_commentable_cohorted(course.id, to_id("General")), - "Course is cohorted, but 'General' isn't.") - + self.assertFalse( + cohorts.is_commentable_cohorted(course.id, to_id("General")), + "Course is cohorted, but 'General' isn't." + ) self.assertTrue( cohorts.is_commentable_cohorted(course.id, to_id("random")), - "Non-top-level discussion is always cohorted in cohorted courses.") + "Non-top-level discussion is always cohorted in cohorted courses." + ) # cohorted, including "Feedback" top-level topics aren't config_course_cohorts( @@ -272,12 +271,14 @@ class TestCohorts(django.test.TestCase): ) self.assertTrue(course.is_cohorted) - self.assertFalse(cohorts.is_commentable_cohorted(course.id, to_id("General")), - "Course is cohorted, but 'General' isn't.") - + self.assertFalse( + cohorts.is_commentable_cohorted(course.id, to_id("General")), + "Course is cohorted, but 'General' isn't." + ) self.assertTrue( cohorts.is_commentable_cohorted(course.id, to_id("Feedback")), - "Feedback was listed as cohorted. Should be.") + "Feedback was listed as cohorted. Should be." + ) def test_get_cohorted_commentables(self): """ @@ -327,11 +328,7 @@ class TestCohorts(django.test.TestCase): lambda: cohorts.get_cohort_by_name(course.id, "CohortDoesNotExist") ) - cohort = CourseUserGroup.objects.create( - name="MyCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT - ) + cohort = CohortFactory(course_id=course.id, name="MyCohort") self.assertEqual(cohorts.get_cohort_by_name(course.id, "MyCohort"), cohort) @@ -346,11 +343,7 @@ class TestCohorts(django.test.TestCase): course. """ course = modulestore().get_course(self.toy_course_key) - cohort = CourseUserGroup.objects.create( - name="MyCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT - ) + cohort = CohortFactory(course_id=course.id, name="MyCohort") self.assertEqual(cohorts.get_cohort_by_id(course.id, cohort.id), cohort) @@ -384,20 +377,12 @@ class TestCohorts(django.test.TestCase): Make sure cohorts.add_user_to_cohort() properly adds a user to a cohort and handles errors. """ - course_user = User.objects.create(username="Username", email="a@b.com") - User.objects.create(username="RandomUsername", email="b@b.com") + course_user = UserFactory(username="Username", email="a@b.com") + UserFactory(username="RandomUsername", email="b@b.com") course = modulestore().get_course(self.toy_course_key) CourseEnrollment.enroll(course_user, self.toy_course_key) - first_cohort = CourseUserGroup.objects.create( - name="FirstCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT - ) - second_cohort = CourseUserGroup.objects.create( - name="SecondCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT - ) + first_cohort = CohortFactory(course_id=course.id, name="FirstCohort") + second_cohort = CohortFactory(course_id=course.id, name="SecondCohort") # Success cases # We shouldn't get back a previous cohort, since the user wasn't in one @@ -430,17 +415,9 @@ class TestCohorts(django.test.TestCase): for a given course. """ course = modulestore().get_course(self.toy_course_key) - user = User.objects.create(username="Username", email="a@b.com") - empty_cohort = CourseUserGroup.objects.create( - name="EmptyCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT - ) - nonempty_cohort = CourseUserGroup.objects.create( - name="NonemptyCohort", - course_id=course.id, - group_type=CourseUserGroup.COHORT - ) + user = UserFactory(username="Username", email="a@b.com") + empty_cohort = CohortFactory(course_id=course.id, name="EmptyCohort") + nonempty_cohort = CohortFactory(course_id=course.id, name="NonemptyCohort") nonempty_cohort.users.add(user) cohorts.delete_empty_cohort(course.id, "EmptyCohort") @@ -448,11 +425,7 @@ class TestCohorts(django.test.TestCase): # Make sure we cannot access the deleted cohort self.assertRaises( CourseUserGroup.DoesNotExist, - lambda: CourseUserGroup.objects.get( - course_id=course.id, - group_type=CourseUserGroup.COHORT, - id=empty_cohort.id - ) + lambda: cohorts.get_cohort_by_id(course.id, empty_cohort.id) ) self.assertRaises( ValueError, diff --git a/common/djangoapps/course_groups/tests/test_views.py b/common/djangoapps/course_groups/tests/test_views.py index 9be53603c31cb56a498fc9b21784b7df1e9c8916..819190237f9e3e38a9e6bbc04cd0f3f3be613a7f 100644 --- a/common/djangoapps/course_groups/tests/test_views.py +++ b/common/djangoapps/course_groups/tests/test_views.py @@ -2,14 +2,11 @@ import json from django.test.client import RequestFactory from django.test.utils import override_settings -from factory import post_generation, Sequence -from factory.django import DjangoModelFactory -from course_groups.tests.helpers import config_course_cohorts +from course_groups.tests.helpers import config_course_cohorts, CohortFactory from collections import namedtuple from django.http import Http404 from django.contrib.auth.models import User -from course_groups.models import CourseUserGroup from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE from student.models import CourseEnrollment from student.tests.factories import UserFactory @@ -17,18 +14,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from opaque_keys.edx.locations import SlashSeparatedCourseKey +from course_groups.models import CourseUserGroup from course_groups.views import list_cohorts, add_cohort, users_in_cohort, add_users_to_cohort, remove_user_from_cohort - -class CohortFactory(DjangoModelFactory): - FACTORY_FOR = CourseUserGroup - - name = Sequence("cohort{}".format) - course_id = "dummy_id" - group_type = CourseUserGroup.COHORT - - @post_generation - def users(self, create, extracted, **kwargs): # pylint: disable=W0613 - self.users.add(*extracted) +from course_groups.cohorts import get_cohort, CohortAssignmentType, get_cohort_by_name, DEFAULT_COHORT_NAME @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @@ -38,8 +26,8 @@ class CohortViewsTestCase(ModuleStoreTestCase): """ def setUp(self): self.course = CourseFactory.create() - self.staff_user = UserFactory.create(is_staff=True, username="staff") - self.non_staff_user = UserFactory.create(username="nonstaff") + self.staff_user = UserFactory(is_staff=True, username="staff") + self.non_staff_user = UserFactory(username="nonstaff") def _enroll_users(self, users, course_key): """Enroll each user in the specified course""" @@ -48,34 +36,18 @@ class CohortViewsTestCase(ModuleStoreTestCase): def _create_cohorts(self): """Creates cohorts for testing""" - self.cohort1_users = [UserFactory.create() for _ in range(3)] - self.cohort2_users = [UserFactory.create() for _ in range(2)] - self.cohort3_users = [UserFactory.create() for _ in range(2)] - self.cohortless_users = [UserFactory.create() for _ in range(3)] - self.unenrolled_users = [UserFactory.create() for _ in range(3)] + self.cohort1_users = [UserFactory() for _ in range(3)] + self.cohort2_users = [UserFactory() for _ in range(2)] + self.cohort3_users = [UserFactory() for _ in range(2)] + self.cohortless_users = [UserFactory() for _ in range(3)] + self.unenrolled_users = [UserFactory() for _ in range(3)] self._enroll_users( self.cohort1_users + self.cohort2_users + self.cohort3_users + self.cohortless_users, self.course.id ) - self.cohort1 = CohortFactory.create(course_id=self.course.id, users=self.cohort1_users) - self.cohort2 = CohortFactory.create(course_id=self.course.id, users=self.cohort2_users) - self.cohort3 = CohortFactory.create(course_id=self.course.id, users=self.cohort3_users) - - def _cohort_in_course(self, cohort_name, course): - """ - Returns true iff `course` contains a cohort with the name - `cohort_name`. - """ - try: - CourseUserGroup.objects.get( - course_id=course.id, - group_type=CourseUserGroup.COHORT, - name=cohort_name - ) - except CourseUserGroup.DoesNotExist: - return False - else: - return True + self.cohort1 = CohortFactory(course_id=self.course.id, users=self.cohort1_users) + self.cohort2 = CohortFactory(course_id=self.course.id, users=self.cohort2_users) + self.cohort3 = CohortFactory(course_id=self.course.id, users=self.cohort3_users) def _user_in_cohort(self, username, cohort): """ @@ -117,26 +89,37 @@ class ListCohortsTestCase(CohortViewsTestCase): self.assertEqual(response.status_code, 200) return json.loads(response.content) - def verify_lists_expected_cohorts(self, response_dict, expected_cohorts): + def verify_lists_expected_cohorts(self, expected_cohorts, response_dict=None): """ Verify that the server response contains the expected_cohorts. + If response_dict is None, the list of cohorts is requested from the server. """ + if response_dict is None: + response_dict = self.request_list_cohorts(self.course) + self.assertTrue(response_dict.get("success")) self.assertItemsEqual( response_dict.get("cohorts"), [ - {"name": cohort.name, "id": cohort.id, "user_count": cohort.user_count} + { + "name": cohort.name, + "id": cohort.id, + "user_count": cohort.user_count, + "assignment_type": cohort.assignment_type + } for cohort in expected_cohorts ] ) @staticmethod - def create_expected_cohort(cohort, user_count): + def create_expected_cohort(cohort, user_count, assignment_type): """ Create a tuple storing the expected cohort information. """ - cohort_tuple = namedtuple("Cohort", "name id user_count") - return cohort_tuple(name=cohort.name, id=cohort.id, user_count=user_count) + cohort_tuple = namedtuple("Cohort", "name id user_count assignment_type") + return cohort_tuple( + name=cohort.name, id=cohort.id, user_count=user_count, assignment_type=assignment_type + ) def test_non_staff(self): """ @@ -148,7 +131,7 @@ class ListCohortsTestCase(CohortViewsTestCase): """ Verify that no cohorts are in response for a course with no cohorts. """ - self.verify_lists_expected_cohorts(self.request_list_cohorts(self.course), []) + self.verify_lists_expected_cohorts([]) def test_some_cohorts(self): """ @@ -156,17 +139,17 @@ class ListCohortsTestCase(CohortViewsTestCase): """ self._create_cohorts() expected_cohorts = [ - ListCohortsTestCase.create_expected_cohort(self.cohort1, 3), - ListCohortsTestCase.create_expected_cohort(self.cohort2, 2), - ListCohortsTestCase.create_expected_cohort(self.cohort3, 2), + ListCohortsTestCase.create_expected_cohort(self.cohort1, 3, CohortAssignmentType.NONE), + ListCohortsTestCase.create_expected_cohort(self.cohort2, 2, CohortAssignmentType.NONE), + ListCohortsTestCase.create_expected_cohort(self.cohort3, 2, CohortAssignmentType.NONE), ] - self.verify_lists_expected_cohorts(self.request_list_cohorts(self.course), expected_cohorts) + self.verify_lists_expected_cohorts(expected_cohorts) def test_auto_cohorts(self): """ Verify that auto cohorts are included in the response. """ - config_course_cohorts(self.course, [], cohorted=True, auto_cohort=True, + config_course_cohorts(self.course, [], cohorted=True, auto_cohort_groups=["AutoGroup1", "AutoGroup2"]) # Will create cohort1, cohort2, and cohort3. Auto cohorts remain uncreated. @@ -174,25 +157,57 @@ class ListCohortsTestCase(CohortViewsTestCase): # Get the cohorts from the course, which will cause auto cohorts to be created. actual_cohorts = self.request_list_cohorts(self.course) # Get references to the created auto cohorts. - auto_cohort_1 = CourseUserGroup.objects.get( - course_id=self.course.location.course_key, - group_type=CourseUserGroup.COHORT, - name="AutoGroup1" - ) - auto_cohort_2 = CourseUserGroup.objects.get( - course_id=self.course.location.course_key, - group_type=CourseUserGroup.COHORT, - name="AutoGroup2" - ) + auto_cohort_1 = get_cohort_by_name(self.course.id, "AutoGroup1") + auto_cohort_2 = get_cohort_by_name(self.course.id, "AutoGroup2") expected_cohorts = [ - ListCohortsTestCase.create_expected_cohort(self.cohort1, 3), - ListCohortsTestCase.create_expected_cohort(self.cohort2, 2), - ListCohortsTestCase.create_expected_cohort(self.cohort3, 2), - ListCohortsTestCase.create_expected_cohort(auto_cohort_1, 0), - ListCohortsTestCase.create_expected_cohort(auto_cohort_2, 0), + ListCohortsTestCase.create_expected_cohort(self.cohort1, 3, CohortAssignmentType.NONE), + ListCohortsTestCase.create_expected_cohort(self.cohort2, 2, CohortAssignmentType.NONE), + ListCohortsTestCase.create_expected_cohort(self.cohort3, 2, CohortAssignmentType.NONE), + ListCohortsTestCase.create_expected_cohort(auto_cohort_1, 0, CohortAssignmentType.RANDOM), + ListCohortsTestCase.create_expected_cohort(auto_cohort_2, 0, CohortAssignmentType.RANDOM), ] - self.verify_lists_expected_cohorts(actual_cohorts, expected_cohorts) + self.verify_lists_expected_cohorts(expected_cohorts, actual_cohorts) + def test_default_cohort(self): + """ + Verify that the default cohort is not created and included in the response until students are assigned to it. + """ + # verify the default cohort is not created when the course is not cohorted + self.verify_lists_expected_cohorts([]) + + # create a cohorted course without any auto_cohort_groups + config_course_cohorts(self.course, [], cohorted=True) + + # verify the default cohort is not yet created until a user is assigned + self.verify_lists_expected_cohorts([]) + + # create enrolled users + users = [UserFactory() for _ in range(3)] + self._enroll_users(users, self.course.id) + + # mimic users accessing the discussion forum + for user in users: + get_cohort(user, self.course.id) + + # verify the default cohort is automatically created + default_cohort = get_cohort_by_name(self.course.id, DEFAULT_COHORT_NAME) + actual_cohorts = self.request_list_cohorts(self.course) + self.verify_lists_expected_cohorts( + [ListCohortsTestCase.create_expected_cohort(default_cohort, len(users), CohortAssignmentType.RANDOM)], + actual_cohorts, + ) + + # set auto_cohort_groups and verify the default cohort is no longer listed as RANDOM + config_course_cohorts(self.course, [], cohorted=True, auto_cohort_groups=["AutoGroup"]) + actual_cohorts = self.request_list_cohorts(self.course) + auto_cohort = get_cohort_by_name(self.course.id, "AutoGroup") + self.verify_lists_expected_cohorts( + [ + ListCohortsTestCase.create_expected_cohort(default_cohort, len(users), CohortAssignmentType.NONE), + ListCohortsTestCase.create_expected_cohort(auto_cohort, 0, CohortAssignmentType.RANDOM), + ], + actual_cohorts, + ) class AddCohortTestCase(CohortViewsTestCase): """ @@ -208,7 +223,7 @@ class AddCohortTestCase(CohortViewsTestCase): self.assertEqual(response.status_code, 200) return json.loads(response.content) - def verify_contains_added_cohort(self, response_dict, cohort_name, course, expected_error_msg=None): + def verify_contains_added_cohort(self, response_dict, cohort_name, expected_error_msg=None): """ Check that `add_cohort`'s response correctly returns the newly added cohort (or error) in the response. Also verify that the cohort was @@ -226,7 +241,7 @@ class AddCohortTestCase(CohortViewsTestCase): response_dict.get("cohort").get("name"), cohort_name ) - self.assertTrue(self._cohort_in_course(cohort_name, course)) + self.assertIsNotNone(get_cohort_by_name(self.course.id, cohort_name)) def test_non_staff(self): """ @@ -242,7 +257,6 @@ class AddCohortTestCase(CohortViewsTestCase): self.verify_contains_added_cohort( self.request_add_cohort(cohort_name, self.course), cohort_name, - self.course ) def test_no_cohort(self): @@ -263,8 +277,7 @@ class AddCohortTestCase(CohortViewsTestCase): self.verify_contains_added_cohort( self.request_add_cohort(cohort_name, self.course), cohort_name, - self.course, - expected_error_msg="Can't create two cohorts with the same name" + expected_error_msg="You cannot create two cohorts with the same name" ) @@ -308,14 +321,14 @@ class UsersInCohortTestCase(CohortViewsTestCase): """ Verify that non-staff users cannot access `check_users_in_cohort`. """ - cohort = CohortFactory.create(course_id=self.course.id, users=[]) + cohort = CohortFactory(course_id=self.course.id, users=[]) self._verify_non_staff_cannot_access(users_in_cohort, "GET", [self.course.id.to_deprecated_string(), cohort.id]) def test_no_users(self): """ Verify that we don't get back any users for a cohort with no users. """ - cohort = CohortFactory.create(course_id=self.course.id, users=[]) + cohort = CohortFactory(course_id=self.course.id, users=[]) response_dict = self.request_users_in_cohort(cohort, self.course, 1) self.verify_users_in_cohort_and_response( cohort, @@ -330,8 +343,8 @@ class UsersInCohortTestCase(CohortViewsTestCase): Verify that we get back all users for a cohort when the cohort has <=100 users. """ - users = [UserFactory.create() for _ in range(5)] - cohort = CohortFactory.create(course_id=self.course.id, users=users) + users = [UserFactory() for _ in range(5)] + cohort = CohortFactory(course_id=self.course.id, users=users) response_dict = self.request_users_in_cohort(cohort, self.course, 1) self.verify_users_in_cohort_and_response( cohort, @@ -345,8 +358,8 @@ class UsersInCohortTestCase(CohortViewsTestCase): """ Verify that pagination works correctly for cohorts with >100 users. """ - users = [UserFactory.create() for _ in range(101)] - cohort = CohortFactory.create(course_id=self.course.id, users=users) + users = [UserFactory() for _ in range(101)] + cohort = CohortFactory(course_id=self.course.id, users=users) response_dict_1 = self.request_users_in_cohort(cohort, self.course, 1) response_dict_2 = self.request_users_in_cohort(cohort, self.course, 2) self.verify_users_in_cohort_and_response( @@ -369,8 +382,8 @@ class UsersInCohortTestCase(CohortViewsTestCase): Verify that we get a blank page of users when requesting page 0 or a page greater than the actual number of pages. """ - users = [UserFactory.create() for _ in range(5)] - cohort = CohortFactory.create(course_id=self.course.id, users=users) + users = [UserFactory() for _ in range(5)] + cohort = CohortFactory(course_id=self.course.id, users=users) response = self.request_users_in_cohort(cohort, self.course, 0) self.verify_users_in_cohort_and_response( cohort, @@ -393,8 +406,8 @@ class UsersInCohortTestCase(CohortViewsTestCase): Verify that we get a `HttpResponseBadRequest` (bad request) when the page we request isn't a positive integer. """ - users = [UserFactory.create() for _ in range(5)] - cohort = CohortFactory.create(course_id=self.course.id, users=users) + users = [UserFactory() for _ in range(5)] + cohort = CohortFactory(course_id=self.course.id, users=users) self.request_users_in_cohort(cohort, self.course, "invalid", should_return_bad_request=True) self.request_users_in_cohort(cohort, self.course, -1, should_return_bad_request=True) @@ -476,7 +489,7 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): """ Verify that non-staff users cannot access `check_users_in_cohort`. """ - cohort = CohortFactory.create(course_id=self.course.id, users=[]) + cohort = CohortFactory(course_id=self.course.id, users=[]) self._verify_non_staff_cannot_access( add_users_to_cohort, "POST", @@ -686,10 +699,10 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): Verify that an error is raised when trying to add users to a cohort which does not belong to the given course. """ - users = [UserFactory.create(username="user{0}".format(i)) for i in range(3)] + users = [UserFactory(username="user{0}".format(i)) for i in range(3)] usernames = [user.username for user in users] wrong_course_key = SlashSeparatedCourseKey("some", "arbitrary", "course") - wrong_course_cohort = CohortFactory.create(name="wrong_cohort", course_id=wrong_course_key, users=[]) + wrong_course_cohort = CohortFactory(name="wrong_cohort", course_id=wrong_course_key, users=[]) self.request_add_users_to_cohort( ",".join(usernames), wrong_course_cohort, @@ -733,7 +746,7 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase): """ Verify that non-staff users cannot access `check_users_in_cohort`. """ - cohort = CohortFactory.create(course_id=self.course.id, users=[]) + cohort = CohortFactory(course_id=self.course.id, users=[]) self._verify_non_staff_cannot_access( remove_user_from_cohort, "POST", @@ -744,7 +757,7 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase): """ Verify that we get an error message when omitting a username. """ - cohort = CohortFactory.create(course_id=self.course.id, users=[]) + cohort = CohortFactory(course_id=self.course.id, users=[]) response_dict = self.request_remove_user_from_cohort(None, cohort) self.verify_removed_user_from_cohort( None, @@ -759,7 +772,7 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase): does not exist. """ username = "bogus" - cohort = CohortFactory.create(course_id=self.course.id, users=[]) + cohort = CohortFactory(course_id=self.course.id, users=[]) response_dict = self.request_remove_user_from_cohort( username, cohort @@ -776,8 +789,8 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase): Verify that we can "remove" a user from a cohort even if they are not a member of that cohort. """ - user = UserFactory.create() - cohort = CohortFactory.create(course_id=self.course.id, users=[]) + user = UserFactory() + cohort = CohortFactory(course_id=self.course.id, users=[]) response_dict = self.request_remove_user_from_cohort(user.username, cohort) self.verify_removed_user_from_cohort(user.username, response_dict, cohort) @@ -785,7 +798,7 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase): """ Verify that we can remove a user from a cohort. """ - user = UserFactory.create() - cohort = CohortFactory.create(course_id=self.course.id, users=[user]) + user = UserFactory() + cohort = CohortFactory(course_id=self.course.id, users=[user]) response_dict = self.request_remove_user_from_cohort(user.username, cohort) self.verify_removed_user_from_cohort(user.username, response_dict, cohort) diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py index 5f670f1f8303a29597eae0cd9d701b5e9566602e..4326a839c921b10aaed5024a60a62f8153623615 100644 --- a/common/djangoapps/course_groups/views.py +++ b/common/djangoapps/course_groups/views.py @@ -48,8 +48,15 @@ def list_cohorts(request, course_key_string): course = get_course_with_access(request.user, 'staff', course_key) - all_cohorts = [{'name': c.name, 'id': c.id, 'user_count': c.users.count()} - for c in cohorts.get_course_cohorts(course)] + all_cohorts = [ + { + 'name': c.name, + 'id': c.id, + 'user_count': c.users.count(), + 'assignment_type': cohorts.CohortAssignmentType.get(c, course) + } + for c in cohorts.get_course_cohorts(course) + ] return json_http_response({'success': True, 'cohorts': all_cohorts}) diff --git a/common/static/js/spec/string_utils_spec.js b/common/static/js/spec/string_utils_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f1b50f7091edfc531509fe0332958629c626992b --- /dev/null +++ b/common/static/js/spec/string_utils_spec.js @@ -0,0 +1,15 @@ +describe('interpolate_ntext', function () { + it('replaces placeholder values', function () { + expect(interpolate_ntext('contains {count} student', 'contains {count} students', 1, {count: 1})). + toBe('contains 1 student'); + expect(interpolate_ntext('contains {count} student', 'contains {count} students', 5, {count: 2})). + toBe('contains 2 students'); + }); +}); + +describe('interpolate_text', function () { + it('replaces placeholder values', function () { + expect(interpolate_text('contains {adjective} students', {adjective: 'awesome'})). + toBe('contains awesome students'); + }); +}); diff --git a/cms/static/js/spec_helpers/create_sinon.js b/common/static/js/spec_helpers/ajax_helpers.js similarity index 73% rename from cms/static/js/spec_helpers/create_sinon.js rename to common/static/js/spec_helpers/ajax_helpers.js index fcaa21c585d2541cad53c7f55bb7c161973bb530..0b2769be57080f2268840e08b52c7f03debe4eb2 100644 --- a/cms/static/js/spec_helpers/create_sinon.js +++ b/common/static/js/spec_helpers/ajax_helpers.js @@ -1,5 +1,6 @@ -define(["sinon", "underscore"], function(sinon, _) { - var fakeServer, fakeRequests, expectJsonRequest, respondWithJson, respondWithError, respondToDelete; +define(['sinon', 'underscore'], function(sinon, _) { + var fakeServer, fakeRequests, expectRequest, expectJsonRequest, + respondWithJson, respondWithError, respondToDelete; /* These utility methods are used by Jasmine tests to create a mock server or * get reference to mock requests. In either case, the cleanup (restore) is done with @@ -45,6 +46,17 @@ define(["sinon", "underscore"], function(sinon, _) { return requests; }; + expectRequest = function(requests, method, url, body, requestIndex) { + var request; + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + request = requests[requestIndex]; + expect(request.url).toEqual(url); + expect(request.method).toEqual(method); + expect(request.requestBody).toEqual(body); + }; + expectJsonRequest = function(requests, method, url, jsonRequest, requestIndex) { var request; if (_.isUndefined(requestIndex)) { @@ -61,7 +73,7 @@ define(["sinon", "underscore"], function(sinon, _) { requestIndex = requests.length - 1; } requests[requestIndex].respond(200, - { "Content-Type": "application/json" }, + { 'Content-Type': 'application/json' }, JSON.stringify(jsonResponse)); }; @@ -70,7 +82,7 @@ define(["sinon", "underscore"], function(sinon, _) { requestIndex = requests.length - 1; } requests[requestIndex].respond(500, - { "Content-Type": "application/json" }, + { 'Content-Type': 'application/json' }, JSON.stringify({ })); }; @@ -79,15 +91,16 @@ define(["sinon", "underscore"], function(sinon, _) { requestIndex = requests.length - 1; } requests[requestIndex].respond(204, - { "Content-Type": "application/json" }); + { 'Content-Type': 'application/json' }); }; return { - "server": fakeServer, - "requests": fakeRequests, - "expectJsonRequest": expectJsonRequest, - "respondWithJson": respondWithJson, - "respondWithError": respondWithError, - "respondToDelete": respondToDelete + 'server': fakeServer, + 'requests': fakeRequests, + 'expectRequest': expectRequest, + 'expectJsonRequest': expectJsonRequest, + 'respondWithJson': respondWithJson, + 'respondWithError': respondWithError, + 'respondToDelete': respondToDelete }; }); diff --git a/common/static/js/spec_helpers/template_helpers.js b/common/static/js/spec_helpers/template_helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..5f9dd317d8fe3801a0eaa6eb25e3a78fde7ab4da --- /dev/null +++ b/common/static/js/spec_helpers/template_helpers.js @@ -0,0 +1,43 @@ +/** + * Provides helper methods for invoking Studio modal windows in Jasmine tests. + */ +define(["jquery", "underscore"], + function($, _) { + var installTemplate, installTemplates; + + installTemplate = function(templateFile, isFirst, templateId) { + var template = readFixtures(templateFile + '.underscore'), + templateName = templateFile, + slashIndex = _.lastIndexOf(templateName, "/"); + if (slashIndex >= 0) { + templateName = templateFile.substring(slashIndex + 1); + } + if (!templateId) { + templateId = templateName + '-tpl'; + } + + if (isFirst) { + setFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template)); + } else { + appendSetFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template)); + } + }; + + installTemplates = function(templateNames, isFirst) { + if (!$.isArray(templateNames)) { + templateNames = [templateNames]; + } + + $.each(templateNames, function(index, templateName) { + installTemplate(templateName, isFirst); + if (isFirst) { + isFirst = false; + } + }); + }; + + return { + 'installTemplate': installTemplate, + 'installTemplates': installTemplates + }; + }); diff --git a/common/static/js/src/string_utils.js b/common/static/js/src/string_utils.js new file mode 100644 index 0000000000000000000000000000000000000000..232b36f02e5548fc58077ad89336f2abb4359b4e --- /dev/null +++ b/common/static/js/src/string_utils.js @@ -0,0 +1,48 @@ +// String utility methods. +(function(_) { + /** + * Takes both a singular and plural version of a templatized string and plugs + * in the placeholder values. Assumes that internationalization has already been + * handled if necessary. Note that for text that needs to be internationalized, + * normally ngettext and interpolate_text would be used instead of this method. + * + * Example usage: + * interpolate_ntext('(contains {count} student)', '(contains {count} students)', + * expectedCount, {count: expectedCount} + * ) + * + * @param singular the singular version of the templatized text + * @param plural the plural version of the templatized text + * @param count the count on which to base singular vs. plural text. Since this method is only + * intended for text that does not need to be passed through ngettext for internationalization, + * the simplistic English rule of count == 1 indicating singular is used. + * @param values the templatized dictionary values + * @returns the text with placeholder values filled in + */ + var interpolate_ntext = function (singular, plural, count, values) { + var text = count === 1 ? singular : plural; + return _.template(text, values, {interpolate: /\{(.+?)\}/g}); + }; + this.interpolate_ntext = interpolate_ntext; + + /** + * Takes a templatized string and plugs in the placeholder values. Assumes that internationalization + * has already been handled if necessary. + * + * Example usages: + * interpolate_text('{title} ({count})', {title: expectedTitle, count: expectedCount} + * interpolate_text( + * ngettext("{numUsersAdded} student has been added to this cohort group", + * "{numUsersAdded} students have been added to this cohort group", numUsersAdded), + * {numUsersAdded: numUsersAdded} + * ); + * + * @param text the templatized text + * @param values the templatized dictionary values + * @returns the text with placeholder values filled in + */ + var interpolate_text = function (text, values) { + return _.template(text, values, {interpolate: /\{(.+?)\}/g}); + }; + this.interpolate_text = interpolate_text; +}).call(this, _); diff --git a/common/static/js/vendor/jquery.event.drag-2.2.js b/common/static/js/vendor/jquery.event.drag-2.2.js index 1cda0e21669e9f32d73d112377701881fc9ae6a2..c446b857e7281418d58813234c89122e630b6b44 100644 --- a/common/static/js/vendor/jquery.event.drag-2.2.js +++ b/common/static/js/vendor/jquery.event.drag-2.2.js @@ -178,7 +178,7 @@ drag = $special.drag = { case !dd.dragging && 'touchmove': event.preventDefault(); case !dd.dragging && 'mousemove': - // drag tolerance, x² + y² = distance² + // drag tolerance, x^2 + y^2 = distance^2 if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) ) break; // distance tolerance not reached event.target = dd.target; // force target from "mousedown" event (fix distance issue) diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py new file mode 100644 index 0000000000000000000000000000000000000000..1ed9b1d8ce4cc5d61fc6b7162eb0fb654f1f75ea --- /dev/null +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +""" +Instructor (2) dashboard page. +""" + +from bok_choy.page_object import PageObject +from .course_page import CoursePage + + +class InstructorDashboardPage(CoursePage): + """ + Instructor dashboard, where course staff can manage a course. + """ + url_path = "instructor" + + def is_browser_on_page(self): + return self.q(css='div.instructor-dashboard-wrapper-2').present + + def select_membership(self): + """ + Selects the membership tab and returns the MembershipSection + """ + self.q(css='a[data-section=membership]').first.click() + membership_section = MembershipPage(self.browser) + membership_section.wait_for_page() + return membership_section + + +class MembershipPage(PageObject): + """ + Membership section of the Instructor dashboard. + """ + url = None + + def is_browser_on_page(self): + return self.q(css='a[data-section=membership].active-section').present + + def _get_cohort_options(self): + """ + Returns the available options in the cohort dropdown, including the initial "Select a cohort group". + """ + return self.q(css=".cohort-management #cohort-select option") + + def _cohort_name(self, label): + """ + Returns the name of the cohort with the count information excluded. + """ + return label.split(' (')[0] + + def _cohort_count(self, label): + """ + Returns the count for the cohort (as specified in the label in the selector). + """ + return int(label.split(' (')[1].split(')')[0]) + + def get_cohorts(self): + """ + Returns, as a list, the names of the available cohorts in the drop-down, filtering out "Select a cohort group". + """ + return [ + self._cohort_name(opt.text) + for opt in self._get_cohort_options().filter(lambda el: el.get_attribute('value') != "") + ] + + def get_selected_cohort(self): + """ + Returns the name of the selected cohort. + """ + return self._cohort_name( + self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0] + ) + + def get_selected_cohort_count(self): + """ + Returns the number of users in the selected cohort. + """ + return self._cohort_count( + self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0] + ) + + def select_cohort(self, cohort_name): + """ + Selects the given cohort in the drop-down. + """ + self.q(css=".cohort-management #cohort-select option").filter( + lambda el: self._cohort_name(el.text) == cohort_name + ).first.click() + + def add_cohort(self, cohort_name): + """ + Adds a new manual cohort with the specified name. + """ + self.q(css="div.cohort-management-nav .action-create").first.click() + textinput = self.q(css="#cohort-create-name").results[0] + textinput.send_keys(cohort_name) + self.q(css="div.form-actions .action-save").first.click() + + def get_cohort_group_setup(self): + """ + Returns the description of the current cohort + """ + return self.q(css='.cohort-management-group-setup .setup-value').first.text[0] + + def select_edit_settings(self): + self.q(css=".action-edit").first.click() + + def add_students_to_selected_cohort(self, users): + """ + Adds a list of users (either usernames or email addresses) to the currently selected cohort. + """ + textinput = self.q(css="#cohort-management-group-add-students").results[0] + for user in users: + textinput.send_keys(user) + textinput.send_keys(",") + self.q(css="div.cohort-management-group-add .action-primary").first.click() + + def get_cohort_student_input_field_value(self): + """ + Returns the contents of the input field where students can be added to a cohort. + """ + return self.q(css="#cohort-management-group-add-students").results[0].get_attribute("value") + + def _get_cohort_messages(self, type): + """ + Returns array of messages for given type. + """ + message_title = self.q(css="div.cohort-management-group-add .cohort-" + type + " .message-title") + if len(message_title.results) == 0: + return [] + messages = [message_title.first.text[0]] + details = self.q(css="div.cohort-management-group-add .cohort-" + type + " .summary-item").results + for detail in details: + messages.append(detail.text) + return messages + + def get_cohort_confirmation_messages(self): + """ + Returns an array of messages present in the confirmation area of the cohort management UI. + The first entry in the array is the title. Any further entries are the details. + """ + return self._get_cohort_messages("confirmations") + + def get_cohort_error_messages(self): + """ + Returns an array of messages present in the error area of the cohort management UI. + The first entry in the array is the title. Any further entries are the details. + """ + return self._get_cohort_messages("errors") diff --git a/common/test/acceptance/tests/discussion/helpers.py b/common/test/acceptance/tests/discussion/helpers.py index f54f0c01367ca061e5a23c4868f1a3264b31f83d..e8aad4a58e5d55b60321610e653a035d1c1d8f94 100644 --- a/common/test/acceptance/tests/discussion/helpers.py +++ b/common/test/acceptance/tests/discussion/helpers.py @@ -9,6 +9,7 @@ from ...fixtures.discussion import ( Thread, Response, ) +from ...fixtures import LMS_BASE_URL class BaseDiscussionMixin(object): @@ -29,3 +30,42 @@ class BaseDiscussionMixin(object): thread_fixture.addResponse(Response(id=str(i), body=str(i))) thread_fixture.push() self.setup_thread_page(thread_id) + + +class CohortTestMixin(object): + """ + Mixin for tests of cohorted courses + """ + def setup_cohort_config(self, course_fixture, auto_cohort_groups=None): + """ + Sets up the course to use cohorting with the given list of auto_cohort_groups. + If auto_cohort_groups is None, no auto cohort groups are set. + """ + course_fixture._update_xblock(course_fixture._course_location, { + "metadata": { + u"cohort_config": { + "auto_cohort_groups": auto_cohort_groups or [], + "cohorted_discussions": [], + "cohorted": True + }, + }, + }) + + def add_manual_cohort(self, course_fixture, cohort_name): + """ + Adds a cohort group by name, returning the ID for the group. + """ + url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/add' + data = {"name": cohort_name} + response = course_fixture.session.post(url, data=data, headers=course_fixture.headers) + self.assertTrue(response.ok, "Failed to create cohort") + return response.json()['cohort']['id'] + + def add_user_to_cohort(self, course_fixture, username, cohort_id): + """ + Adds a user to the specified cohort group. + """ + url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + "/cohorts/{}/add".format(cohort_id) + data = {"users": username} + response = course_fixture.session.post(url, data=data, headers=course_fixture.headers) + self.assertTrue(response.ok, "Failed to add user to cohort") diff --git a/common/test/acceptance/tests/discussion/test_cohort_management.py b/common/test/acceptance/tests/discussion/test_cohort_management.py new file mode 100644 index 0000000000000000000000000000000000000000..f67af694dd61558f127727d0fdb98a3c7fc2e1f0 --- /dev/null +++ b/common/test/acceptance/tests/discussion/test_cohort_management.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +""" +End-to-end tests related to the cohort management on the LMS Instructor Dashboard +""" + +from bok_choy.promise import EmptyPromise +from .helpers import CohortTestMixin +from ..helpers import UniqueCourseTest +from ...fixtures.course import CourseFixture +from ...pages.lms.auto_auth import AutoAuthPage +from ...pages.lms.instructor_dashboard import InstructorDashboardPage +from ...pages.studio.settings_advanced import AdvancedSettingsPage + +import uuid + + +class CohortConfigurationTest(UniqueCourseTest, CohortTestMixin): + """ + Tests for cohort management on the LMS Instructor Dashboard + """ + + def setUp(self): + """ + Set up a cohorted course + """ + super(CohortConfigurationTest, self).setUp() + + # create course with cohorts + self.manual_cohort_name = "ManualCohort1" + self.auto_cohort_name = "AutoCohort1" + self.course_fixture = CourseFixture(**self.course_info).install() + self.setup_cohort_config(self.course_fixture, auto_cohort_groups=[self.auto_cohort_name]) + self.manual_cohort_id = self.add_manual_cohort(self.course_fixture, self.manual_cohort_name) + + # create a non-instructor who will be registered for the course and in the manual cohort. + self.student_name = "student_user" + self.student_id = AutoAuthPage( + self.browser, username=self.student_name, course_id=self.course_id, staff=False + ).visit().get_user_id() + self.add_user_to_cohort(self.course_fixture, self.student_name, self.manual_cohort_id) + + # login as an instructor + self.instructor_name = "instructor_user" + self.instructor_id = AutoAuthPage( + self.browser, username=self.instructor_name, course_id=self.course_id, staff=True + ).visit().get_user_id() + + # go to the membership page on the instructor dashboard + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) + instructor_dashboard_page.visit() + self.membership_page = instructor_dashboard_page.select_membership() + + def verify_cohort_description(self, cohort_name, expected_description): + """ + Selects the cohort with the given name and verifies the expected description is presented. + """ + self.membership_page.select_cohort(cohort_name) + self.assertEquals(self.membership_page.get_selected_cohort(), cohort_name) + self.assertIn(expected_description, self.membership_page.get_cohort_group_setup()) + + def test_cohort_description(self): + """ + Scenario: the cohort configuration management in the instructor dashboard specifies whether + students are automatically or manually assigned to specific cohorts. + + Given I have a course with a manual cohort and an automatic cohort defined + When I view the manual cohort in the instructor dashboard + There is text specifying that students are only added to the cohort manually + And when I vew the automatic cohort in the instructor dashboard + There is text specifying that students are automatically added to the cohort + """ + self.verify_cohort_description( + self.manual_cohort_name, + 'Students are added to this group only when you provide their email addresses or usernames on this page', + ) + self.verify_cohort_description( + self.auto_cohort_name, + 'Students are added to this group automatically', + ) + + def test_link_to_studio(self): + """ + Scenario: a link is present from the cohort configuration in the instructor dashboard + to the Studio Advanced Settings. + + Given I have a course with a cohort defined + When I view the cohort in the LMS instructor dashboard + There is a link to take me to the Studio Advanced Settings for the course + """ + self.membership_page.select_cohort(self.manual_cohort_name) + self.membership_page.select_edit_settings() + advanced_settings_page = AdvancedSettingsPage( + self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'] + ) + advanced_settings_page.wait_for_page() + + def test_add_students_to_cohort_success(self): + """ + Scenario: When students are added to a cohort, the appropriate notification is shown. + + Given I have a course with two cohorts + And there is a user in one cohort + And there is a user in neither cohort + When I add the two users to the cohort that initially had no users + Then there are 2 users in total in the cohort + And I get a notification that 2 users have been added to the cohort + And I get a notification that 1 user was moved from the other cohort + And the user input field is empty + """ + self.membership_page.select_cohort(self.auto_cohort_name) + self.assertEqual(0, self.membership_page.get_selected_cohort_count()) + self.membership_page.add_students_to_selected_cohort([self.student_name, self.instructor_name]) + # Wait for the number of users in the cohort to change, indicating that the add operation is complete. + EmptyPromise( + lambda: 2 == self.membership_page.get_selected_cohort_count(), 'Waiting for added students' + ).fulfill() + confirmation_messages = self.membership_page.get_cohort_confirmation_messages() + self.assertEqual(2, len(confirmation_messages)) + self.assertEqual("2 students have been added to this cohort group", confirmation_messages[0]) + self.assertEqual("1 student was removed from " + self.manual_cohort_name, confirmation_messages[1]) + self.assertEqual("", self.membership_page.get_cohort_student_input_field_value()) + + def test_add_students_to_cohort_failure(self): + """ + Scenario: When errors occur when adding students to a cohort, the appropriate notification is shown. + + Given I have a course with a cohort and a user already in it + When I add the user already in a cohort to that same cohort + And I add a non-existing user to that cohort + Then there is no change in the number of students in the cohort + And I get a notification that one user was already in the cohort + And I get a notification that one user is unknown + And the user input field still contains the incorrect email addresses + """ + self.membership_page.select_cohort(self.manual_cohort_name) + self.assertEqual(1, self.membership_page.get_selected_cohort_count()) + self.membership_page.add_students_to_selected_cohort([self.student_name, "unknown_user"]) + # Wait for notification messages to appear, indicating that the add operation is complete. + EmptyPromise( + lambda: 2 == len(self.membership_page.get_cohort_confirmation_messages()), 'Waiting for notification' + ).fulfill() + self.assertEqual(1, self.membership_page.get_selected_cohort_count()) + + confirmation_messages = self.membership_page.get_cohort_confirmation_messages() + self.assertEqual(2, len(confirmation_messages)) + self.assertEqual("0 students have been added to this cohort group", confirmation_messages[0]) + self.assertEqual("1 student was already in the cohort group", confirmation_messages[1]) + + error_messages = self.membership_page.get_cohort_error_messages() + self.assertEqual(2, len(error_messages)) + self.assertEqual("There was an error when trying to add students:", error_messages[0]) + self.assertEqual("Unknown user: unknown_user", error_messages[1]) + self.assertEqual( + self.student_name + ",unknown_user,", + self.membership_page.get_cohort_student_input_field_value() + ) + + def test_add_new_cohort(self): + """ + Scenario: A new manual cohort can be created, and a student assigned to it. + + Given I have a course with a user in the course + When I add a new manual cohort to the course via the LMS instructor dashboard + Then the new cohort is displayed and has no users in it + And when I add the user to the new cohort + Then the cohort has 1 user + """ + new_cohort = str(uuid.uuid4().get_hex()[0:20]) + self.assertFalse(new_cohort in self.membership_page.get_cohorts()) + self.membership_page.add_cohort(new_cohort) + # After adding the cohort, it should automatically be selected + EmptyPromise( + lambda: new_cohort == self.membership_page.get_selected_cohort(), "Waiting for new cohort to appear" + ).fulfill() + self.assertEqual(0, self.membership_page.get_selected_cohort_count()) + self.membership_page.add_students_to_selected_cohort([self.instructor_name]) + # Wait for the number of users in the cohort to change, indicating that the add operation is complete. + EmptyPromise( + lambda: 1 == self.membership_page.get_selected_cohort_count(), 'Waiting for student to be added' + ).fulfill() diff --git a/common/test/acceptance/tests/discussion/test_cohorts.py b/common/test/acceptance/tests/discussion/test_cohorts.py index 047978342a7593fd4d6f22780cfe312df10a44c9..dce9e509943f74a86033869842e7d4639e58da6c 100644 --- a/common/test/acceptance/tests/discussion/test_cohorts.py +++ b/common/test/acceptance/tests/discussion/test_cohorts.py @@ -3,11 +3,11 @@ Tests related to the cohorting feature. """ from uuid import uuid4 -from helpers import BaseDiscussionMixin -from ...pages.lms.auto_auth import AutoAuthPage +from .helpers import BaseDiscussionMixin +from .helpers import CohortTestMixin from ..helpers import UniqueCourseTest +from ...pages.lms.auto_auth import AutoAuthPage from ...fixtures.course import (CourseFixture, XBlockFixtureDesc) -from ...fixtures import LMS_BASE_URL from ...pages.lms.discussion import (DiscussionTabSingleThreadPage, InlineDiscussionThreadPage, InlineDiscussionPage) from ...pages.lms.courseware import CoursewarePage @@ -17,7 +17,7 @@ from nose.plugins.attrib import attr class NonCohortedDiscussionTestMixin(BaseDiscussionMixin): """ - Mixin for tests of non-cohorted courses. + Mixin for tests of discussion in non-cohorted courses. """ def setup_cohorts(self): """ @@ -30,36 +30,17 @@ class NonCohortedDiscussionTestMixin(BaseDiscussionMixin): self.assertEquals(self.thread_page.get_group_visibility_label(), "This post is visible to everyone.") -class CohortedDiscussionTestMixin(BaseDiscussionMixin): +class CohortedDiscussionTestMixin(BaseDiscussionMixin, CohortTestMixin): """ - Mixin for tests of cohorted courses. + Mixin for tests of discussion in cohorted courses. """ - def add_cohort(self, name): - """ - Adds a cohort group by name, returning the ID for the group. - """ - url = LMS_BASE_URL + "/courses/" + self.course_fixture._course_key + '/cohorts/add' - data = {"name": name} - response = self.course_fixture.session.post(url, data=data, headers=self.course_fixture.headers) - self.assertTrue(response.ok, "Failed to create cohort") - return response.json()['cohort']['id'] - def setup_cohorts(self): """ Sets up the course to use cohorting with a single defined cohort group. """ - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - u"cohort_config": { - "auto_cohort_groups": [], - "auto_cohort": False, - "cohorted_discussions": [], - "cohorted": True - }, - }, - }) + self.setup_cohort_config(self.course_fixture) self.cohort_1_name = "Cohort Group 1" - self.cohort_1_id = self.add_cohort(self.cohort_1_name) + self.cohort_1_id = self.add_manual_cohort(self.course_fixture, self.cohort_1_name) def test_cohort_visibility_label(self): # Must be moderator to view content in a cohort other than your own diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py index 70b2291ddbaea16e94346120c9103eb05cacb07f..c4dae8872a95d2c63e3a8671f33a22e15869a190 100644 --- a/common/test/acceptance/tests/discussion/test_discussion.py +++ b/common/test/acceptance/tests/discussion/test_discussion.py @@ -29,7 +29,7 @@ from ...fixtures.discussion import ( SearchResult, ) -from helpers import BaseDiscussionMixin +from .helpers import BaseDiscussionMixin class DiscussionResponsePaginationTestMixin(BaseDiscussionMixin): diff --git a/common/test/acceptance/tests/lms/__init__.py b/common/test/acceptance/tests/lms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/common/test/acceptance/tests/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py similarity index 80% rename from common/test/acceptance/tests/test_lms.py rename to common/test/acceptance/tests/lms/test_lms.py index f98788d5843de6179ef28316b1c87c6c338592bb..54f9aae3b50f6ec7c759e5d40aacee43bc5105d5 100644 --- a/common/test/acceptance/tests/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -1,26 +1,25 @@ # -*- coding: utf-8 -*- """ -E2E tests for the LMS. +End-to-end tests for the LMS. """ from textwrap import dedent from unittest import skip from bok_choy.web_app_test import WebAppTest -from .helpers import UniqueCourseTest, load_data_str -from ..pages.lms.auto_auth import AutoAuthPage -from ..pages.lms.find_courses import FindCoursesPage -from ..pages.lms.course_about import CourseAboutPage -from ..pages.lms.course_info import CourseInfoPage -from ..pages.lms.tab_nav import TabNavPage -from ..pages.lms.course_nav import CourseNavPage -from ..pages.lms.progress import ProgressPage -from ..pages.lms.dashboard import DashboardPage -from ..pages.lms.problem import ProblemPage -from ..pages.lms.video.video import VideoPage -from ..pages.xblock.acid import AcidView -from ..pages.lms.courseware import CoursewarePage -from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc +from ..helpers import UniqueCourseTest, load_data_str +from ...pages.lms.auto_auth import AutoAuthPage +from ...pages.lms.find_courses import FindCoursesPage +from ...pages.lms.course_about import CourseAboutPage +from ...pages.lms.course_info import CourseInfoPage +from ...pages.lms.tab_nav import TabNavPage +from ...pages.lms.course_nav import CourseNavPage +from ...pages.lms.progress import ProgressPage +from ...pages.lms.dashboard import DashboardPage +from ...pages.lms.problem import ProblemPage +from ...pages.lms.video.video import VideoPage +from ...pages.lms.courseware import CoursewarePage +from ...fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc class RegistrationTest(UniqueCourseTest): @@ -267,7 +266,7 @@ class VideoTest(UniqueCourseTest): XBlockFixtureDesc('sequential', 'Test Subsection').add_children( XBlockFixtureDesc('vertical', 'Test Unit').add_children( XBlockFixtureDesc('video', 'Video') - )))).install() + )))).install() # Auto-auth register for the course AutoAuthPage(self.browser, course_id=self.course_id).visit() @@ -311,115 +310,6 @@ class VideoTest(UniqueCourseTest): self.assertGreaterEqual(self.video.duration, self.video.elapsed_time) -class XBlockAcidBase(UniqueCourseTest): - """ - Base class for tests that verify that XBlock integration is working correctly - """ - __test__ = False - - def setUp(self): - """ - Create a unique identifier for the course used in this test. - """ - # Ensure that the superclass sets up - super(XBlockAcidBase, self).setUp() - - self.setup_fixtures() - - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - self.course_info_page = CourseInfoPage(self.browser, self.course_id) - self.tab_nav = TabNavPage(self.browser) - - def validate_acid_block_view(self, acid_block): - """ - Verify that the LMS view for the Acid Block is correct - """ - self.assertTrue(acid_block.init_fn_passed) - self.assertTrue(acid_block.resource_url_passed) - self.assertTrue(acid_block.scope_passed('user_state')) - self.assertTrue(acid_block.scope_passed('user_state_summary')) - self.assertTrue(acid_block.scope_passed('preferences')) - self.assertTrue(acid_block.scope_passed('user_info')) - - def test_acid_block(self): - """ - Verify that all expected acid block tests pass in the lms. - """ - - self.course_info_page.visit() - self.tab_nav.go_to_tab('Courseware') - - acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]') - self.validate_acid_block_view(acid_block) - - -class XBlockAcidNoChildTest(XBlockAcidBase): - """ - Tests of an AcidBlock with no children - """ - __test__ = True - - def setup_fixtures(self): - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid', 'Acid Block') - ) - ) - ) - ).install() - - @skip('Flakey test, TE-401') - def test_acid_block(self): - super(XBlockAcidNoChildTest, self).test_acid_block() - - -class XBlockAcidChildTest(XBlockAcidBase): - """ - Tests of an AcidBlock with children - """ - __test__ = True - - def setup_fixtures(self): - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid_parent', 'Acid Parent Block').add_children( - XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}), - XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}), - XBlockFixtureDesc('html', 'Html Child', data="<html>Contents</html>"), - ) - ) - ) - ) - ).install() - - def validate_acid_block_view(self, acid_block): - super(XBlockAcidChildTest, self).validate_acid_block_view() - self.assertTrue(acid_block.child_tests_passed) - - @skip('This will fail until we fix support of children in pure XBlocks') - def test_acid_block(self): - super(XBlockAcidChildTest, self).test_acid_block() - - class VisibleToStaffOnlyTest(UniqueCourseTest): """ Tests that content with visible_to_staff_only set to True cannot be viewed by students. @@ -574,7 +464,8 @@ class ProblemExecutionTest(UniqueCourseTest): course_fix.add_children( XBlockFixtureDesc('chapter', 'Test Section').add_children( XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('problem', 'Python Problem', data=dedent("""\ + XBlockFixtureDesc('problem', 'Python Problem', data=dedent( + """\ <problem> <script type="loncapa/python"> from number_helpers import seventeen, fortytwo @@ -594,7 +485,7 @@ class ProblemExecutionTest(UniqueCourseTest): </customresponse> </problem> """ - )), + )) ) ) ).install() diff --git a/common/test/acceptance/tests/lms/test_lms_acid_xblock.py b/common/test/acceptance/tests/lms/test_lms_acid_xblock.py new file mode 100644 index 0000000000000000000000000000000000000000..4d3958d4007774591310fab4438080023bb0baa8 --- /dev/null +++ b/common/test/acceptance/tests/lms/test_lms_acid_xblock.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" +End-to-end tests for the LMS. +""" + +from unittest import skip + +from ..helpers import UniqueCourseTest +from ...pages.lms.auto_auth import AutoAuthPage +from ...pages.lms.course_info import CourseInfoPage +from ...pages.lms.tab_nav import TabNavPage +from ...pages.xblock.acid import AcidView +from ...fixtures.course import CourseFixture, XBlockFixtureDesc + + +class XBlockAcidBase(UniqueCourseTest): + """ + Base class for tests that verify that XBlock integration is working correctly + """ + __test__ = False + + def setUp(self): + """ + Create a unique identifier for the course used in this test. + """ + # Ensure that the superclass sets up + super(XBlockAcidBase, self).setUp() + + self.setup_fixtures() + + AutoAuthPage(self.browser, course_id=self.course_id).visit() + + self.course_info_page = CourseInfoPage(self.browser, self.course_id) + self.tab_nav = TabNavPage(self.browser) + + def validate_acid_block_view(self, acid_block): + """ + Verify that the LMS view for the Acid Block is correct + """ + self.assertTrue(acid_block.init_fn_passed) + self.assertTrue(acid_block.resource_url_passed) + self.assertTrue(acid_block.scope_passed('user_state')) + self.assertTrue(acid_block.scope_passed('user_state_summary')) + self.assertTrue(acid_block.scope_passed('preferences')) + self.assertTrue(acid_block.scope_passed('user_info')) + + def test_acid_block(self): + """ + Verify that all expected acid block tests pass in the lms. + """ + + self.course_info_page.visit() + self.tab_nav.go_to_tab('Courseware') + + acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]') + self.validate_acid_block_view(acid_block) + + +class XBlockAcidNoChildTest(XBlockAcidBase): + """ + Tests of an AcidBlock with no children + """ + __test__ = True + + def setup_fixtures(self): + course_fix = CourseFixture( + self.course_info['org'], + self.course_info['number'], + self.course_info['run'], + self.course_info['display_name'] + ) + + course_fix.add_children( + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Unit').add_children( + XBlockFixtureDesc('acid', 'Acid Block') + ) + ) + ) + ).install() + + @skip('Flakey test, TE-401') + def test_acid_block(self): + super(XBlockAcidNoChildTest, self).test_acid_block() + + +class XBlockAcidChildTest(XBlockAcidBase): + """ + Tests of an AcidBlock with children + """ + __test__ = True + + def setup_fixtures(self): + course_fix = CourseFixture( + self.course_info['org'], + self.course_info['number'], + self.course_info['run'], + self.course_info['display_name'] + ) + + course_fix.add_children( + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Unit').add_children( + XBlockFixtureDesc('acid_parent', 'Acid Parent Block').add_children( + XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}), + XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}), + XBlockFixtureDesc('html', 'Html Child', data="<html>Contents</html>"), + ) + ) + ) + ) + ).install() + + def validate_acid_block_view(self, acid_block): + super(XBlockAcidChildTest, self).validate_acid_block_view() + self.assertTrue(acid_block.child_tests_passed) + + @skip('This will fail until we fix support of children in pure XBlocks') + def test_acid_block(self): + super(XBlockAcidChildTest, self).test_acid_block() diff --git a/common/test/acceptance/tests/test_courseware.py b/common/test/acceptance/tests/lms/test_lms_courseware.py similarity index 89% rename from common/test/acceptance/tests/test_courseware.py rename to common/test/acceptance/tests/lms/test_lms_courseware.py index 2d951ca2871e26db33a1905da44083e5c18ec7c1..ae5eb031fba5ffe634cba24a0ce65cdea734766f 100644 --- a/common/test/acceptance/tests/test_courseware.py +++ b/common/test/acceptance/tests/lms/test_lms_courseware.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- """ -E2E tests for the LMS. +End-to-end tests for the LMS. """ import time -from .helpers import UniqueCourseTest -from ..pages.studio.auto_auth import AutoAuthPage -from ..pages.studio.overview import CourseOutlinePage -from ..pages.lms.courseware import CoursewarePage -from ..pages.lms.problem import ProblemPage -from ..pages.common.logout import LogoutPage -from ..fixtures.course import CourseFixture, XBlockFixtureDesc +from ..helpers import UniqueCourseTest +from ...pages.studio.auto_auth import AutoAuthPage +from ...pages.studio.overview import CourseOutlinePage +from ...pages.lms.courseware import CoursewarePage +from ...pages.lms.problem import ProblemPage +from ...pages.common.logout import LogoutPage +from ...fixtures.course import CourseFixture, XBlockFixtureDesc class CoursewareTest(UniqueCourseTest): @@ -77,7 +77,6 @@ class CoursewareTest(UniqueCourseTest): AutoAuthPage(self.browser, username=username, email=email, course_id=self.course_id, staff=staff).visit() - def test_courseware(self): """ Test courseware if recent visited subsection become unpublished. diff --git a/common/test/acceptance/tests/test_matlab_problem.py b/common/test/acceptance/tests/lms/test_lms_matlab_problem.py similarity index 92% rename from common/test/acceptance/tests/test_matlab_problem.py rename to common/test/acceptance/tests/lms/test_lms_matlab_problem.py index d2e7e9d90a00419e8eb62c87b238d62e8678dd73..f62d021bb51b406e1f9bb002a659700ec25d2bcc 100644 --- a/common/test/acceptance/tests/test_matlab_problem.py +++ b/common/test/acceptance/tests/lms/test_lms_matlab_problem.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- """ -E2E tests for the LMS. +End-to-end tests for the LMS. """ import time -from .helpers import UniqueCourseTest -from ..pages.studio.auto_auth import AutoAuthPage -from ..pages.lms.courseware import CoursewarePage -from ..pages.lms.matlab_problem import MatlabProblemPage -from ..fixtures.course import CourseFixture, XBlockFixtureDesc -from ..fixtures.xqueue import XQueueResponseFixture +from ..helpers import UniqueCourseTest +from ...pages.studio.auto_auth import AutoAuthPage +from ...pages.lms.courseware import CoursewarePage +from ...pages.lms.matlab_problem import MatlabProblemPage +from ...fixtures.course import CourseFixture, XBlockFixtureDesc +from ...fixtures.xqueue import XQueueResponseFixture from textwrap import dedent diff --git a/common/test/acceptance/tests/test_staff_view.py b/common/test/acceptance/tests/lms/test_lms_staff_view.py similarity index 96% rename from common/test/acceptance/tests/test_staff_view.py rename to common/test/acceptance/tests/lms/test_lms_staff_view.py index 7187df18ba7e584243c43e935452476f686465d3..3d6bf77c094a1de3580c811f7e4f0186f1a5ebc2 100644 --- a/common/test/acceptance/tests/test_staff_view.py +++ b/common/test/acceptance/tests/lms/test_lms_staff_view.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """ -E2E tests for the LMS. +End-to-end tests for the LMS. """ -from .helpers import UniqueCourseTest -from ..pages.studio.auto_auth import AutoAuthPage -from ..pages.lms.courseware import CoursewarePage -from ..pages.lms.staff_view import StaffPage -from ..fixtures.course import CourseFixture, XBlockFixtureDesc +from ..helpers import UniqueCourseTest +from ...pages.studio.auto_auth import AutoAuthPage +from ...pages.lms.courseware import CoursewarePage +from ...pages.lms.staff_view import StaffPage +from ...fixtures.course import CourseFixture, XBlockFixtureDesc from textwrap import dedent diff --git a/docs/en_us/internal/testing.md b/docs/en_us/internal/testing.md index ec751be4b9c88022ce9a8931d3f7b923fc46e0e9..30b32a86631f70c46ecdd40463279fa0509be42f 100644 --- a/docs/en_us/internal/testing.md +++ b/docs/en_us/internal/testing.md @@ -209,6 +209,7 @@ We use Jasmine to run JavaScript unit tests. To run all the JavaScript tests: To run a specific set of JavaScript tests and print the results to the console: paver test_js_run -s lms + paver test_js_run -s lms-coffee paver test_js_run -s cms paver test_js_run -s cms-squire paver test_js_run -s xmodule @@ -217,6 +218,7 @@ To run a specific set of JavaScript tests and print the results to the console: To run JavaScript tests in your default browser: paver test_js_dev -s lms + paver test_js_dev -s lms-coffee paver test_js_dev -s cms paver test_js_dev -s cms-squire paver test_js_dev -s xmodule diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 0b31547cf518b4c822a7deba1a067541c7a50a51..af1959f20931806556eab9f51d38d63e83c20948 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -21,12 +21,11 @@ from django.conf import settings from lms.lib.xblock.runtime import quote_slashes from xmodule_modifiers import wrap_xblock from xmodule.html_module import HtmlDescriptor -from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xblock.field_data import DictFieldData from xblock.fields import ScopeIds from courseware.access import has_access -from courseware.courses import get_course_by_id, get_cms_course_link +from courseware.courses import get_course_by_id, get_studio_url from django_comment_client.utils import has_forum_access from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR from student.models import CourseEnrollment @@ -51,7 +50,6 @@ def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_key, depth=None) - is_studio_course = (modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml) access = { 'admin': request.user.is_staff, @@ -67,11 +65,11 @@ def instructor_dashboard_2(request, course_id): raise Http404() sections = [ - _section_course_info(course_key, access), - _section_membership(course_key, access), - _section_student_admin(course_key, access), - _section_data_download(course_key, access), - _section_analytics(course_key, access), + _section_course_info(course, access), + _section_membership(course, access), + _section_student_admin(course, access), + _section_data_download(course, access), + _section_analytics(course, access), ] #check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course @@ -85,19 +83,15 @@ def instructor_dashboard_2(request, course_id): # Gate access to course email by feature flag & by course-specific authorization if bulk_email_is_enabled_for_course(course_key): - sections.append(_section_send_email(course_key, access, course)) + sections.append(_section_send_email(course, access)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: - sections.append(_section_metrics(course_key, access)) + sections.append(_section_metrics(course, access)) # Gate access to Ecommerce tab if course_mode_has_price: - sections.append(_section_e_commerce(course_key, access)) - - studio_url = None - if is_studio_course: - studio_url = get_cms_course_link(course) + sections.append(_section_e_commerce(course, access)) enrollment_count = sections[0]['enrollment_count']['total'] disable_buttons = False @@ -117,7 +111,7 @@ def instructor_dashboard_2(request, course_id): context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': course_key.to_deprecated_string()}), - 'studio_url': studio_url, + 'studio_url': get_studio_url(course, 'course'), 'sections': sections, 'disable_buttons': disable_buttons, 'analytics_dashboard_message': analytics_dashboard_message @@ -139,8 +133,9 @@ section_display_name will be used to generate link titles in the nav bar. """ # pylint: disable=W0105 -def _section_e_commerce(course_key, access): +def _section_e_commerce(course, access): """ Provide data for the corresponding dashboard section """ + course_key = course.id coupons = Coupon.objects.filter(course_id=course_key).order_by('-is_active') total_amount = None course_price = None @@ -209,9 +204,9 @@ def set_course_mode_price(request, course_id): return HttpResponse(_("CourseMode price updated successfully")) -def _section_course_info(course_key, access): +def _section_course_info(course, access): """ Provide data for the corresponding dashboard section """ - course = get_course_by_id(course_key, depth=None) + course_key = course.id section_data = { 'section_key': 'course_info', @@ -240,8 +235,9 @@ def _section_course_info(course_key, access): return section_data -def _section_membership(course_key, access): +def _section_membership(course, access): """ Provide data for the corresponding dashboard section """ + course_key = course.id section_data = { 'section_key': 'membership', 'section_display_name': _('Membership'), @@ -253,12 +249,15 @@ def _section_membership(course_key, access): 'modify_access_url': reverse('modify_access', kwargs={'course_id': course_key.to_deprecated_string()}), 'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_key.to_deprecated_string()}), 'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': course_key.to_deprecated_string()}), + 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_key_string': course_key.to_deprecated_string()}), + 'advanced_settings_url': get_studio_url(course, 'settings/advanced'), } return section_data -def _section_student_admin(course_key, access): +def _section_student_admin(course, access): """ Provide data for the corresponding dashboard section """ + course_key = course.id is_small_course = False enrollment_count = CourseEnrollment.num_enrolled_in(course_key) max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") @@ -295,8 +294,9 @@ def _section_extensions(course): return section_data -def _section_data_download(course_key, access): +def _section_data_download(course, access): """ Provide data for the corresponding dashboard section """ + course_key = course.id section_data = { 'section_key': 'data_download', 'section_display_name': _('Data Download'), @@ -311,8 +311,10 @@ def _section_data_download(course_key, access): return section_data -def _section_send_email(course_key, access, course): +def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ + course_key = course.id + # This HtmlDescriptor is only being used to generate a nice text editor. html_module = HtmlDescriptor( course.system, @@ -348,8 +350,9 @@ def _section_send_email(course_key, access, course): return section_data -def _section_analytics(course_key, access): +def _section_analytics(course, access): """ Provide data for the corresponding dashboard section """ + course_key = course.id section_data = { 'section_key': 'instructor_analytics', 'section_display_name': _('Analytics'), @@ -364,8 +367,9 @@ def _section_analytics(course_key, access): return section_data -def _section_metrics(course_key, access): +def _section_metrics(course, access): """Provide data for the corresponding dashboard section """ + course_key = course.id section_data = { 'section_key': 'metrics', 'section_display_name': _('Metrics'), diff --git a/lms/envs/common.py b/lms/envs/common.py index 715258231dc9ca1bc192477c699ebc73fcc55e2a..294dd4765d6115d9ffed4c0dc8801cc4f49ccfa2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -992,6 +992,7 @@ courseware_js = ( base_vendor_js = [ 'js/vendor/jquery.min.js', 'js/vendor/jquery.cookie.js', + 'js/vendor/underscore-min.js' ] main_vendor_js = base_vendor_js + [ @@ -999,7 +1000,6 @@ main_vendor_js = base_vendor_js + [ 'js/RequireJS-namespace-undefine.js', 'js/vendor/json2.js', 'js/vendor/jquery-ui.min.js', - 'js/vendor/jquery.cookie.js', 'js/vendor/jquery.qtip.min.js', 'js/vendor/swfobject/swfobject.js', 'js/vendor/jquery.ba-bbq.min.js', @@ -1129,6 +1129,7 @@ PIPELINE_JS = { 'js/src/utility.js', 'js/src/accessibility_tools.js', 'js/src/ie_shim.js', + 'js/src/string_utils.js', ], 'output_filename': 'js/lms-application.js', }, diff --git a/lms/static/js/collections/cohort.js b/lms/static/js/collections/cohort.js new file mode 100644 index 0000000000000000000000000000000000000000..c4d83788b30f133ecaa0763d32a3390b2cb95670 --- /dev/null +++ b/lms/static/js/collections/cohort.js @@ -0,0 +1,11 @@ +(function(Backbone) { + var CohortCollection = Backbone.Collection.extend({ + model : this.CohortModel, + comparator: "name", + + parse: function(response) { + return response.cohorts; + } + }); + this.CohortCollection = CohortCollection; +}).call(this, Backbone); diff --git a/lms/static/js/common_helpers b/lms/static/js/common_helpers new file mode 120000 index 0000000000000000000000000000000000000000..ac288058f722957ed34587908c5d2214d3d2c10a --- /dev/null +++ b/lms/static/js/common_helpers @@ -0,0 +1 @@ +../../../common/static/js/spec_helpers \ No newline at end of file diff --git a/lms/static/js/models/cohort.js b/lms/static/js/models/cohort.js new file mode 100644 index 0000000000000000000000000000000000000000..32d929027a440a56d7ef03e197e281d1cbbff12f --- /dev/null +++ b/lms/static/js/models/cohort.js @@ -0,0 +1,11 @@ +(function(Backbone) { + var CohortModel = Backbone.Model.extend({ + idAttribute: 'id', + defaults: { + name: '', + user_count: 0 + } + }); + + this.CohortModel = CohortModel; +}).call(this, Backbone); diff --git a/lms/static/js/models/notification.js b/lms/static/js/models/notification.js new file mode 100644 index 0000000000000000000000000000000000000000..d256ef180303e86a8608ce242e8609368f84c679 --- /dev/null +++ b/lms/static/js/models/notification.js @@ -0,0 +1,46 @@ +(function(Backbone) { + var NotificationModel = Backbone.Model.extend({ + defaults: { + /** + * The type of notification to be shown. + * Supported types are "confirmation", "warning" and "error". + */ + type: "confirmation", + /** + * The title to be shown for the notification. This string should be short so + * that it can be shown on a single line. + */ + title: "", + /** + * An optional message giving more details for the notification. This string can be as long + * as needed and will wrap. + */ + message: "", + /** + * An optional array of detail messages to be shown beneath the title and message. This is + * typically used to enumerate a set of warning or error conditions that occurred. + */ + details: [], + /** + * The text label to be shown for an action button, or null if there is no associated action. + */ + actionText: null, + /** + * The class to be added to the action button. This allows selectors to be written that can + * target the action button directly. + */ + actionClass: "", + /** + * An optional icon class to be shown before the text on the action button. + */ + actionIconClass: "", + /** + * An optional callback that will be invoked when the user clicks on the action button. + */ + actionCallback: null + } + }); + + this.NotificationModel = NotificationModel; +}).call(this, Backbone); + diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js new file mode 100644 index 0000000000000000000000000000000000000000..d68407d3bcf32408b054ad22f175cfb728e50a30 --- /dev/null +++ b/lms/static/js/spec/main.js @@ -0,0 +1,261 @@ +(function(requirejs, define) { + + // TODO: how can we share the vast majority of this config that is in common with CMS? + requirejs.config({ + paths: { + 'gettext': 'xmodule_js/common_static/js/test/i18n', + 'mustache': 'xmodule_js/common_static/js/vendor/mustache', + 'codemirror': 'xmodule_js/common_static/js/vendor/CodeMirror/codemirror', + 'jquery': 'xmodule_js/common_static/js/vendor/jquery.min', + 'jquery.ui': 'xmodule_js/common_static/js/vendor/jquery-ui.min', + 'jquery.flot': 'xmodule_js/common_static/js/vendor/flot/jquery.flot.min', + 'jquery.form': 'xmodule_js/common_static/js/vendor/jquery.form', + 'jquery.markitup': 'xmodule_js/common_static/js/vendor/markitup/jquery.markitup', + 'jquery.leanModal': 'xmodule_js/common_static/js/vendor/jquery.leanModal.min', + 'jquery.ajaxQueue': 'xmodule_js/common_static/js/vendor/jquery.ajaxQueue', + 'jquery.smoothScroll': 'xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min', + 'jquery.scrollTo': 'xmodule_js/common_static/js/vendor/jquery.scrollTo-1.4.2-min', + 'jquery.timepicker': 'xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker', + 'jquery.cookie': 'xmodule_js/common_static/js/vendor/jquery.cookie', + 'jquery.qtip': 'xmodule_js/common_static/js/vendor/jquery.qtip.min', + 'jquery.fileupload': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload', + 'jquery.iframe-transport': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport', + 'jquery.inputnumber': 'xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill', + 'jquery.immediateDescendents': 'xmodule_js/common_static/coffee/src/jquery.immediateDescendents', + 'jquery.simulate': 'xmodule_js/common_static/js/vendor/jquery.simulate', + 'datepair': 'xmodule_js/common_static/js/vendor/timepicker/datepair', + 'date': 'xmodule_js/common_static/js/vendor/date', + 'underscore': 'xmodule_js/common_static/js/vendor/underscore-min', + 'underscore.string': 'xmodule_js/common_static/js/vendor/underscore.string.min', + 'backbone': 'xmodule_js/common_static/js/vendor/backbone-min', + 'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min', + 'backbone.paginator': 'xmodule_js/common_static/js/vendor/backbone.paginator.min', + 'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min', + 'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce', + 'xmodule': 'xmodule_js/src/xmodule', + 'utility': 'xmodule_js/common_static/js/src/utility', + 'accessibility': 'xmodule_js/common_static/js/src/accessibility_tools', + 'sinon': 'xmodule_js/common_static/js/vendor/sinon-1.7.1', + 'squire': 'xmodule_js/common_static/js/vendor/Squire', + 'jasmine-jquery': 'xmodule_js/common_static/js/vendor/jasmine-jquery', + 'jasmine-imagediff': 'xmodule_js/common_static/js/vendor/jasmine-imagediff', + 'jasmine-stealth': 'xmodule_js/common_static/js/vendor/jasmine-stealth', + 'jasmine.async': 'xmodule_js/common_static/js/vendor/jasmine.async', + 'draggabilly': 'xmodule_js/common_static/js/vendor/draggabilly.pkgd', + 'domReady': 'xmodule_js/common_static/js/vendor/domReady', + 'URI': 'xmodule_js/common_static/js/vendor/URI.min', + 'mathjax': '//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured', + 'youtube': '//www.youtube.com/player_api?noext', + 'tender': '//edxedge.tenderapp.com/tender_widget', + 'coffee/src/ajax_prefix': 'xmodule_js/common_static/coffee/src/ajax_prefix', + 'xmodule_js/common_static/js/test/add_ajax_prefix': 'xmodule_js/common_static/js/test/add_ajax_prefix', + 'xblock/core': 'xmodule_js/common_static/coffee/src/xblock/core', + 'xblock/runtime.v1': 'xmodule_js/common_static/coffee/src/xblock/runtime.v1', + 'xblock/lms.runtime.v1': 'coffee/src/xblock/lms.runtime.v1', + 'capa/display': 'xmodule_js/src/capa/display', + 'string_utils': 'xmodule_js/common_static/js/src/string_utils', + + // Manually specify LMS files that are not converted to RequireJS + 'js/verify_student/photocapture': 'js/verify_student/photocapture', + 'js/staff_debug_actions': 'js/staff_debug_actions', + + // Backbone classes loaded explicitly until they are converted to use RequireJS + 'js/models/cohort': 'js/models/cohort', + 'js/collections/cohort': 'js/collections/cohort', + 'js/views/cohort_editor': 'js/views/cohort_editor', + 'js/views/cohorts': 'js/views/cohorts', + 'js/views/notification': 'js/views/notification', + 'js/models/notification': 'js/models/notification' + }, + shim: { + 'gettext': { + exports: 'gettext' + }, + 'string_utils': { + deps: ['underscore'] + }, + 'date': { + exports: 'Date' + }, + 'jquery.ui': { + deps: ['jquery'], + exports: 'jQuery.ui' + }, + 'jquery.flot': { + deps: ['jquery'], + exports: 'jQuery.flot' + }, + 'jquery.form': { + deps: ['jquery'], + exports: 'jQuery.fn.ajaxForm' + }, + 'jquery.markitup': { + deps: ['jquery'], + exports: 'jQuery.fn.markitup' + }, + 'jquery.leanModal': { + deps: ['jquery'], + exports: 'jQuery.fn.leanModal' + }, + 'jquery.smoothScroll': { + deps: ['jquery'], + exports: 'jQuery.fn.smoothScroll' + }, + 'jquery.ajaxQueue': { + deps: ['jquery'], + exports: 'jQuery.fn.ajaxQueue' + }, + 'jquery.scrollTo': { + deps: ['jquery'], + exports: 'jQuery.fn.scrollTo' + }, + 'jquery.cookie': { + deps: ['jquery'], + exports: 'jQuery.fn.cookie' + }, + 'jquery.qtip': { + deps: ['jquery'], + exports: 'jQuery.fn.qtip' + }, + 'jquery.fileupload': { + deps: ['jquery.iframe-transport'], + exports: 'jQuery.fn.fileupload' + }, + 'jquery.inputnumber': { + deps: ['jquery'], + exports: 'jQuery.fn.inputNumber' + }, + 'jquery.simulate': { + deps: ['jquery'], + exports: 'jQuery.fn.simulate' + }, + 'jquery.tinymce': { + deps: ['jquery', 'tinymce'], + exports: 'jQuery.fn.tinymce' + }, + 'datepair': { + deps: ['jquery.ui', 'jquery.timepicker'] + }, + 'underscore': { + exports: '_' + }, + 'backbone': { + deps: ['underscore', 'jquery'], + exports: 'Backbone' + }, + 'backbone.associations': { + deps: ['backbone'], + exports: 'Backbone.Associations' + }, + 'backbone.paginator': { + deps: ['backbone'], + exports: 'Backbone.Paginator' + }, + 'youtube': { + exports: 'YT' + }, + 'codemirror': { + exports: 'CodeMirror' + }, + 'tinymce': { + exports: 'tinymce' + }, + 'mathjax': { + exports: 'MathJax', + init: function() { + MathJax.Hub.Config({ + tex2jax: { + inlineMath: [['\\(', '\\)'], ['[mathjaxinline]', '[/mathjaxinline]']], + displayMath: [['\\[', '\\]'], ['[mathjax]', '[/mathjax]']] + } + }); + return MathJax.Hub.Configured(); + } + }, + 'URI': { + exports: 'URI' + }, + 'xmodule': { + exports: 'XModule' + }, + 'sinon': { + exports: 'sinon' + }, + 'jasmine-jquery': { + deps: ['jasmine'] + }, + 'jasmine-imagediff': { + deps: ['jasmine'] + }, + 'jasmine-stealth': { + deps: ['jasmine'] + }, + 'jasmine.async': { + deps: ['jasmine'], + exports: 'AsyncSpec' + }, + 'xblock/core': { + exports: 'XBlock', + deps: ['jquery', 'jquery.immediateDescendents'] + }, + 'xblock/runtime.v1': { + exports: 'XBlock.Runtime.v1', + deps: ['xblock/core'] + }, + 'xblock/lms.runtime.v1': { + exports: 'LmsRuntime.v1', + deps: ['xblock/runtime.v1'] + }, + 'xmodule_js/common_static/js/test/add_ajax_prefix': { + exports: 'AjaxPrefix', + deps: ['coffee/src/ajax_prefix'] + }, + + // LMS class loaded explicitly until they are converted to use RequireJS + 'js/verify_student/photocapture': { + exports: 'js/verify_student/photocapture' + }, + 'js/staff_debug_actions': { + exports: 'js/staff_debug_actions', + deps: ['gettext'] + }, + // Backbone classes loaded explicitly until they are converted to use RequireJS + 'js/models/cohort': { + exports: 'CohortModel', + deps: ['backbone'] + }, + 'js/collections/cohort': { + exports: 'CohortCollection', + deps: ['backbone', 'js/models/cohort'] + }, + 'js/views/cohort_editor': { + exports: 'CohortsEditor', + deps: ['backbone', 'jquery', 'underscore', 'js/views/notification', 'js/models/notification', + 'string_utils' + ] + }, + 'js/views/cohorts': { + exports: 'CohortsView', + deps: ['backbone', 'js/views/cohort_editor'] + }, + 'js/models/notification': { + exports: 'NotificationModel', + deps: ['backbone'] + }, + 'js/views/notification': { + exports: 'NotificationView', + deps: ['backbone', 'jquery', 'underscore'] + } + }, + }); + + // TODO: why do these need 'lms/include' at the front but the CMS equivalent logic doesn't? + define([ + // Run the LMS tests + 'lms/include/js/spec/views/cohorts_spec.js', + 'lms/include/js/spec/photocapture_spec.js', + 'lms/include/js/spec/staff_debug_actions_spec.js', + 'lms/include/js/spec/views/notification_spec.js' + ]); + +}).call(this, requirejs, define); diff --git a/lms/static/js/spec/photocapture_spec.js b/lms/static/js/spec/photocapture_spec.js index 844963a4be827f315592a168ac0d4c9c497df4e5..bcac881188ed4b35219396c8a4e9fa22dce593de 100644 --- a/lms/static/js/spec/photocapture_spec.js +++ b/lms/static/js/spec/photocapture_spec.js @@ -1,40 +1,44 @@ -describe("Photo Verification", function() { +define(['backbone', 'jquery', 'js/verify_student/photocapture'], + function (Backbone, $) { - beforeEach(function() { - setFixtures('<div id="order-error" style="display: none;"></div><input type="radio" name="contribution" value="35" id="contribution-35" checked="checked"><input type="radio" id="contribution-other" name="contribution" value=""><input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="30"><img id="face_image" src="src="data:image/png;base64,dummy"><img id="photo_id_image" src="src="data:image/png;base64,dummy">'); - }); + describe("Photo Verification", function () { - it('retake photo', function() { - spyOn(window,"refereshPageMessage").andCallFake(function(){ - return - }) - spyOn($, "ajax").andCallFake(function(e) { - e.success({"success":false}); - }); - submitToPaymentProcessing(); - expect(window.refereshPageMessage).toHaveBeenCalled(); - }); + beforeEach(function () { + setFixtures('<div id="order-error" style="display: none;"></div><input type="radio" name="contribution" value="35" id="contribution-35" checked="checked"><input type="radio" id="contribution-other" name="contribution" value=""><input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="30"><img id="face_image" src="src="data:image/png;base64,dummy"><img id="photo_id_image" src="src="data:image/png;base64,dummy">'); + }); - it('successful submission', function() { - spyOn(window,"submitForm").andCallFake(function(){ - return - }) - spyOn($, "ajax").andCallFake(function(e) { - e.success({"success":true}); - }); - submitToPaymentProcessing(); - expect(window.submitForm).toHaveBeenCalled(); - }); + it('retake photo', function () { + spyOn(window, "refereshPageMessage").andCallFake(function () { + return; + }); + spyOn($, "ajax").andCallFake(function (e) { + e.success({"success": false}); + }); + submitToPaymentProcessing(); + expect(window.refereshPageMessage).toHaveBeenCalled(); + }); - it('Error during process', function() { - spyOn(window,"showSubmissionError").andCallFake(function(){ - return - }) - spyOn($, "ajax").andCallFake(function(e) { - e.error({}); - }); - submitToPaymentProcessing(); - expect(window.showSubmissionError).toHaveBeenCalled(); - }); + it('successful submission', function () { + spyOn(window, "submitForm").andCallFake(function () { + return; + }); + spyOn($, "ajax").andCallFake(function (e) { + e.success({"success": true}); + }); + submitToPaymentProcessing(); + expect(window.submitForm).toHaveBeenCalled(); + }); -}); + it('Error during process', function () { + spyOn(window, "showSubmissionError").andCallFake(function () { + return; + }); + spyOn($, "ajax").andCallFake(function (e) { + e.error({}); + }); + submitToPaymentProcessing(); + expect(window.showSubmissionError).toHaveBeenCalled(); + }); + + }); + }); \ No newline at end of file diff --git a/lms/static/js/spec/staff_debug_actions_spec.js b/lms/static/js/spec/staff_debug_actions_spec.js index 1e13fbaf79e35b73f7f231b18799807f71f8b656..5dd78d8ec6e910bc554bddef945217cc3e0d783d 100644 --- a/lms/static/js/spec/staff_debug_actions_spec.js +++ b/lms/static/js/spec/staff_debug_actions_spec.js @@ -1,90 +1,92 @@ -describe('StaffDebugActions', function() { - var location = 'i4x://edX/Open_DemoX/edx_demo_course/problem/test_loc'; - var locationName = 'test_loc' - var fixture_id = 'sd_fu_' + locationName; - var fixture = $('<input>', { id: fixture_id, placeholder: "userman" }); +define(['backbone', 'jquery', 'js/staff_debug_actions'], + function (Backbone, $) { - describe('get_url ', function() { - it('defines url to courseware ajax entry point', function() { - spyOn(StaffDebug, "get_current_url").andReturn("/courses/edX/Open_DemoX/edx_demo_course/courseware/stuff"); - expect(StaffDebug.get_url('rescore_problem')).toBe('/courses/edX/Open_DemoX/edx_demo_course/instructor/api/rescore_problem'); - }); - }); - - describe('get_user', function() { + describe('StaffDebugActions', function () { + var location = 'i4x://edX/Open_DemoX/edx_demo_course/problem/test_loc'; + var locationName = 'test_loc'; + var fixture_id = 'sd_fu_' + locationName; + var fixture = $('<input>', { id: fixture_id, placeholder: "userman" }); - it('gets the placeholder username if input field is empty', function() { - $('body').append(fixture); - expect(StaffDebug.get_user(locationName)).toBe('userman'); - $('#' + fixture_id).remove(); - }); - it('gets a filled in name if there is one', function() { - $('body').append(fixture); - $('#' + fixture_id).val('notuserman'); - expect(StaffDebug.get_user(locationName)).toBe('notuserman'); + describe('get_url ', function () { + it('defines url to courseware ajax entry point', function () { + spyOn(StaffDebug, "get_current_url").andReturn("/courses/edX/Open_DemoX/edx_demo_course/courseware/stuff"); + expect(StaffDebug.get_url('rescore_problem')).toBe('/courses/edX/Open_DemoX/edx_demo_course/instructor/api/rescore_problem'); + }); + }); - $('#' + fixture_id).val(''); - $('#' + fixture_id).remove(); - }); - }); - describe('reset', function() { - it('makes an ajax call with the expected parameters', function() { - $('body').append(fixture); + describe('get_user', function () { - spyOn($, 'ajax'); - StaffDebug.reset(locationName, location); + it('gets the placeholder username if input field is empty', function () { + $('body').append(fixture); + expect(StaffDebug.get_user(locationName)).toBe('userman'); + $('#' + fixture_id).remove(); + }); + it('gets a filled in name if there is one', function () { + $('body').append(fixture); + $('#' + fixture_id).val('notuserman'); + expect(StaffDebug.get_user(locationName)).toBe('notuserman'); - expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET'); - expect($.ajax.mostRecentCall.args[0]['data']).toEqual({ - 'problem_to_reset': location, - 'unique_student_identifier': 'userman', - 'delete_module': false + $('#' + fixture_id).val(''); + $('#' + fixture_id).remove(); + }); }); - expect($.ajax.mostRecentCall.args[0]['url']).toEqual( - '/instructor/api/reset_student_attempts' - ); - $('#' + fixture_id).remove(); - }); - }); - describe('sdelete', function() { - it('makes an ajax call with the expected parameters', function() { - $('body').append(fixture); + describe('reset', function () { + it('makes an ajax call with the expected parameters', function () { + $('body').append(fixture); - spyOn($, 'ajax'); - StaffDebug.sdelete(locationName, location); + spyOn($, 'ajax'); + StaffDebug.reset(locationName, location); - expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET'); - expect($.ajax.mostRecentCall.args[0]['data']).toEqual({ - 'problem_to_reset': location, - 'unique_student_identifier': 'userman', - 'delete_module': true + expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET'); + expect($.ajax.mostRecentCall.args[0]['data']).toEqual({ + 'problem_to_reset': location, + 'unique_student_identifier': 'userman', + 'delete_module': false + }); + expect($.ajax.mostRecentCall.args[0]['url']).toEqual( + '/instructor/api/reset_student_attempts' + ); + $('#' + fixture_id).remove(); + }); }); - expect($.ajax.mostRecentCall.args[0]['url']).toEqual( - '/instructor/api/reset_student_attempts' - ); + describe('sdelete', function () { + it('makes an ajax call with the expected parameters', function () { + $('body').append(fixture); - $('#' + fixture_id).remove(); - }); - }); - describe('rescore', function() { - it('makes an ajax call with the expected parameters', function() { - $('body').append(fixture); + spyOn($, 'ajax'); + StaffDebug.sdelete(locationName, location); - spyOn($, 'ajax'); - StaffDebug.rescore(locationName, location); + expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET'); + expect($.ajax.mostRecentCall.args[0]['data']).toEqual({ + 'problem_to_reset': location, + 'unique_student_identifier': 'userman', + 'delete_module': true + }); + expect($.ajax.mostRecentCall.args[0]['url']).toEqual( + '/instructor/api/reset_student_attempts' + ); - expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET'); - expect($.ajax.mostRecentCall.args[0]['data']).toEqual({ - 'problem_to_reset': location, - 'unique_student_identifier': 'userman', - 'delete_module': false + $('#' + fixture_id).remove(); + }); }); - expect($.ajax.mostRecentCall.args[0]['url']).toEqual( - '/instructor/api/rescore_problem' - ); - $('#' + fixture_id).remove(); - }); - }); + describe('rescore', function () { + it('makes an ajax call with the expected parameters', function () { + $('body').append(fixture); -}); + spyOn($, 'ajax'); + StaffDebug.rescore(locationName, location); + expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET'); + expect($.ajax.mostRecentCall.args[0]['data']).toEqual({ + 'problem_to_reset': location, + 'unique_student_identifier': 'userman', + 'delete_module': false + }); + expect($.ajax.mostRecentCall.args[0]['url']).toEqual( + '/instructor/api/rescore_problem' + ); + $('#' + fixture_id).remove(); + }); + }); + }); + }); \ No newline at end of file diff --git a/lms/static/js/spec/views/cohorts_spec.js b/lms/static/js/spec/views/cohorts_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0bf1a1a5d61e7551d21710d338e38641a84a11c3 --- /dev/null +++ b/lms/static/js/spec/views/cohorts_spec.js @@ -0,0 +1,410 @@ +define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', + 'js/views/cohorts', 'js/collections/cohort', 'string_utils'], + function (Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection) { + describe("Cohorts View", function () { + var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage, + createMockCohort, createMockCohorts, createCohortsView, cohortsView, requests, respondToRefresh, + verifyMessage, verifyNoMessage, verifyDetailedMessage, verifyHeader; + + createMockCohort = function (name, id, user_count) { + return { + id: id || 1, + name: name, + user_count: user_count || 0 + }; + }; + + createMockCohorts = function (catCount, dogCount) { + return { + cohorts: [ + createMockCohort('Cat Lovers', 1, catCount || catLoversInitialCount), + createMockCohort('Dog Lovers', 2, dogCount || dogLoversInitialCount) + ] + }; + }; + + createCohortsView = function (test, initialCohortID, initialCohorts) { + var cohorts = new CohortCollection(initialCohorts || createMockCohorts(), {parse: true}); + cohorts.url = '/mock_service'; + requests = AjaxHelpers.requests(test); + cohortsView = new CohortsView({ + model: cohorts + }); + cohortsView.render(); + if (initialCohortID) { + cohortsView.$('.cohort-select').val(initialCohortID.toString()).change(); + } + }; + + respondToRefresh = function(catCount, dogCount) { + AjaxHelpers.respondWithJson(requests, createMockCohorts(catCount, dogCount)); + }; + + verifyMessage = function(expectedTitle, expectedMessageType, expectedAction, hasDetails) { + expect(cohortsView.$('.message-title').text().trim()).toBe(expectedTitle); + expect(cohortsView.$('div.message')).toHaveClass('message-' + expectedMessageType); + if (expectedAction) { + expect(cohortsView.$('.message-actions .action-primary').text().trim()).toBe(expectedAction); + } + else { + expect(cohortsView.$('.message-actions .action-primary').length).toBe(0); + } + if (!hasDetails) { + expect(cohortsView.$('.summary-items').length).toBe(0); + } + }; + + verifyNoMessage = function() { + expect(cohortsView.$('.message').length).toBe(0); + }; + + verifyDetailedMessage = function(expectedTitle, expectedMessageType, expectedDetails, expectedAction) { + var numDetails = cohortsView.$('.summary-items').children().length; + verifyMessage(expectedTitle, expectedMessageType, expectedAction, true); + expect(numDetails).toBe(expectedDetails.length); + cohortsView.$('.summary-item').each(function (index) { + expect($(this).text().trim()).toBe(expectedDetails[index]); + }); + }; + + verifyHeader = function(expectedCohortId, expectedTitle, expectedCount) { + var header = cohortsView.$('.cohort-management-group-header'); + expect(cohortsView.$('.cohort-select').val()).toBe(expectedCohortId.toString()); + expect(cohortsView.$('.cohort-select option:selected').text()).toBe( + interpolate_text( + '{title} ({count})', {title: expectedTitle, count: expectedCount} + ) + ); + expect(header.find('.title-value').text()).toBe(expectedTitle); + expect(header.find('.group-count').text()).toBe( + interpolate_ntext( + '(contains {count} student)', + '(contains {count} students)', + expectedCount, + {count: expectedCount} + ) + ); + }; + + unknownUserMessage = function (name) { + return "Unknown user: " + name; + }; + + beforeEach(function () { + setFixtures("<div></div>"); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohorts'); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/add-cohort-form'); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-selector'); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-editor'); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification'); + }); + + it("Show an error if no cohorts are defined", function() { + createCohortsView(this, null, { cohorts: [] }); + verifyMessage( + 'You currently have no cohort groups configured', + 'warning', + 'Add Cohort Group' + ); + }); + + describe("Cohort Selector", function () { + it('has no initial selection', function () { + createCohortsView(this); + expect(cohortsView.$('.cohort-select').val()).toBe(''); + expect(cohortsView.$('.cohort-management-group-header .title-value').text()).toBe(''); + }); + + it('can select a cohort', function () { + createCohortsView(this, 1); + verifyHeader(1, 'Cat Lovers', catLoversInitialCount); + }); + + it('can switch cohort', function () { + createCohortsView(this, 1); + cohortsView.$('.cohort-select').val("2").change(); + verifyHeader(2, 'Dog Lovers', dogLoversInitialCount); + }); + }); + + describe("Add Cohorts Form", function () { + var defaultCohortName = 'New Cohort'; + + it("can add a cohort", function() { + createCohortsView(this, null, { cohorts: [] }); + cohortsView.$('.action-create').click(); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(1); + expect(cohortsView.$('.cohort-management-nav')).toHaveClass('is-disabled'); + expect(cohortsView.$('.cohort-management-group')).toHaveClass('is-hidden'); + cohortsView.$('.cohort-create-name').val(defaultCohortName); + cohortsView.$('.action-save').click(); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/add', 'name=New+Cohort'); + AjaxHelpers.respondWithJson( + requests, + { + success: true, + cohort: { id: 1, name: defaultCohortName } + } + ); + AjaxHelpers.respondWithJson( + requests, + { cohorts: createMockCohort(defaultCohortName) } + ); + verifyMessage( + 'The ' + defaultCohortName + ' cohort group has been created.' + + ' You can manually add students to this group below.', + 'confirmation' + ); + verifyHeader(1, defaultCohortName, 0); + expect(cohortsView.$('.cohort-management-nav')).not.toHaveClass('is-disabled'); + expect(cohortsView.$('.cohort-management-group')).not.toHaveClass('is-hidden'); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(0); + }); + + it("trims off whitespace before adding a cohort", function() { + createCohortsView(this); + cohortsView.$('.action-create').click(); + cohortsView.$('.cohort-create-name').val(' New Cohort '); + cohortsView.$('.action-save').click(); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/add', 'name=New+Cohort'); + }); + + it("does not allow a blank cohort name to be submitted", function() { + createCohortsView(this, 1); + cohortsView.$('.action-create').click(); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(1); + cohortsView.$('.cohort-create-name').val(''); + expect(cohortsView.$('.cohort-management-nav')).toHaveClass('is-disabled'); + cohortsView.$('.action-save').click(); + expect(requests.length).toBe(0); + verifyMessage('Please enter a name for your new cohort group.', 'error'); + }); + + it("shows a message when adding a cohort throws a server error", function() { + createCohortsView(this, 1); + cohortsView.$('.action-create').click(); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(1); + cohortsView.$('.cohort-create-name').val(defaultCohortName); + cohortsView.$('.action-save').click(); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/add', 'name=New+Cohort'); + AjaxHelpers.respondWithError(requests); + verifyHeader(1, 'Cat Lovers', catLoversInitialCount); + verifyMessage( + "We've encountered an error. Please refresh your browser and then try again.", + 'error' + ); + }); + + it("shows a server message if adding a cohort fails", function() { + createCohortsView(this, 1); + cohortsView.$('.action-create').click(); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(1); + cohortsView.$('.cohort-create-name').val('Cat Lovers'); + cohortsView.$('.action-save').click(); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/add', 'name=Cat+Lovers'); + AjaxHelpers.respondWithJson( + requests, + { + success: false, + msg: 'You cannot create two cohorts with the same name' + } + ); + verifyHeader(1, 'Cat Lovers', catLoversInitialCount); + verifyMessage('You cannot create two cohorts with the same name', 'error'); + }); + + it("is removed when 'Cancel' is clicked", function() { + createCohortsView(this, 1); + cohortsView.$('.action-create').click(); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(1); + expect(cohortsView.$('.cohort-management-nav')).toHaveClass('is-disabled'); + cohortsView.$('.action-cancel').click(); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(0); + expect(cohortsView.$('.cohort-management-nav')).not.toHaveClass('is-disabled'); + }); + + it("shows an error if canceled when no cohorts are defined", function() { + createCohortsView(this, null, { cohorts: [] }); + cohortsView.$('.action-create').click(); + expect(cohortsView.$('.cohort-management-create-form').length).toBe(1); + expect(cohortsView.$('.cohort-management-nav')).toHaveClass('is-disabled'); + cohortsView.$('.action-cancel').click(); + verifyMessage( + 'You currently have no cohort groups configured', + 'warning', + 'Add Cohort Group' + ); + }); + + it("hides any error message when switching to show a cohort", function() { + createCohortsView(this, 1); + + // First try to save a blank name to create a message + cohortsView.$('.action-create').click(); + cohortsView.$('.cohort-create-name').val(''); + cohortsView.$('.action-save').click(); + verifyMessage('Please enter a name for your new cohort group.', 'error'); + + // Now switch to a different cohort + cohortsView.$('.cohort-select').val("2").change(); + verifyHeader(2, 'Dog Lovers', dogLoversInitialCount); + verifyNoMessage(); + }); + + it("hides any error message when canceling the form", function() { + createCohortsView(this, 1); + + // First try to save a blank name to create a message + cohortsView.$('.action-create').click(); + cohortsView.$('.cohort-create-name').val(''); + cohortsView.$('.action-save').click(); + verifyMessage('Please enter a name for your new cohort group.', 'error'); + + // Now cancel the form + cohortsView.$('.action-cancel').click(); + verifyNoMessage(); + }); + }); + + describe("Add Students Button", function () { + var getStudentInput, addStudents, respondToAdd; + + getStudentInput = function() { + return cohortsView.$('.cohort-management-group-add-students'); + }; + + addStudents = function(students) { + getStudentInput().val(students); + cohortsView.$('.cohort-management-group-add-form').submit(); + }; + + respondToAdd = function(result) { + AjaxHelpers.respondWithJson( + requests, + _.extend({ unknown: [], added: [], present: [], changed: [], success: true }, result) + ); + }; + + it('shows an error when adding with no students specified', function() { + createCohortsView(this, 1); + addStudents(' '); + expect(requests.length).toBe(0); + verifyMessage('Please enter a username or email.', 'error'); + expect(getStudentInput().val()).toBe(''); + }); + + it('can add a single student', function() { + var catLoversUpdatedCount = catLoversInitialCount + 1; + createCohortsView(this, 1); + addStudents('student@sample.com'); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/1/add', 'users=student%40sample.com'); + respondToAdd({ added: ['student@sample.com'] }); + respondToRefresh(catLoversUpdatedCount, dogLoversInitialCount); + verifyHeader(1, 'Cat Lovers', catLoversUpdatedCount); + verifyMessage('1 student has been added to this cohort group', 'confirmation'); + expect(getStudentInput().val()).toBe(''); + }); + + it('shows an error when adding a student that does not exist', function() { + createCohortsView(this, 1); + addStudents('unknown@sample.com'); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/1/add', 'users=unknown%40sample.com'); + respondToAdd({ unknown: ['unknown@sample.com'] }); + respondToRefresh(catLoversInitialCount, dogLoversInitialCount); + verifyHeader(1, 'Cat Lovers', catLoversInitialCount); + verifyDetailedMessage('There was an error when trying to add students:', 'error', + [unknownUserMessage('unknown@sample.com')] + ); + expect(getStudentInput().val()).toBe('unknown@sample.com'); + }); + + it('shows a "view all" button when more than 5 students do not exist', function() { + var sixUsers = 'unknown1@sample.com, unknown2@sample.com, unknown3@sample.com, unknown4@sample.com, unknown5@sample.com, unknown6@sample.com'; + createCohortsView(this, 1); + + addStudents(sixUsers); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/1/add', + 'users=' + sixUsers.replace(/@/g, "%40").replace(/, /g, "%2C+") + ); + respondToAdd({ unknown: [ + 'unknown1@sample.com', + 'unknown2@sample.com', + 'unknown3@sample.com', + 'unknown4@sample.com', + 'unknown5@sample.com', + 'unknown6@sample.com'] + }); + respondToRefresh(catLoversInitialCount + 6, dogLoversInitialCount); + verifyDetailedMessage('There were 6 errors when trying to add students:', 'error', + [ + unknownUserMessage('unknown1@sample.com'), unknownUserMessage('unknown2@sample.com'), + unknownUserMessage('unknown3@sample.com'), unknownUserMessage('unknown4@sample.com'), + unknownUserMessage('unknown5@sample.com') + ], + 'View all errors' + ); + expect(getStudentInput().val()).toBe(sixUsers); + // Click "View all" + cohortsView.$('.action-expand').click(); + verifyDetailedMessage('There were 6 errors when trying to add students:', 'error', + [ + unknownUserMessage('unknown1@sample.com'), unknownUserMessage('unknown2@sample.com'), + unknownUserMessage('unknown3@sample.com'), unknownUserMessage('unknown4@sample.com'), + unknownUserMessage('unknown5@sample.com'), unknownUserMessage('unknown6@sample.com') + ] + ); + }); + + it('shows students moved from one cohort to another', function() { + var sixUsers = 'moved1@sample.com, moved2@sample.com, moved3@sample.com, alreadypresent@sample.com'; + createCohortsView(this, 1); + + addStudents(sixUsers); + AjaxHelpers.expectRequest(requests, 'POST', '/mock_service/1/add', + 'users=' + sixUsers.replace(/@/g, "%40").replace(/, /g, "%2C+") + ); + respondToAdd({ + changed: [ + {email: 'moved1@sample.com', name: 'moved1', previous_cohort: 'group 2', username: 'moved1'}, + {email: 'moved2@sample.com', name: 'moved2', previous_cohort: 'group 2', username: 'moved2'}, + {email: 'moved3@sample.com', name: 'moved3', previous_cohort: 'group 3', username: 'moved3'} + ], + present: ['alreadypresent@sample.com'] + }); + respondToRefresh(); + + verifyDetailedMessage('3 students have been added to this cohort group', 'confirmation', + [ + "2 students were removed from group 2", + "1 student was removed from group 3", + "1 student was already in the cohort group" + ] + ); + expect(getStudentInput().val()).toBe(''); + }); + + it('shows a message when the add fails', function() { + createCohortsView(this, 1); + addStudents('student@sample.com'); + AjaxHelpers.respondWithError(requests); + verifyMessage('Error adding students.', 'error'); + expect(getStudentInput().val()).toBe('student@sample.com'); + }); + + it('clears an error message on subsequent add', function() { + createCohortsView(this, 1); + + // First verify that an error is shown + addStudents('student@sample.com'); + AjaxHelpers.respondWithError(requests); + verifyMessage('Error adding students.', 'error'); + + // Now verify that the error is removed on a subsequent add + addStudents('student@sample.com'); + respondToAdd({ added: ['student@sample.com'] }); + respondToRefresh(catLoversInitialCount + 1, dogLoversInitialCount); + verifyMessage('1 student has been added to this cohort group', 'confirmation'); + }); + }); + }); + }); diff --git a/lms/static/js/spec/views/notification_spec.js b/lms/static/js/spec/views/notification_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..66747b37b44b68e1bb623f9b733787a41aa9a4e3 --- /dev/null +++ b/lms/static/js/spec/views/notification_spec.js @@ -0,0 +1,98 @@ +define(['backbone', 'jquery', 'js/models/notification', 'js/views/notification', 'js/common_helpers/template_helpers'], + function (Backbone, $, NotificationModel, NotificationView, TemplateHelpers) { + describe("NotificationView", function () { + var createNotification, verifyTitle, verifyMessage, verifyDetails, verifyAction, notificationView; + + createNotification = function (modelVals) { + var notificationModel = new NotificationModel(modelVals); + notificationView = new NotificationView({ + model: notificationModel + }); + notificationView.render(); + return notificationView; + }; + + verifyTitle = function (expectedTitle) { + expect(notificationView.$('.message-title').text().trim()).toBe(expectedTitle); + }; + + verifyMessage = function (expectedMessage) { + expect(notificationView.$('.message-copy').text().trim()).toBe(expectedMessage); + }; + + verifyDetails = function (expectedDetails) { + var details = notificationView.$('.summary-item'); + expect(details.length).toBe(expectedDetails.length); + details.each(function (index) { + expect($(this).text().trim()).toBe(expectedDetails[index]); + }); + }; + + verifyAction = function (expectedActionText) { + var actionButton = notificationView.$('.action-primary'); + if (expectedActionText) { + expect(actionButton.text().trim()).toBe(expectedActionText); + } + else { + expect(actionButton.length).toBe(0); + } + }; + + beforeEach(function () { + setFixtures("<div></div>"); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification'); + }); + + it('has default values', function () { + createNotification({}); + expect(notificationView.$('div.message')).toHaveClass('message-confirmation'); + verifyTitle(''); + verifyDetails([]); + verifyAction(null); + }); + + it('can use an error type', function () { + createNotification({type: 'error'}); + expect(notificationView.$('div.message')).toHaveClass('message-error'); + expect(notificationView.$('div.message')).not.toHaveClass('message-confirmation'); + }); + + it('can specify a title', function () { + createNotification({title: 'notification title'}); + verifyTitle('notification title'); + }); + + it('can specify a message', function () { + createNotification({message: 'This is a dummy message'}); + verifyMessage('This is a dummy message'); + }); + + it('can specify details', function () { + var expectedDetails = ['detail 1', 'detail 2']; + createNotification({details: expectedDetails}); + verifyDetails(expectedDetails); + }); + + it ('shows an action button if text and callback are provided', function () { + createNotification({actionText: 'action text', actionCallback: function () {}}); + verifyAction('action text'); + }); + + it ('shows an action button if only text is provided', function () { + createNotification({actionText: 'action text'}); + verifyAction('action text'); + }); + + it ('does not show an action button if text is not provided', function () { + createNotification({actionCallback: function () {}}); + verifyAction(null); + }); + + it ('triggers the callback when the action button is clicked', function () { + var actionCallback = jasmine.createSpy('Spy on callback'); + var view = createNotification({actionText: 'action text', actionCallback: actionCallback}); + notificationView.$('button.action-primary').click(); + expect(actionCallback).toHaveBeenCalledWith(view); + }); + }); + }); diff --git a/lms/static/js/views/cohort_editor.js b/lms/static/js/views/cohort_editor.js new file mode 100644 index 0000000000000000000000000000000000000000..bb9294689c3d62235237fc1999977148cecb355a --- /dev/null +++ b/lms/static/js/views/cohort_editor.js @@ -0,0 +1,207 @@ +(function(Backbone, _, $, gettext, ngettext, interpolate_text, NotificationModel, NotificationView) { + var CohortEditorView = Backbone.View.extend({ + events : { + "submit .cohort-management-group-add-form": "addStudents" + }, + + initialize: function(options) { + this.template = _.template($('#cohort-editor-tpl').text()); + this.cohorts = options.cohorts; + this.advanced_settings_url = options.advanced_settings_url; + }, + + // Any errors that are currently being displayed to the instructor (for example, unknown email addresses). + errorNotifications: null, + // Any confirmation messages that are currently being displayed (for example, number of students added). + confirmationNotifications: null, + + render: function() { + this.$el.html(this.template({ + cohort: this.model, + advanced_settings_url: this.advanced_settings_url + })); + return this; + }, + + setCohort: function(cohort) { + this.model = cohort; + this.render(); + }, + + addStudents: function(event) { + event.preventDefault(); + var self = this, + cohorts = this.cohorts, + input = this.$('.cohort-management-group-add-students'), + add_url = this.model.url() + '/add', + students = input.val().trim(), + cohortId = this.model.id; + + if (students.length > 0) { + $.post( + add_url, {'users': students} + ).done(function(modifiedUsers) { + self.refreshCohorts().done(function() { + // Find the equivalent cohort in the new collection and select it + var cohort = cohorts.get(cohortId); + self.setCohort(cohort); + + // Show the notifications + self.addNotifications(modifiedUsers); + + // If an unknown user was specified then update the new input to have + // the original input's value. This is to allow the user to correct the + // value in case it was a typo. + if (modifiedUsers.unknown.length > 0) { + self.$('.cohort-management-group-add-students').val(students); + } + }); + }).fail(function() { + self.showErrorMessage(gettext('Error adding students.'), true); + }); + } else { + self.showErrorMessage(gettext('Please enter a username or email.'), true); + input.val(''); + } + }, + + /** + * Refresh the cohort collection to get the latest set as well as up-to-date counts. + */ + refreshCohorts: function() { + return this.cohorts.fetch(); + }, + + undelegateViewEvents: function (view) { + if (view) { + view.undelegateEvents(); + } + }, + + showErrorMessage: function(message, removeConfirmations, model) { + if (removeConfirmations && this.confirmationNotifications) { + this.undelegateViewEvents(this.confirmationNotifications); + this.confirmationNotifications.$el.html(''); + this.confirmationNotifications = null; + } + if (model === undefined) { + model = new NotificationModel(); + } + model.set('type', 'error'); + model.set('title', message); + + this.undelegateViewEvents(this.errorNotifications); + + this.errorNotifications = new NotificationView({ + el: this.$('.cohort-errors'), + model: model + }); + this.errorNotifications.render(); + }, + + addNotifications: function(modifiedUsers) { + var oldCohort, title, details, numPresent, numUsersAdded, numErrors, + createErrorDetails, errorActionCallback, errorModel, + errorLimit = 5; + + // Show confirmation messages. + this.undelegateViewEvents(this.confirmationNotifications); + numUsersAdded = modifiedUsers.added.length + modifiedUsers.changed.length; + numPresent = modifiedUsers.present.length; + if (numUsersAdded > 0 || numPresent > 0) { + title = interpolate_text( + ngettext("{numUsersAdded} student has been added to this cohort group", + "{numUsersAdded} students have been added to this cohort group", numUsersAdded), + {numUsersAdded: numUsersAdded} + ); + + var movedByCohort = {}; + _.each(modifiedUsers.changed, function (changedInfo) { + oldCohort = changedInfo.previous_cohort; + if (oldCohort in movedByCohort) { + movedByCohort[oldCohort] = movedByCohort[oldCohort] + 1; + } + else { + movedByCohort[oldCohort] = 1; + } + }); + + details = []; + for (oldCohort in movedByCohort) { + details.push( + interpolate_text( + ngettext("{numMoved} student was removed from {oldCohort}", + "{numMoved} students were removed from {oldCohort}", movedByCohort[oldCohort]), + {numMoved: movedByCohort[oldCohort], oldCohort: oldCohort} + ) + ); + } + if (numPresent > 0) { + details.push( + interpolate_text( + ngettext("{numPresent} student was already in the cohort group", + "{numPresent} students were already in the cohort group", numPresent), + {numPresent: numPresent} + ) + ); + } + + this.confirmationNotifications = new NotificationView({ + el: this.$('.cohort-confirmations'), + model: new NotificationModel({ + type: "confirmation", + title: title, + details: details + }) + }); + this.confirmationNotifications.render(); + } + else if (this.confirmationNotifications) { + this.confirmationNotifications.$el.html(''); + this.confirmationNotifications = null; + } + + // Show error messages. + this.undelegateViewEvents(this.errorNotifications); + numErrors = modifiedUsers.unknown.length; + if (numErrors > 0) { + createErrorDetails = function (unknownUsers, showAllErrors) { + var numErrors = unknownUsers.length, details = []; + + for (var i = 0; i < (showAllErrors ? numErrors : Math.min(errorLimit, numErrors)); i++) { + details.push(interpolate_text(gettext("Unknown user: {user}"), {user: unknownUsers[i]})); + } + return details; + }; + + title = interpolate_text( + ngettext("There was an error when trying to add students:", + "There were {numErrors} errors when trying to add students:", numErrors), + {numErrors: numErrors} + ); + details = createErrorDetails(modifiedUsers.unknown, false); + + errorActionCallback = function (view) { + view.model.set("actionText", null); + view.model.set("details", createErrorDetails(modifiedUsers.unknown, true)); + view.render(); + }; + + errorModel = new NotificationModel({ + details: details, + actionText: numErrors > errorLimit ? gettext("View all errors") : null, + actionCallback: errorActionCallback, + actionClass: 'action-expand' + }); + + this.showErrorMessage(title, false, errorModel); + } + else if (this.errorNotifications) { + this.errorNotifications.$el.html(''); + this.errorNotifications = null; + } + } + }); + + this.CohortEditorView = CohortEditorView; +}).call(this, Backbone, _, $, gettext, ngettext, interpolate_text, NotificationModel, NotificationView); diff --git a/lms/static/js/views/cohorts.js b/lms/static/js/views/cohorts.js new file mode 100644 index 0000000000000000000000000000000000000000..c7cf01aeba3f8785a58f67e4652c828c24c13ac6 --- /dev/null +++ b/lms/static/js/views/cohorts.js @@ -0,0 +1,175 @@ +(function($, _, Backbone, gettext, interpolate_text, CohortEditorView, NotificationModel, NotificationView) { + var hiddenClass = 'is-hidden', + disabledClass = 'is-disabled'; + + this.CohortsView = Backbone.View.extend({ + events : { + 'change .cohort-select': 'onCohortSelected', + 'click .action-create': 'showAddCohortForm', + 'click .action-cancel': 'cancelAddCohortForm', + 'click .action-save': 'saveAddCohortForm' + }, + + initialize: function(options) { + this.template = _.template($('#cohorts-tpl').text()); + this.selectorTemplate = _.template($('#cohort-selector-tpl').text()); + this.addCohortFormTemplate = _.template($('#add-cohort-form-tpl').text()); + this.advanced_settings_url = options.advanced_settings_url; + this.model.on('sync', this.onSync, this); + }, + + render: function() { + this.$el.html(this.template({ + cohorts: this.model.models + })); + this.onSync(); + return this; + }, + + renderSelector: function(selectedCohort) { + this.$('.cohort-select').html(this.selectorTemplate({ + cohorts: this.model.models, + selectedCohort: selectedCohort + })); + }, + + onSync: function() { + var selectedCohort = this.lastSelectedCohortId && this.model.get(this.lastSelectedCohortId), + hasCohorts = this.model.length > 0; + this.hideAddCohortForm(); + if (hasCohorts) { + this.$('.cohort-management-nav').removeClass(hiddenClass); + this.renderSelector(selectedCohort); + if (selectedCohort) { + this.showCohortEditor(selectedCohort); + } + } else { + this.$('.cohort-management-nav').addClass(hiddenClass); + this.showNotification({ + type: 'warning', + title: gettext('You currently have no cohort groups configured'), + actionText: gettext('Add Cohort Group'), + actionClass: 'action-create', + actionIconClass: 'icon-plus' + }); + } + }, + + getSelectedCohort: function() { + var id = this.$('.cohort-select').val(); + return id && this.model.get(parseInt(id)); + }, + + onCohortSelected: function(event) { + event.preventDefault(); + var selectedCohort = this.getSelectedCohort(); + this.lastSelectedCohortId = selectedCohort.get('id'); + this.showCohortEditor(selectedCohort); + }, + + showCohortEditor: function(cohort) { + this.removeNotification(); + if (this.editor) { + this.editor.setCohort(cohort); + } else { + this.editor = new CohortEditorView({ + el: this.$('.cohort-management-group'), + model: cohort, + cohorts: this.model, + advanced_settings_url: this.advanced_settings_url + }); + this.editor.render(); + } + }, + + showNotification: function(options, beforeElement) { + var model = new NotificationModel(options); + this.removeNotification(); + this.notification = new NotificationView({ + model: model + }); + this.notification.render(); + if (!beforeElement) { + beforeElement = this.$('.cohort-management-group'); + } + beforeElement.before(this.notification.$el); + }, + + removeNotification: function() { + if (this.notification) { + this.notification.remove(); + } + }, + + showAddCohortForm: function(event) { + event.preventDefault(); + this.removeNotification(); + this.addCohortForm = $(this.addCohortFormTemplate({})); + this.addCohortForm.insertAfter(this.$('.cohort-management-nav')); + this.setCohortEditorVisibility(false); + }, + + hideAddCohortForm: function() { + this.setCohortEditorVisibility(true); + if (this.addCohortForm) { + this.addCohortForm.remove(); + this.addCohortForm = null; + } + }, + + setCohortEditorVisibility: function(showEditor) { + if (showEditor) { + this.$('.cohort-management-group').removeClass(hiddenClass); + this.$('.cohort-management-nav').removeClass(disabledClass); + } else { + this.$('.cohort-management-group').addClass(hiddenClass); + this.$('.cohort-management-nav').addClass(disabledClass); + } + }, + + saveAddCohortForm: function(event) { + event.preventDefault(); + var self = this, + showAddError, + cohortName = this.$('.cohort-create-name').val().trim(); + showAddError = function(message) { + self.showNotification( + {type: 'error', title: message}, + self.$('.cohort-management-create-form-name label') + ); + }; + this.removeNotification(); + if (cohortName.length > 0) { + $.post( + this.model.url + '/add', + {name: cohortName} + ).done(function(result) { + if (result.success) { + self.lastSelectedCohortId = result.cohort.id; + self.model.fetch().done(function() { + self.showNotification({ + type: 'confirmation', + title: interpolate_text( + gettext('The {cohortGroupName} cohort group has been created. You can manually add students to this group below.'), + {cohortGroupName: cohortName} + ) + }); + }); + } else { + showAddError(result.msg); + } + }).fail(function() { + showAddError(gettext("We've encountered an error. Please refresh your browser and then try again.")); + }); + } else { + showAddError(gettext('Please enter a name for your new cohort group.')); + } + }, + + cancelAddCohortForm: function(event) { + event.preventDefault(); + this.removeNotification(); + this.onSync(); + } + }); +}).call(this, $, _, Backbone, gettext, interpolate_text, CohortEditorView, NotificationModel, NotificationView); diff --git a/lms/static/js/views/notification.js b/lms/static/js/views/notification.js new file mode 100644 index 0000000000000000000000000000000000000000..3b94384029ed40002eddb2a2ba1100641023c0ff --- /dev/null +++ b/lms/static/js/views/notification.js @@ -0,0 +1,34 @@ +(function(Backbone, $, _) { + var NotificationView = Backbone.View.extend({ + events : { + "click .action-primary": "triggerCallback" + }, + + initialize: function() { + this.template = _.template($('#notification-tpl').text()); + }, + + render: function() { + this.$el.html(this.template({ + type: this.model.get("type"), + title: this.model.get("title"), + message: this.model.get("message"), + details: this.model.get("details"), + actionText: this.model.get("actionText"), + actionClass: this.model.get("actionClass"), + actionIconClass: this.model.get("actionIconClass") + })); + return this; + }, + + triggerCallback: function(event) { + event.preventDefault(); + var actionCallback = this.model.get("actionCallback"); + if (actionCallback) { + actionCallback(this); + } + } + }); + + this.NotificationView = NotificationView; +}).call(this, Backbone, $, _); diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index 06132bb559a82ad948973095f99947f37084f6e3..1748bea098a81e5d749cd9297f1911da10b08e05 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -1,5 +1,5 @@ --- -# JavaScript test suite description +# LMS JavaScript tests (using RequireJS). # # # To run all the tests and print results to the console: @@ -16,7 +16,7 @@ test_suite_name: lms -test_runner: jasmine +test_runner: jasmine_requirejs # Path prepended to source files in the coverage report (optional) # For example, if the source path @@ -46,16 +46,18 @@ lib_paths: - xmodule_js/src/capa/ - xmodule_js/src/video/ - xmodule_js/src/xmodule.js + - xmodule_js/common_static/js/vendor/underscore-min.js + - xmodule_js/common_static/js/vendor/backbone-min.js # Paths to source JavaScript files src_paths: - - coffee/src - - js/src - js + - js/common_helpers + - xmodule_js + - xmodule_js/common_static # Paths to spec (test) JavaScript files spec_paths: - - coffee/spec - js/spec # Paths to fixture files (optional) @@ -68,23 +70,13 @@ spec_paths: # loadFixtures('path/to/fixture/fixture.html'); # fixture_paths: - - coffee/fixtures - - js/fixtures + - templates/instructor/instructor_dashboard_2 -# Regular expressions used to exclude *.js files from -# appearing in the test runner page. -# Files are included by default, which means that they -# are loaded using a <script> tag in the test runner page. -# When loading many files, this can be slow, so -# exclude any files you don't need. -#exclude_from_page: -# - path/to/lib/exclude/* +requirejs: + paths: + main: js/spec/main -# Regular expression used to guarantee that a *.js file -# is included in the test runner page. -# If a file name matches both `exclude_from_page` and -# `include_in_page`, the file WILL be included. -# You can use this to exclude all files in a directory, -# but make an exception for particular files. -#include_in_page: -# - path/to/lib/include/* +# Because require.js is responsible for loading all dependencies, we exclude +# all files from being included in <script> tags +exclude_from_page: + - .* diff --git a/lms/static/js_test_coffee.yml b/lms/static/js_test_coffee.yml new file mode 100644 index 0000000000000000000000000000000000000000..9c5b118d7a14169ab5d0a820a69491cd76fcad76 --- /dev/null +++ b/lms/static/js_test_coffee.yml @@ -0,0 +1,86 @@ +--- +# LMS CoffeeScript tests that are not yet using RequireJS. +# +# +# To run all the tests and print results to the console: +# +# js-test-tool run TEST_SUITE --use-firefox +# +# where `TEST_SUITE` is this file. +# +# +# To run the tests in your default browser ("dev mode"): +# +# js-test-tool dev TEST_SUITE +# + +test_suite_name: lms-coffee + +test_runner: jasmine + +# Path prepended to source files in the coverage report (optional) +# For example, if the source path +# is "src/source.js" (relative to this YAML file) +# and the prepend path is "base/dir" +# then the coverage report will show +# "base/dir/src/source.js" +prepend_path: lms/static + +# Paths to library JavaScript files (optional) +lib_paths: + - xmodule_js/common_static/js/test/i18n.js + - xmodule_js/common_static/coffee/src/ajax_prefix.js + - xmodule_js/common_static/coffee/src/logger.js + - xmodule_js/common_static/js/vendor/jasmine-jquery.js + - xmodule_js/common_static/js/vendor/jasmine-imagediff.js + - xmodule_js/common_static/js/vendor/require.js + - js/RequireJS-namespace-undefine.js + - xmodule_js/common_static/js/vendor/jquery.min.js + - xmodule_js/common_static/js/vendor/jquery-ui.min.js + - xmodule_js/common_static/js/vendor/jquery.cookie.js + - xmodule_js/common_static/js/vendor/flot/jquery.flot.js + - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js + - xmodule_js/common_static/js/vendor/URI.min.js + - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js + - xmodule_js/common_static/coffee/src/xblock + - xmodule_js/src/capa/ + - xmodule_js/src/video/ + - xmodule_js/src/xmodule.js + +# Paths to source JavaScript files +src_paths: + - coffee/src + +# Paths to spec (test) JavaScript files +spec_paths: + - coffee/spec + +# Paths to fixture files (optional) +# The fixture path will be set automatically when using jasmine-jquery. +# (https://github.com/velesin/jasmine-jquery) +# +# You can then access fixtures using paths relative to +# the test suite description: +# +# loadFixtures('path/to/fixture/fixture.html'); +# +fixture_paths: + - coffee/fixtures + +# Regular expressions used to exclude *.js files from +# appearing in the test runner page. +# Files are included by default, which means that they +# are loaded using a <script> tag in the test runner page. +# When loading many files, this can be slow, so +# exclude any files you don't need. +#exclude_from_page: +# - path/to/lib/exclude/* + +# Regular expression used to guarantee that a *.js file +# is included in the test runner page. +# If a file name matches both `exclude_from_page` and +# `include_in_page`, the file WILL be included. +# You can use this to exclude all files in a directory, +# but make an exception for particular files. +#include_in_page: +# - path/to/lib/include/* diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index c7f978e6170e8411edae627e7aeae8840c4a867c..a382a3a770e651e153434c57716bdce48c7a6e01 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -300,6 +300,11 @@ mark { @extend %ui-disabled; } +// UI - is hidden +.is-hidden { + display: none; +} + // UI - semantically hide text .sr { @extend %text-sr; diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 3b37b53ba68b9739599a9014dc904375f49f13bd..d530877f3f56eab0d5d385b0fa2947d8048b92d2 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -6,6 +6,10 @@ padding: 8px 17px 8px 17px; font-size: em(13); line-height: 1.3em; + + .icon { + margin-right: ($baseline/5); + } } .instructor-dashboard-wrapper-2 { @@ -34,7 +38,6 @@ } // system feedback - messages - .wrapper-msg { margin-bottom: ($baseline*1.5); } @@ -48,10 +51,6 @@ .copy { font-weight: 600; } - - &.is-shown { - display: block; - } } // TYPE: warning @@ -60,10 +59,6 @@ background: tint($warning-color,95%); display: none; color: $warning-color; - - &.is-shown { - display: block; - } } // TYPE: confirm @@ -72,10 +67,6 @@ background: tint($confirm-color,95%); display: none; color: $confirm-color; - - &.is-shown { - display: block; - } } // TYPE: confirm @@ -86,10 +77,6 @@ .copy { color: $error-color; } - - &.is-shown { - display: block; - } } // inline copy @@ -121,6 +108,8 @@ } } +// instructor dashboard 2 +// ==================== section.instructor-dashboard-content-2 { @extend .content; // position: relative; @@ -210,35 +199,119 @@ section.instructor-dashboard-content-2 { } } } +} - section.idash-section { - display: none; - margin-top: ($baseline*1.5); - // background-color: #0f0; +// elements - general +// -------------------- +.idash-section { - &.active-section { - display: block; - // background-color: #ff0; + // messages + .message { + margin-bottom: $baseline; + display: block; + border-radius: 1px; + padding: ($baseline*0.75) $baseline; + } + + .message-title { + @extend %t-title6; + @extend %t-weight4; + margin-bottom: ($baseline/4); + } + + .message-copy { + @extend %t-copy-sub1; + } + + .message-actions { + margin-top: ($baseline/2); + + .action-primary { + @include idashbutton($gray-l4); } + } + + // type - error + .message-error { + border-top: 2px solid $error-color; + background: tint($error-color,95%); - .basic-data { - padding: 6px; + .message-title { + color: $error-color; } - .running-tasks-section { - display: none; + .message-copy { + color: $base-font-color; } + } - .no-pending-tasks-message { - display: none; - p { - color: #a2a2a2; - font-style: italic; - } + // type - confirmation + .message-confirmation { + border-top: 2px solid $confirm-color; + background: tint($confirm-color,95%); + + .message-title { + color: $confirm-color; + } + + .message-copy { + color: $base-font-color; + } + } + + // type - error + .message-warning { + border-top: 2px solid $warning-color; + background: tint($warning-color,95%); + } + + // grandfathered + display: none; + margin-top: ($baseline*1.5); + + &.active-section { + display: block; + } + + .basic-data { + padding: 6px; + } + + .running-tasks-section { + display: none; + } + + .no-pending-tasks-message { + display: none; + + p { + color: #a2a2a2; + font-style: italic; + } + } + + .section-title { + @include clearfix(); + margin-bottom: ($baseline/2); + + .value { + float: left; + } + + .description { + @extend %t-title7; + float: right; + text-transform: none; + letter-spacing: 0; + text-align: right; + color: $gray; } } } + +// view - course info +// -------------------- .instructor-dashboard-wrapper-2 section.idash-section#course_info { .course-errors-wrapper { margin-top: 2em; @@ -301,6 +374,8 @@ section.instructor-dashboard-content-2 { } } +// view - bulk email +// -------------------- .instructor-dashboard-wrapper-2 section.idash-section#send_email { // form fields .list-fields { @@ -325,22 +400,261 @@ section.instructor-dashboard-content-2 { } } - +// view - membership +// -------------------- .instructor-dashboard-wrapper-2 section.idash-section#membership { - $half_width: $baseline * 20; - .vert-left, - .vert-right { - display: inline-block; - vertical-align: top; - width: 48%; - margin-right: 2%; + .membership-section { + margin-bottom: ($baseline*1.5); + + &:last-child { + margin-bottom: 0; + } + } + + // cohort management + %cohort-management-form { + + .form-fields { + + .label, .input, .tip { + display: block; + } + + .label { + @extend %t-title7; + @extend %t-weight4; + margin-bottom: ($baseline/2); + } + + .tip { + @extend %t-copy-sub1; + margin-top: ($baseline/4); + color: $gray-l3; + } + + .field-text { + + // needed to reset poor input styling + input[type="text"] { + height: auto; + } + + .input { + width: 100%; + padding: ($baseline/2) ($baseline*0.75); + } + } + } + + .form-submit, .form-cancel { + display: inline-block; + vertical-align: middle; + } + + .form-submit { + @include idashbutton($blue); + @include font-size(14); + @include line-height(14); + margin-right: ($baseline/2); + margin-bottom: 0; + text-shadow: none; + } + + .form-cancel { + @extend %t-copy-sub1; + } + } + + .cohort-management-nav { + @include clearfix(); + margin-bottom: $baseline; + + .cohort-management-nav-form { + width: 60%; + float: left; + } + + .cohort-select { + width: 100%; + margin-top: ($baseline/4); + } + + .action-create { + @include idashbutton($blue); + float: right; + text-align: right; + text-shadow: none; + font-weight: 600; + } + + // STATE: is disabled + &.is-disabled { + + + .cohort-select { + opacity: 0.25; + } + + .action-create { + opacity: 0.50; + } + } + } + + .cohort-management { + + // specific message actions + .message .action-create { + @include idashbutton($blue); + } + } + + // create or edit cohort group + .cohort-management-create, .cohort-management-edit { + @extend %cohort-management-form; + border: 1px solid $gray-l5; + margin-bottom: $baseline; + + .form-title { + @extend %t-title5; + @extend %t-weight4; + border-bottom: ($baseline/10) solid $gray-l4; + background: $gray-l5; + padding: $baseline; + } + + .form-fields { + padding: $baseline; + } + + .form-actions { + padding: 0 $baseline $baseline $baseline; + } } - .vert-right { - margin-right: 0; + // cohort group + .cohort-management-group { + border: 1px solid $gray-l5; } + .cohort-management-group-header { + border-bottom: ($baseline/10) solid $gray-l4; + background: $gray-l5; + padding: $baseline; + + .group-header-title { + margin-bottom: ($baseline/2); + border-bottom: 1px solid $gray-l4; + padding-bottom: ($baseline/2); + + &:hover, &:active, &:focus { + + .action-edit-name { + opacity: 1.0; + pointer-events: auto; + } + } + } + + .title-value, .group-count, .action-edit { + display: inline-block; + vertical-align: middle; + } + + .title-value { + @extend %t-title5; + @extend %t-weight4; + margin-right: ($baseline/4); + } + + .group-count { + @extend %t-title7; + @extend %t-weight4; + } + + .action-edit-name { + @include idashbutton($gray-l3); + @include transition(opacity 0.25s ease-in-out); + @include font-size(14); + @include line-height(14); + margin-left: ($baseline/2); + margin-bottom: 0; + opacity: 0.0; + pointer-events: none; + padding: ($baseline/4) ($baseline/2); + } + } + + .cohort-management-group-setup { + @include clearfix(); + @extend %t-copy-sub1; + color: $gray-l2; + + .setup-value { + float: left; + width: 70%; + margin-right: 5%; + } + + .setup-actions { + float: right; + width: 20%; + text-align: right; + } + } + + .cohort-management-group-add { + @extend %cohort-management-form; + padding: $baseline $baseline 0 $baseline; + + .message-title { + @extend %t-title7; + } + + .form-title { + @extend %t-title6; + @extend %t-weight4; + margin-bottom: ($baseline/4); + } + + .form-introduction { + @extend %t-copy-sub1; + margin-bottom: $baseline; + + p { + color: $gray-l1; + } + } + + .form-fields { + padding: $baseline 0; + } + + .form-actions { + padding: 0 0 $baseline 0; + } + + .cohort-management-group-add-students { + min-height: ($baseline*10); + width: 100%; + padding: ($baseline/2) ($baseline*0.75); + } + } + + + .cohort-management-supplemental { + @extend %t-copy-sub1; + margin-top: ($baseline/2); + + .icon { + margin-right: ($baseline/4); + color: $gray-l1; + } + } + + + .batch-enrollment, .batch-beta-testers { textarea { margin-top: 0.2em; @@ -535,7 +849,8 @@ section.instructor-dashboard-content-2 { } } - +// view - student admin +// -------------------- .instructor-dashboard-wrapper-2 section.idash-section#student_admin > { .action-type-container{ margin-bottom: $baseline * 2; @@ -570,7 +885,8 @@ section.instructor-dashboard-content-2 { } } - +// view - data download +// -------------------- .instructor-dashboard-wrapper-2 section.idash-section#data_download { input { // display: block; @@ -600,7 +916,8 @@ section.instructor-dashboard-content-2 { } } - +// view - metrics +// -------------------- .instructor-dashboard-wrapper-2 section.idash-section#metrics { .metrics-container, .metrics-header-container { diff --git a/lms/static/sass/elements/_system-feedback.scss b/lms/static/sass/elements/_system-feedback.scss index 065fcf4e0556eb1185bf6da47a63db8547e409a5..2943df4dc8530e5f5b592ac1db2f374070ff360c 100644 --- a/lms/static/sass/elements/_system-feedback.scss +++ b/lms/static/sass/elements/_system-feedback.scss @@ -11,10 +11,6 @@ background: $notify-banner-bg-1; padding: $baseline ($baseline*1.5); - &.is-hidden { - display: none; - } - // basic object .msg { @include clearfix(); diff --git a/lms/static/templates b/lms/static/templates new file mode 120000 index 0000000000000000000000000000000000000000..564a409d419605fdbf070db35b5eecb7d8089bb3 --- /dev/null +++ b/lms/static/templates @@ -0,0 +1 @@ +../templates \ No newline at end of file diff --git a/lms/templates/discussion/_js_head_dependencies.html b/lms/templates/discussion/_js_head_dependencies.html index dd437c33b36d43c795d3078ecb7d0be2c34ebece..4bf92ae79c90507c6cba87fbe1945db775e2164a 100644 --- a/lms/templates/discussion/_js_head_dependencies.html +++ b/lms/templates/discussion/_js_head_dependencies.html @@ -11,7 +11,6 @@ <script type="text/javascript" src="${static.url('js/src/jquery.timeago.locale.js')}"></script> <script type="text/javascript" src="${static.url('js/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script> -<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/src/tooltip_manager.js')}"></script> diff --git a/lms/templates/instructor/instructor_dashboard_2/add-cohort-form.underscore b/lms/templates/instructor/instructor_dashboard_2/add-cohort-form.underscore new file mode 100644 index 0000000000000000000000000000000000000000..8770977fe745fde1817c697ec4f22f867abc1a02 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/add-cohort-form.underscore @@ -0,0 +1,26 @@ +<div class="cohort-management-create"> + <form action="" method="post" name="" id="cohort-management-create-form" class="cohort-management-create-form"> + + <h3 class="form-title"><%- gettext('Add a New Cohort Group') %></h3> + + <div class="form-fields"> + <div class="cohort-management-create-form-name field field-text"> + <label for="cohort-create-name" class="label"> + <%- gettext('New Cohort Name') %> * + <span class="sr"><%- gettext('(Required Field)')%></span> + </label> + <input type="text" name="cohort-create-name" value="" class="input cohort-create-name" + id="cohort-create-name" + placeholder="<%- gettext("Enter Your New Cohort Group's Name") %>" required="required" /> + </div> + </div> + + <div class="form-actions"> + <button class="form-submit button action-primary action-save"> + <i class="icon icon-plus"></i> + <%- gettext('Save') %> + </button> + <a href="" class="form-cancel action-secondary action-cancel"><%- gettext('Cancel') %></a> + </div> + </form> +</div> diff --git a/lms/templates/instructor/instructor_dashboard_2/cohort-editor.underscore b/lms/templates/instructor/instructor_dashboard_2/cohort-editor.underscore new file mode 100644 index 0000000000000000000000000000000000000000..8b22401b70873694b898d1d463327fab93096a54 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/cohort-editor.underscore @@ -0,0 +1,66 @@ +<header class="cohort-management-group-header"> + <h3 class="group-header-title"> + <span class="title-value"><%- cohort.get('name') %></span> + <span class="group-count"><%- + interpolate( + ngettext( + '(contains 1 student)', + '(contains %(student_count)s students)', + cohort.get('user_count') + ), + { student_count: cohort.get('user_count') }, + true + ) + %></span> + </h3> + <div class="cohort-management-group-setup"> + <div class="setup-value"> + <% if (cohort.get('assignment_type') == "none") { %> + <%= gettext("Students are added to this group only when you provide their email addresses or usernames on this page.") %> + <% } else { %> + <%= gettext("Students are added to this group automatically.") %> + <% } %> + <a href="http://edx.readthedocs.org" class="incontext-help action-secondary action-help"><%= gettext("What does this mean?") %></a> + </div> + <div class="setup-actions"> + <% if (advanced_settings_url != "None") { %> + <a href="<%= advanced_settings_url %>" class="action-secondary action-edit"><%= gettext("Edit settings in Studio") %></a> + <% } %> + </div> + </div> +</header> + +<!-- individual group - form --> +<div class="cohort-management-group-add"> + <form action="" method="post" id="cohort-management-group-add-form" class="cohort-management-group-add-form"> + + <h4 class="form-title"><%- gettext('Add students to this cohort group') %></h4> + + <div class="form-introduction"> + <p><%- gettext('Note: Students can only be in one cohort group. Adding students to this group overrides any previous group assignment.') %></p> + </div> + + <div class="cohort-confirmations"></div> + <div class="cohort-errors"></div> + + <div class="form-fields"> + <div class="field field-textarea is-required"> + <label for="cohort-management-group-add-students" class="label"> + <%- gettext('Enter email addresses and/or usernames separated by new lines or commas for students to add. *') %> + <span class="sr"><%- gettext('(Required Field)') %></span> + </label> + <textarea name="cohort-management-group-add-students" id="cohort-management-group-add-students" + class="input cohort-management-group-add-students" + placeholder="<%- gettext('e.g. johndoe@example.com, JaneDoe, joeydoe@example.com') %>"></textarea> + + <span class="tip"><%- gettext('You will not get notification for emails that bounce, so please double-check spelling.') %></span> + </div> + </div> + + <div class="form-actions"> + <button class="form-submit button action-primary action-view"> + <i class="button-icon icon icon-plus"></i> <%- gettext('Add Students') %> + </button> + </div> + </form> +</div> diff --git a/lms/templates/instructor/instructor_dashboard_2/cohort-selector.underscore b/lms/templates/instructor/instructor_dashboard_2/cohort-selector.underscore new file mode 100644 index 0000000000000000000000000000000000000000..36ee2946857d47a8d591ec5398b9a4dadfeaffd8 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/cohort-selector.underscore @@ -0,0 +1,14 @@ +<% if (!selectedCohort) { %> + <option value=""><%- gettext('Select a cohort group') %></option> +<% } %> +<% _.each(cohorts, function(cohort) { %> + <% + var label = interpolate( + gettext('%(cohort_name)s (%(user_count)s)'), + { cohort_name: cohort.get('name'), user_count: cohort.get('user_count') }, + true + ); + var isSelected = selectedCohort && selectedCohort.get('id') === cohort.get('id') + %> + <option value="<%- cohort.get('id') %>" <%= isSelected ? 'selected' : '' %>><%- label %></option> +<% }); %> diff --git a/lms/templates/instructor/instructor_dashboard_2/cohorts.underscore b/lms/templates/instructor/instructor_dashboard_2/cohorts.underscore new file mode 100644 index 0000000000000000000000000000000000000000..2cd0f98bc06bea9ca56d81dabfd9e058de5ba8c4 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/cohorts.underscore @@ -0,0 +1,27 @@ +<h2 class="section-title"> + <span class="value"><%- gettext('Cohort Management') %></span> + <span class="description"></span> +</h2> + +<!-- nav --> +<div class="cohort-management-nav"> + <form action="" method="post" name="" id="cohort-management-nav-form" class="cohort-management-nav-form"> + <div class="cohort-management-nav-form-select field field-select"> + <label for="cohort-select" class="label sr">${_("Select a cohort group to manage")}</label> + <select class="input cohort-select" name="cohort-select" id="cohort-select"> + </select> + </div> + + <div class="form-actions"> + <button class="form-submit button action-primary action-view sr"><%- gettext('View Cohort Group') %></button> + </div> + </form> + + <a href="" class="action-primary action-create"> + <i class="icon icon-plus"></i> + <%- gettext('Add Cohort Group') %> + </a> +</div> + +<!-- individual group --> +<div class="cohort-management-group"></div> diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index 4b12593e2caa8bb29e261fd1b1188fea80d6a8d5..931f424b80a7530dc1f40ad071e6b08150d18ce2 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -32,13 +32,12 @@ window.Range.prototype = { }; } </script> - <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> + <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js')}"></script> - <script type="text/javascript" src="${static.url('js/course_groups/cohorts.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery.event.drag-2.2.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery.event.drop-2.2.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/slick.core.js')}"></script> @@ -50,6 +49,24 @@ <script type="text/javascript" src="${static.url('js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js')}"></script> <%static:js group='module-descriptor-js'/> <%static:js group='instructor_dash'/> + <%static:js group='application'/> + + ## Backbone classes declared explicitly until RequireJS is supported + <script type="text/javascript" src="${static.url('js/models/notification.js')}"></script> + <script type="text/javascript" src="${static.url('js/models/cohort.js')}"></script> + <script type="text/javascript" src="${static.url('js/collections/cohort.js')}"></script> + <script type="text/javascript" src="${static.url('js/views/notification.js')}"></script> + <script type="text/javascript" src="${static.url('js/views/cohort_editor.js')}"></script> + <script type="text/javascript" src="${static.url('js/views/cohorts.js')}"></script> +</%block> + +## Include Underscore templates +<%block name="header_extras"> +% for template_name in ["cohorts", "cohort-editor", "cohort-selector", "add-cohort-form", "notification"]: +<script type="text/template" id="${template_name}-tpl"> + <%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" /> +</script> +% endfor </%block> ## NOTE that instructor is set as the active page so that the instructor button lights up, even though this is the instructor_2 page. @@ -60,44 +77,44 @@ <script language="JavaScript" type="text/javascript"></script> <section class="container"> -<div class="instructor-dashboard-wrapper-2"> - <section class="instructor-dashboard-content-2" id="instructor-dashboard-content"> - <div class="wrap-instructor-info studio-view"> - %if studio_url: - <a class="instructor-info-action" href="${studio_url}">${_("View Course in Studio")}</a> - %endif - %if settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'): - <a class="instructor-info-action" href="${ old_dashboard_url }"> ${_("Revert to Legacy Dashboard")} </a> - %endif - </div> + <div class="instructor-dashboard-wrapper-2"> + <section class="instructor-dashboard-content-2" id="instructor-dashboard-content"> + <div class="wrap-instructor-info studio-view"> + %if studio_url: + <a class="instructor-info-action" href="${studio_url}">${_("View Course in Studio")}</a> + %endif + %if settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'): + <a class="instructor-info-action" href="${ old_dashboard_url }"> ${_("Revert to Legacy Dashboard")} </a> + %endif + </div> - <h1>${_("Instructor Dashboard")}</h1> + <h1>${_("Instructor Dashboard")}</h1> - %if analytics_dashboard_message: - <div class="wrapper-msg urgency-low is-shown"> - <p>${analytics_dashboard_message}</p> - </div> - %endif + %if analytics_dashboard_message: + <div class="wrapper-msg urgency-low is-shown"> + <p>${analytics_dashboard_message}</p> + </div> + %endif - ## links which are tied to idash-sections below. - ## the links are activated and handled in instructor_dashboard.coffee - ## when the javascript loads, it clicks on the first section - <ul class="instructor-nav"> - % for section_data in sections: - ## This is necessary so we don't scrape 'section_display_name' as a string. - <% dname = section_data['section_display_name'] %> - <li class="nav-item"><a href="" data-section="${ section_data['section_key'] }">${_(dname)}</a></li> - % endfor - </ul> + ## links which are tied to idash-sections below. + ## the links are activated and handled in instructor_dashboard.coffee + ## when the javascript loads, it clicks on the first section + <ul class="instructor-nav"> + % for section_data in sections: + ## This is necessary so we don't scrape 'section_display_name' as a string. + <% dname = section_data['section_display_name'] %> + <li class="nav-item"><a href="" data-section="${ section_data['section_key'] }">${_(dname)}</a></li> + % endfor + </ul> - ## each section corresponds to a section_data sub-dictionary provided by the view - ## to keep this short, sections can be pulled out into their own files + ## each section corresponds to a section_data sub-dictionary provided by the view + ## to keep this short, sections can be pulled out into their own files - % for section_data in sections: - <section id="${ section_data['section_key'] }" class="idash-section"> - <%include file="${ section_data['section_key'] }.html" args="section_data=section_data" /> + % for section_data in sections: + <section id="${ section_data['section_key'] }" class="idash-section"> + <%include file="${ section_data['section_key'] }.html" args="section_data=section_data" /> + </section> + % endfor </section> - % endfor - </section> -</div> + </div> </section> diff --git a/lms/templates/instructor/instructor_dashboard_2/membership.html b/lms/templates/instructor/instructor_dashboard_2/membership.html index 03aff4a16305bfb8aad93a761ad628ee55ce4726..6a91820f3ea85d309991f5c4759ef46aa78afbd1 100644 --- a/lms/templates/instructor/instructor_dashboard_2/membership.html +++ b/lms/templates/instructor/instructor_dashboard_2/membership.html @@ -26,8 +26,7 @@ </div> </script> -<div class="vert-left"> -<div class="batch-enrollment"> +<div class="batch-enrollment membership-section"> <h2> ${_("Batch Enrollment")} </h2> <p> <label for="student-ids"> @@ -41,7 +40,7 @@ <label for="auto-enroll">${_("Auto Enroll")}</label> <div class="hint auto-enroll-hint"> <span class="hint-caret"></span> - <p> + <p> ${_("If this option is <em>checked</em>, users who have not yet registered for {platform_name} will be automatically enrolled.").format(platform_name=settings.PLATFORM_NAME)} ${_("If this option is left <em>unchecked</em>, users who have not yet registered for {platform_name} will not be enrolled, but will be allowed to enroll once they make an account.").format(platform_name=settings.PLATFORM_NAME)} <br /><br /> @@ -55,7 +54,7 @@ <label for="email-students">${_("Notify users by email")}</label> <div class="hint email-students-hint"> <span class="hint-caret"></span> - <p> + <p> ${_("If this option is <em>checked</em>, users will receive an email notification.")} </p> </div> @@ -69,8 +68,10 @@ <div class="request-response-error"></div> </div> +<hr class="divider" /> + %if section_data['access']['instructor']: -<div class="batch-beta-testers"> +<div class="batch-beta-testers membership-section"> <h2> ${_("Batch Beta Tester Addition")} </h2> <p> <label for="student-ids-for-beta"> @@ -111,10 +112,11 @@ <div class="request-response"></div> <div class="request-response-error"></div> </div> + +<hr class="divider" /> %endif -</div> -<div class="vert-right member-lists-management"> +<div class="member-lists-management membership-section"> ## Translators: an "Administration List" is a list, such as Course Staff, that users can be added to. <h2> ${_("Administration List Management")} </h2> @@ -216,3 +218,32 @@ %endif </div> + +%if course.is_cohorted: + <hr class="divider" /> + <div class="cohort-management membership-section" + data-ajax_url="${section_data['cohorts_ajax_url']}" + data-advanced-settings-url="${section_data['advanced_settings_url']}" + > + </div> + + <%block name="headextra"> + <script> + $(document).ready(function() { + var cohortManagementElement = $('.cohort-management'); + if (cohortManagementElement.length > 0) { + var cohorts = new CohortCollection(); + cohorts.url = cohortManagementElement.data('ajax_url'); + var cohortsView = new CohortsView({ + el: cohortManagementElement, + model: cohorts, + advanced_settings_url: cohortManagementElement.data('advanced-settings-url') + }); + cohorts.fetch().done(function() { + cohortsView.render(); + }); + } + }); + </script> + </%block> +% endif diff --git a/lms/templates/instructor/instructor_dashboard_2/notification.underscore b/lms/templates/instructor/instructor_dashboard_2/notification.underscore new file mode 100644 index 0000000000000000000000000000000000000000..48b9b59be4c01a08faa0babc8cd8725640619c1c --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/notification.underscore @@ -0,0 +1,31 @@ +<div class="message message-<%= type %>"> + <h3 class="message-title"> + <%- title %> + </h3> + + <% if (details.length > 0 || message) { %> + <div class="message-copy"> + <% if (message) { %> + <p><%- message %></p> + <% } %> + <% if (details.length > 0) { %> + <ul class="list-summary summary-items"> + <% for (var i = 0; i < details.length; i++) { %> + <li class="summary-item"><%- details[i] %></li> + <% } %> + </ul> + <% } %> + </div> + <% } %> + + <% if (actionText) { %> + <div class="message-actions"> + <button class="action-primary <%- actionClass %>"> + <% if (actionIconClass) { %> + <i class="icon <%- actionIconClass %>"></i> + <% } %> + <%- actionText %> + </button> + </div> + <% } %> +</div> diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index dfec9af5f8ddfe1b31bac0ca1a907049fa2f86e3..dbf8536c18ce0b557fb949ad5981c18fc4a46d44 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -6,7 +6,6 @@ <%block name="headextra"> <%static:css group='style-course-vendor'/> <%static:css group='style-course'/> -<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> </%block> <%block name="pagetitle">${_("{course_number} Staff Grading").format(course_number=course.display_number_with_default) | h}</%block> diff --git a/lms/templates/ux/reference/instructor_dashboard/membership.html b/lms/templates/ux/reference/instructor_dashboard/membership.html index 3a1370aeae874e0d06992112e25f10ac66bd284d..9a528c1b726c7af98eb1f18b66b9beb10f5be51d 100644 --- a/lms/templates/ux/reference/instructor_dashboard/membership.html +++ b/lms/templates/ux/reference/instructor_dashboard/membership.html @@ -19,7 +19,6 @@ window.Range.prototype = { }; } </script> -<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script> @@ -44,326 +43,422 @@ <section class="instructor-dashboard-content-2" id="instructor-dashboard-content"> <h1>Instructor Dashboard</h1> - <div class="batch-enrollment membership-section"> - <h2> Batch Enrollment </h2> - <p> - <label for="student-ids"> - Enter email addresses and/or usernames separated by new lines or commas. - You will not get notification for emails that bounce, so please double-check spelling. </label> - <textarea rows="6" name="student-ids" placeholder="Email Addresses/Usernames" spellcheck="false"></textarea> - </p> - - <div class="enroll-option"> - <input type="checkbox" name="auto-enroll" value="Auto-Enroll" checked="yes"> - <label for="auto-enroll">Auto Enroll</label> - <div class="hint auto-enroll-hint"> - <span class="hint-caret"></span> - <p> - If this option is <em>checked</em>, users who have not yet registered for {platform_name} will be automatically enrolled.").format(platform_name=settings.PLATFORM_NAME)} - If this option is left <em>unchecked</em>, users who have not yet registered for {platform_name} will not be enrolled, but will be allowed to enroll once they make an account.").format(platform_name=settings.PLATFORM_NAME)} - <br /><br /> - Checking this box has no effect if 'Unenroll' is selected. - </p> + <section class="idash-section active-section" id="membership"> + + <div class="batch-enrollment membership-section"> + <h2> Batch Enrollment </h2> + <p> + <label for="student-ids"> + Enter email addresses and/or usernames separated by new lines or commas. + You will not get notification for emails that bounce, so please double-check spelling. </label> + <textarea rows="6" name="student-ids" placeholder="Email Addresses/Usernames" spellcheck="false"></textarea> + </p> + + <div class="enroll-option"> + <input type="checkbox" name="auto-enroll" value="Auto-Enroll" checked="yes"> + <label for="auto-enroll">Auto Enroll</label> + <div class="hint auto-enroll-hint"> + <span class="hint-caret"></span> + <p> + If this option is <em>checked</em>, users who have not yet registered for {platform_name} will be automatically enrolled.").format(platform_name=settings.PLATFORM_NAME)} + If this option is left <em>unchecked</em>, users who have not yet registered for {platform_name} will not be enrolled, but will be allowed to enroll once they make an account.").format(platform_name=settings.PLATFORM_NAME)} + <br /><br /> + Checking this box has no effect if 'Unenroll' is selected. + </p> + </div> </div> - </div> - <div class="enroll-option"> - <input type="checkbox" name="email-students" value="Notify-students-by-email" checked="yes"> - <label for="email-students">Notify users by email</label> - <div class="hint email-students-hint"> - <span class="hint-caret"></span> - <p> - If this option is <em>checked</em>, users will receive an email notification. - </p> + <div class="enroll-option"> + <input type="checkbox" name="email-students" value="Notify-students-by-email" checked="yes"> + <label for="email-students">Notify users by email</label> + <div class="hint email-students-hint"> + <span class="hint-caret"></span> + <p> + If this option is <em>checked</em>, users will receive an email notification. + </p> + </div> </div> - </div> - <div> - <input type="button" name="enrollment-button" class="enrollment-button" value="Enroll" data-action="enroll" > - <input type="button" name="enrollment-button" class="enrollment-button" value="Unenroll" data-action="unenroll" > + <div> + <input type="button" name="enrollment-button" class="enrollment-button" value="Enroll" data-action="enroll" > + <input type="button" name="enrollment-button" class="enrollment-button" value="Unenroll" data-action="unenroll" > + </div> + <div class="request-response"></div> + <div class="request-response-error"></div> </div> - <div class="request-response"></div> - <div class="request-response-error"></div> - </div> - <hr class="divider" /> - - <div class="batch-beta-testers membership-section"> - <h2> Batch Beta Tester Addition </h2> - <p> - <label for="student-ids-for-beta"> - Enter email addresses and/or usernames separated by new lines or commas.<br/> - Note: Users must have an activated {platform_name} account before they can be enrolled as a beta tester.").format(platform_name=settings.PLATFORM_NAME)} - </label> - - <textarea rows="6" cols="50" name="student-ids-for-beta" placeholder="Email Addresses/Usernames" spellcheck="false"></textarea> - </p> - - <div class="enroll-option"> - <input type="checkbox" name="auto-enroll" value="Auto-Enroll" checked="yes"> - <label for="auto-enroll-beta">Auto Enroll</label> - <div class="hint auto-enroll-beta-hint"> - <span class="hint-caret"></span> - <p> - If this option is <em>checked</em>, users who have not enrolled in your course will be automatically enrolled. - <br /><br /> - Checking this box has no effect if 'Remove beta testers' is selected. - </p> + <hr class="divider" /> + + <div class="batch-beta-testers membership-section"> + <h2> Batch Beta Tester Addition </h2> + <p> + <label for="student-ids-for-beta"> + Enter email addresses and/or usernames separated by new lines or commas.<br/> + Note: Users must have an activated {platform_name} account before they can be enrolled as a beta tester.").format(platform_name=settings.PLATFORM_NAME)} + </label> + + <textarea rows="6" cols="50" name="student-ids-for-beta" placeholder="Email Addresses/Usernames" spellcheck="false"></textarea> + </p> + + <div class="enroll-option"> + <input type="checkbox" name="auto-enroll" value="Auto-Enroll" checked="yes"> + <label for="auto-enroll-beta">Auto Enroll</label> + <div class="hint auto-enroll-beta-hint"> + <span class="hint-caret"></span> + <p> + If this option is <em>checked</em>, users who have not enrolled in your course will be automatically enrolled. + <br /><br /> + Checking this box has no effect if 'Remove beta testers' is selected. + </p> + </div> + </div> + + <div class="enroll-option"> + <input type="checkbox" name="email-students-beta" value="Notify-students-by-email" checked="yes"> + <label for="email-students-beta">Notify users by email</label> + <div class="hint email-students-beta-hint"> + <span class="hint-caret"></span> + <p> If this option is <em>checked</em>, users will receive an email notification.</p> + </div> </div> - </div> - <div class="enroll-option"> - <input type="checkbox" name="email-students-beta" value="Notify-students-by-email" checked="yes"> - <label for="email-students-beta">Notify users by email</label> - <div class="hint email-students-beta-hint"> - <span class="hint-caret"></span> - <p> If this option is <em>checked</em>, users will receive an email notification.</p> + <div> + <input type="button" name="beta-testers" class="enrollment-button" value="Add beta testers" data-action="add" > + <input type="button" name="beta-testers" class="enrollment-button" value="Remove beta testers" data-action="remove" > </div> - </div> - <div> - <input type="button" name="beta-testers" class="enrollment-button" value="Add beta testers" data-action="add" > - <input type="button" name="beta-testers" class="enrollment-button" value="Remove beta testers" data-action="remove" > + <div class="request-response"></div> + <div class="request-response-error"></div> </div> - <div class="request-response"></div> - <div class="request-response-error"></div> - </div> - - <hr class="divider" /> + <hr class="divider" /> - <div class="member-lists-management membership-section"> - ## Translators: an "Administration List" is a list, such as Course Staff, that users can be added to. - <h2> Administration List Management </h2> + <div class="member-lists-management membership-section"> + ## Translators: an "Administration List" is a list, such as Course Staff, that users can be added to. + <h2> Administration List Management </h2> - <div class="request-response-error"></div> + <div class="request-response-error"></div> - <div class="wrapper-member-select"> - ## Translators: an "Administrator Group" is a group, such as Course Staff, that users can be added to. - <label for="member-lists-selector">Select an Administrator Group:</label> - <select id="member-lists-selector" class="member-lists-selector"> - <option> Getting available lists... </option> - </select> + <div class="wrapper-member-select"> + ## Translators: an "Administrator Group" is a group, such as Course Staff, that users can be added to. + <label for="member-lists-selector">Select an Administrator Group:</label> + <select id="member-lists-selector" class="member-lists-selector"> + <option> Getting available lists... </option> + </select> - </div> + </div> - <p> - Staff cannot modify staff or beta tester lists. To modify these lists, " - "contact your instructor and ask them to add you as an instructor for staff " - "and beta lists, or a discussion admin for discussion management. - </p> + <p> + Staff cannot modify staff or beta tester lists. To modify these lists, " + "contact your instructor and ask them to add you as an instructor for staff " + "and beta lists, or a discussion admin for discussion management. + </p> + + <div class="auth-list-container" + data-rolename="staff" + data-display-name="Course Staff" + data-info-text=" + Course staff can help you manage limited aspects of your course. Staff " + "can enroll and unenroll students, as well as modify their grades and " + "see all course data. Course staff are not automatically given access " + "to Studio and will not be able to edit your course." + data-list-endpoint="" + data-modify-endpoint="" + data-add-button-label="Add Staff" + ></div> <div class="auth-list-container" - data-rolename="staff" - data-display-name="Course Staff" + data-rolename="instructor" + data-display-name="Instructors" data-info-text=" - Course staff can help you manage limited aspects of your course. Staff " - "can enroll and unenroll students, as well as modify their grades and " - "see all course data. Course staff are not automatically given access " - "to Studio and will not be able to edit your course." + Instructors are the core administration of your course. Instructors can " + "add and remove course staff, as well as administer discussion access." data-list-endpoint="" data-modify-endpoint="" - data-add-button-label="Add Staff" + data-add-button-label="Add Instructor" ></div> - <div class="auth-list-container" - data-rolename="instructor" - data-display-name="Instructors" - data-info-text=" - Instructors are the core administration of your course. Instructors can " - "add and remove course staff, as well as administer discussion access." - data-list-endpoint="" - data-modify-endpoint="" - data-add-button-label="Add Instructor" - ></div> - - <div class="auth-list-container" - data-rolename="beta" - data-display-name="Beta Testers" - data-info-text=" - Beta testers can see course content before the rest of the students. " - "They can make sure that the content works, but have no additional " - "privileges." - data-list-endpoint="" - data-modify-endpoint="" - data-add-button-label="Add Beta Tester" - ></div> - - <div class="auth-list-container" - data-rolename="Administrator" - data-display-name="Discussion Admins" - data-info-text=" - Discussion admins can edit or delete any post, clear misuse flags, close " - "and re-open threads, endorse responses, and see posts from all cohorts. " - "They CAN add/delete other moderators and their posts are marked as 'staff'." - data-list-endpoint="" - data-modify-endpoint="" - data-add-button-label="Add Discussion Admin" - ></div> - - <div class="auth-list-container" - data-rolename="Moderator" - data-display-name="Discussion Moderators" - data-info-text=" - Discussion moderators can edit or delete any post, clear misuse flags, close " - "and re-open threads, endorse responses, and see posts from all cohorts. " - "They CANNOT add/delete other moderators and their posts are marked as 'staff'." - data-list-endpoint="" - data-modify-endpoint="" - data-add-button-label="Add Moderator" - ></div> - - <div class="auth-list-container" - data-rolename="Community TA" - data-display-name="Discussion Community TAs" - data-info-text=" - Community TA's are members of the community whom you deem particularly " - "helpful on the discussion boards. They can edit or delete any post, clear misuse flags, " - "close and re-open threads, endorse responses, and see posts from all cohorts. " - "Their posts are marked 'Community TA'." - data-list-endpoint="" - data-modify-endpoint="" - data-add-button-label="Add Community TA" - ></div> - </div> + <div class="auth-list-container" + data-rolename="beta" + data-display-name="Beta Testers" + data-info-text=" + Beta testers can see course content before the rest of the students. " + "They can make sure that the content works, but have no additional " + "privileges." + data-list-endpoint="" + data-modify-endpoint="" + data-add-button-label="Add Beta Tester" + ></div> + + <div class="auth-list-container" + data-rolename="Administrator" + data-display-name="Discussion Admins" + data-info-text=" + Discussion admins can edit or delete any post, clear misuse flags, close " + "and re-open threads, endorse responses, and see posts from all cohorts. " + "They CAN add/delete other moderators and their posts are marked as 'staff'." + data-list-endpoint="" + data-modify-endpoint="" + data-add-button-label="Add Discussion Admin" + ></div> + + <div class="auth-list-container" + data-rolename="Moderator" + data-display-name="Discussion Moderators" + data-info-text=" + Discussion moderators can edit or delete any post, clear misuse flags, close " + "and re-open threads, endorse responses, and see posts from all cohorts. " + "They CANNOT add/delete other moderators and their posts are marked as 'staff'." + data-list-endpoint="" + data-modify-endpoint="" + data-add-button-label="Add Moderator" + ></div> + + <div class="auth-list-container" + data-rolename="Community TA" + data-display-name="Discussion Community TAs" + data-info-text=" + Community TA's are members of the community whom you deem particularly " + "helpful on the discussion boards. They can edit or delete any post, clear misuse flags, " + "close and re-open threads, endorse responses, and see posts from all cohorts. " + "Their posts are marked 'Community TA'." + data-list-endpoint="" + data-modify-endpoint="" + data-add-button-label="Add Community TA" + ></div> + </div> - <hr class="divider" /> - - <div class="cohort-management membership-section"> - <h2 class="section-title"> - <span class="value">Cohort Management</span> - <span class="description">Cohorts are such and such and are used for this and that to do all the things</span> - </h2> - - <!-- nav --> - <div class="cohort-management-nav"> - <form action="" method="post" name="" id="cohort-management-nav-form" class="cohort-management-nav-form"> - - <div class="cohort-management-nav-form-select field field-select"> - <label for="cohort-select" class="label sr">Select a cohort to manage</label> - - <select class="input cohort-select" name="cohort-select" id="cohort-select"> - <option value="cohort-name-1">Cohort Name is Placed Here and Should Accommodate Almost Everything (12,546)</option> - <option value="cohort-name-2" selected>Cras mattis consectetur purus sit amet fermentum (8,546)</option> - <option value="cohort-name-3">Donec id elit non mi porta gravida at eget metus. (4) - </option> - <option value="cohort-name-4">Donec id elit non mi porta gravida at eget metus. (4) - </option> - </select> - </div> + <hr class="divider" /> - <button class="form-submit button action-primary action-view sr">View cohort group</button> - </form> - </div> + <div class="cohort-management membership-section"> + <h2 class="section-title"> + <span class="value">Cohort Management</span> + <span class="description">Cohorts are such and such and are used for this and that to do all the things</span> + </h2> - <!-- message - error - no groups --> - <div class="message message-warning is-shown"> - <h3 class="message-title">You currently have no cohort groups configured</h3> + <!-- nav --> + <div class="cohort-management-nav"> + <form action="" method="post" name="" id="cohort-management-nav-form" class="cohort-management-nav-form"> - <div class="message-copy"> - <p>Please complete your cohort group configuration by creating groups within Studio</p> - </div> + <div class="form-fields"> + <div class="cohort-management-nav-form-select field field-select"> + <label for="cohort-select" class="label sr">Select a cohort group to manage</label> + + <select class="input cohort-select" name="cohort-select" id="cohort-select"> + <option>Select a cohort group</option> + <option value="cohort-name-1">Cohort Name is Placed Here and Should Accommodate Almost Everything (12,546)</option> + <option value="cohort-name-2" selected>Cras mattis consectetur purus sit amet fermentum (8,546)</option> + <option value="cohort-name-3">Donec id elit non mi porta gravida at eget metus. (4) + </option> + <option value="cohort-name-4">Donec id elit non mi porta gravida at eget metus. (4) + </option> + </select> + </div> + </div> + + <div class="form-actions"> + <button class="form-submit button action-primary action-view sr">View cohort group</button> + </div> + </form> - <div class="message-actions"> - <a href="" class="action-primary action-review">Revise Configuration in Studio</a> + <a href="" class="action-primary action-create"> + <i class="icon-plus"></i> + Add Cohort Group + </a> </div> - </div> - <!-- message - error - bad configuration --> - <div class="message message-error is-shown"> - <h3 class="message-title">There's currently an error with your cohorts configuration within this course.</h3> + <!-- message - error - no groups --> + <div class="message message-warning is-shown"> + <h3 class="message-title">You currently have no cohort groups configured</h3> + + <div class="message-copy"> + <p>Please complete your cohort group configuration by creating groups within Studio</p> + </div> - <div class="message-copy"> - <p>Error output (if any and near-human legible/comprehendable can be displayed here)</p> + <div class="message-actions"> + <button class="action-primary action-create"> + <i class="icon icon-plus"></i> + Add a Cohort Group + </button> + </div> </div> - <div class="message-actions"> - <a href="" class="action-primary action-review">Review Configuration in Studio</a> + <!-- message - error - bad configuration --> + <div class="message message-error"> + <h3 class="message-title">There's currently an error with your cohorts configuration within this course.</h3> + + <div class="message-copy"> + <p>Error output (if any and near-human legible/comprehendable can be displayed here)</p> + </div> + + <div class="message-actions"> + <a href="" class="action-primary action-review">Review Configuration in Studio</a> + </div> </div> - </div> - <!-- individual group --> - <div class="cohort-management-group"> - <header class="cohort-management-group-header"> - <h3 class="group-header-title"> - <span class="title-value">Cohort Name Can be Placed Here and Should Accommodate Almost Everything</span> - <span class="group-count">(contains 12,546 Students)</span> - </h3> - - <div class="cohort-management-group-setup"> - <h4 class="sr">This cohort group's current management set up:</h4> - <div class="setup-value"> - Students are added to this group automatically - <a href="" class="incontext-help action-secondary action-help">What does this mean?</a> + <!-- adding new group --> + <div class="cohort-management-create"> + <form action="" method="post" name="" id="cohort-management-create-form" class="cohort-management-create-form"> + + <h3 class="form-title">Add a New Cohort Group</h3> + + <div class="form-fields"> + <div class="cohort-management-create-form-name field field-text"> + <label for="cohort-create-name" class="label"> + New Cohort Name * + <span class="sr">(Required Field)</span> + </label> + <input type="text" name="cohort-create-name" value="" class=" input cohort-create-name" id="cohort-create-name" placeholder="Enter Your New Cohort Group's Name" required="required" /> + </div> </div> - <div class="setup-actions"> - <a href="" class="action-secondary action-edit">Edit settings in Studio</a> + <div class="form-actions"> + <button class="form-submit button action-primary action-save"> + <i class="icon icon-plus"></i> + Save + </button> + <a href="" class="form-cancel action-secondary action-cancel">Cancel</a> </div> - </div> - </header> + </form> + </div> - <!-- individual group - form --> - <div class="cohort-management-group-add"> - <form action="" method="post" name="" id="cohort-management-group-add-form" class="cohort-management-group-add-form"> + <!-- editing group --> + <div class="cohort-management-edit"> + <form action="" method="post" name="" id="cohort-management-edit-form" class="cohort-management-edit-form"> - <h4 class="form-title">Add students to this cohort group</h4> + <h3 class="form-title">Editing "Cohort Group's Name"</h3> - <div class="form-introduction"> - <p>Please Note: Adding a student to this group will remove them from any groups they are currently a part of.</p> + <div class="form-fields"> + <div class="cohort-management-edit-form-name field field-text"> + <label for="cohort-create-name" class="label"> + Cohort Name * + <span class="sr">(Required Field)</span> + </label> + <input type="text" name="cohort-edit-name" value="" class=" input cohort-edit-name" id="cohort-edit-name" placeholder="Enter Your Cohort Group's Name" required="required" /> + </div> </div> - <!-- individual group - form message - confirmation --> - <div class="message message-confirmation is-shown"> - <h3 class="message-title">2,546 students have been added to this cohort group</h3> + <div class="form-actions"> + <button class="form-submit button action-primary action-save"> + <i class="icon icon-plus"></i> + Save + </button> + <a href="" class="form-cancel action-secondary action-cancel">Cancel</a> + </div> + </form> + </div> + + <!-- create/edit cohort group messages --> + <div class="message message-confirmation"> + <h3 class="message-title">New Cohort Name has been created. You can manually add students to this group below.</h3> + </div> + + <!-- individual group --> + <div class="cohort-management-group"> + <header class="cohort-management-group-header"> + <h3 class="group-header-title"> + <span class="title-value">Cohort Name Can be Placed Here and Should Accommodate Almost Everything</span> + <span class="group-count">(contains 12,546 Students)</span> + + <a href="" class="action-secondary action-edit action-edit-name">Edit</a> + </h3> + + <div class="cohort-management-group-setup"> + <h4 class="sr">This cohort group's current management set up:</h4> + <div class="setup-value"> + Students are added to this group automatically. + <a href="" class="incontext-help action-secondary action-help">What does this mean?</a> + </div> - <div class="message-copy"> - <ul class="list-summary summary-items"> - <li class="summary-item">1,245 were removed from Cohort Name is Placed Here and Should Accommodate Almost Everything</li> - <li class="summary-item">1,245 were removed from Cohort Name is Placed Here and Should Accommodate Almost Everything</li> - <li class="summary-item">1,245 were removed from Cohort Name is Placed Here and Should Accommodate Almost Everything</li> - </ul> + <div class="setup-actions"> + <a href="" class="action-secondary action-edit">Edit settings in Studio</a> </div> </div> - <!-- individual group - form message - error (collapsed) --> - <div class="message message-error is-shown"> - <h3 class="message-title">There were 25 errors when trying to add students:</h3> - - <div class="message-copy"> - <ul class="list-summary summary-items"> - <li class="summary-item">Unknown user: ahgaeubgoq</li> - <li class="summary-item">Unknown user: hagaihga</li> - <li class="summary-item">Unknown user: ahgaeubgoq</li> - <li class="summary-item">Unknown user: ahgaeubgoq</li> - <li class="summary-item">Unknown user: hagaihga</li> - </ul> + <div class="cohort-management-group-setup"> + <h4 class="sr">This cohort group's current management set up:</h4> + <div class="setup-value"> + Students are added to this group only when you provide their email addresses or usernames on this page. + <a href="" class="incontext-help action-secondary action-help">What does this mean?</a> </div> - <div class="message-actions"> - <a href="" class="action-primary action-expand">View all errors</a> + <div class="setup-actions"> + <a href="" class="action-secondary action-edit">Edit settings in Studio</a> </div> </div> + </header> - <div class="form-fields"> - <div class="field field-textarea is-required"> - <label for="cohort-management-group-add-students" class="label">Student ID or Email Address of Students to be added (one per line or comma-separated)</label> + <!-- individual group - form --> + <div class="cohort-management-group-add"> + <form action="" method="post" name="" id="cohort-management-group-add-form" class="cohort-management-group-add-form"> + + <h4 class="form-title">Add students to this cohort group</h4> - <textarea name="cohort-management-group-add-students" id="cohort-management-group-add-students" class="input cohort-management-group-add-students" placeholder="e.g. johndoe@example.com, JaneDoe, joeydoe@example.com"></textarea> + <div class="form-introduction"> + <p>Note: Students can only be in one cohort group. Adding students to this group overrides any previous group assignment.</p> </div> - </div> - <button class="form-submit button action-primary action-view"> - <i class="button-icon icon icon-plus"></i> Add Students - </button> - </form> + <!-- individual group - form message - confirmation --> + <div class="message message-confirmation"> + <h3 class="message-title">2,546 students have been added to this cohort group</h3> + + <div class="message-copy"> + <ul class="list-summary summary-items"> + <li class="summary-item">1,245 were removed from Cohort Name is Placed Here and Should Accommodate Almost Everything</li> + <li class="summary-item">1,245 were removed from Cohort Name is Placed Here and Should Accommodate Almost Everything</li> + <li class="summary-item">1,245 were removed from Cohort Name is Placed Here and Should Accommodate Almost Everything</li> + </ul> + </div> + </div> + + <!-- individual group - form message - error (collapsed) --> + <div class="message message-error"> + <h3 class="message-title">There were 25 errors when trying to add students:</h3> + + <div class="message-copy"> + <ul class="list-summary summary-items"> + <li class="summary-item">Unknown user: ahgaeubgoq</li> + <li class="summary-item">Unknown user: hagaihga</li> + <li class="summary-item">Unknown user: ahgaeubgoq</li> + <li class="summary-item">Unknown user: ahgaeubgoq</li> + <li class="summary-item">Unknown user: hagaihga</li> + </ul> + </div> + + <div class="message-actions"> + <a href="" class="action-primary action-expand">View all errors</a> + </div> + </div> + + <div class="form-fields"> + <div class="field field-textarea is-required"> + <label for="cohort-management-group-add-students" class="label"> + Enter email addresses and/or usernames separated by new lines or commas for students to add. * + <span class="sr">(Required Field)</span> + </label> + + <textarea name="cohort-management-group-add-students" id="cohort-management-group-add-students" class="input cohort-management-group-add-students" placeholder="e.g. johndoe@example.com, JaneDoe, joeydoe@example.com" required="required"></textarea> + + <span class="tip">You will not get notification for emails that bounce, so please double-check spelling.</span> + </div> + </div> + + <div class="form-actions"> + <button class="form-submit button action-primary action-add"> + <i class="button-icon icon icon-plus"></i> Add Students + </button> + </div> + </form> + </div> </div> - </div> - <!-- cta - view download --> - <div class="cohort-management-supplemental"> - <p class=""><i class="icon icon-info-sign"></i> You may view individual student information for each cohort via your entire course profile data download on <a href="" class="link-cross-reference">the data download view</a></p> + <!-- cta - view download --> + <div class="cohort-management-supplemental"> + <p class=""><i class="icon icon-info-sign"></i> You may view individual student information for each cohort via your entire course profile data download on <a href="" class="link-cross-reference">the data download view</a></p> + </div> </div> + </section> + </section> </div> </section> diff --git a/pavelib/utils/envs.py b/pavelib/utils/envs.py index 25367acfc7c002f8897516fea6dbf9f072e73ff0..ca4fb123126eba65394b0275d60eb8a2479825b5 100644 --- a/pavelib/utils/envs.py +++ b/pavelib/utils/envs.py @@ -88,6 +88,7 @@ class Env(object): # reason. See issue TE-415. JS_TEST_ID_FILES = [ REPO_ROOT / 'lms/static/js_test.yml', + REPO_ROOT / 'lms/static/js_test_coffee.yml', REPO_ROOT / 'cms/static/js_test.yml', REPO_ROOT / 'cms/static/js_test_squire.yml', REPO_ROOT / 'common/lib/xmodule/xmodule/js/js_test.yml', @@ -96,6 +97,7 @@ class Env(object): JS_TEST_ID_KEYS = [ 'lms', + 'lms-coffee', 'cms', 'cms-squire', 'xmodule',