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',