Commit 7f4cc1c7 by Christina Roberts

Merge pull request #5407 from edx/cohort-management

Cohorts UI
parents 348c7a3c 6219ecac
......@@ -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).
......
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"
......
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()
......
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")
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.
......
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]
......
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()
......
../../../common/static/js/spec_helpers
\ No newline at end of file
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');
......
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(
......
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 () {
......
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 () {
......
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;
......
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);
......
......@@ -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'
......
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();
});
});
});
......
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);
});
......
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);
});
});
});
......
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);
};
......
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, {
......
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() {
......
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);
......
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);
});
......
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');
......
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');
......
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);
});
......
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/);
});
});
});
......
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: []
});
......
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());
......
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 () {
......
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();
});
});
});
......
/**
* 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": []
});
......
/**
* 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,
......
/**
* 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,
......
/**
* 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,
......
......@@ -67,6 +67,7 @@ lib_paths:
src_paths:
- coffee/src
- js
- js/common_helpers
# Paths to spec (test) JavaScript files
spec_paths:
......
......@@ -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.
......
......@@ -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()
"""
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
......
......@@ -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,
......
......@@ -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)
......@@ -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})
......
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');
});
});
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
};
});
/**
* 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
};
});
// 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, _);
......@@ -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)
......
# -*- 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")
......@@ -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")
# -*- 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()
......@@ -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
......
......@@ -29,7 +29,7 @@ from ...fixtures.discussion import (
SearchResult,
)
from helpers import BaseDiscussionMixin
from .helpers import BaseDiscussionMixin
class DiscussionResponsePaginationTestMixin(BaseDiscussionMixin):
......
# -*- 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()
......
# -*- 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()
# -*- 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.
......
# -*- 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
......
# -*- 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
......
......@@ -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
......
......@@ -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'),
......
......@@ -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',
},
......
(function(Backbone) {
var CohortCollection = Backbone.Collection.extend({
model : this.CohortModel,
comparator: "name",
parse: function(response) {
return response.cohorts;
}
});
this.CohortCollection = CohortCollection;
}).call(this, Backbone);
../../../common/static/js/spec_helpers
\ No newline at end of file
(function(Backbone) {
var CohortModel = Backbone.Model.extend({
idAttribute: 'id',
defaults: {
name: '',
user_count: 0
}
});
this.CohortModel = CohortModel;
}).call(this, Backbone);
(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);
(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);
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
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
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');
});
});
});
});
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);
});
});
});
(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);
(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);
(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, $, _);
---
# 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:
- .*
---
# 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/*
......@@ -300,6 +300,11 @@ mark {
@extend %ui-disabled;
}
// UI - is hidden
.is-hidden {
display: none;
}
// UI - semantically hide text
.sr {
@extend %text-sr;
......
......@@ -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 {
......
......@@ -11,10 +11,6 @@
background: $notify-banner-bg-1;
padding: $baseline ($baseline*1.5);
&.is-hidden {
display: none;
}
// basic object
.msg {
@include clearfix();
......
../templates
\ No newline at end of file
......@@ -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>
......
<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>
<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>
<% 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>
<% }); %>
<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>
......@@ -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>
......@@ -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
<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>
......@@ -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>
......
......@@ -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>
......@@ -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',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment