Merge pull request #4782 from edx/reruns/ui

Reruns/ui
This commit is contained in:
Ben McMorran
2014-08-21 16:57:00 -04:00
55 changed files with 2766 additions and 376 deletions

View File

@@ -233,6 +233,8 @@ define([
"js/spec/views/pages/container_subviews_spec",
"js/spec/views/pages/group_configurations_spec",
"js/spec/views/pages/course_outline_spec",
"js/spec/views/pages/course_rerun_spec",
"js/spec/views/pages/index_spec",
"js/spec/views/modals/base_modal_spec",
"js/spec/views/modals/edit_xblock_spec",

View File

@@ -1,21 +1,29 @@
require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
function (domReady, $, _, CancelOnEscape) {
define(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape", "js/views/utils/create_course_utils",
"js/views/utils/view_utils"],
function (domReady, $, _, CancelOnEscape, CreateCourseUtilsFactory, ViewUtils) {
var CreateCourseUtils = CreateCourseUtilsFactory({
name: '.new-course-name',
org: '.new-course-org',
number: '.new-course-number',
run: '.new-course-run',
save: '.new-course-save',
errorWrapper: '.wrap-error',
errorMessage: '#course_creation_error',
tipError: 'span.tip-error',
error: '.error',
allowUnicode: '.allow-unicode-course-id'
}, {
shown: 'is-shown',
showing: 'is-showing',
hiding: 'is-hiding',
disabled: 'is-disabled',
error: 'error'
});
var saveNewCourse = function (e) {
e.preventDefault();
// One final check for empty values
var errors = _.reduce(
['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'],
function (acc, ele) {
var $ele = $(ele);
var error = validateRequiredField($ele.val());
setNewCourseFieldInErr($ele.parent('li'), error);
return error ? true : acc;
},
false
);
if (errors) {
if (CreateCourseUtils.hasInvalidRequiredFields()) {
return;
}
@@ -25,29 +33,19 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
var number = $newCourseForm.find('.new-course-number').val();
var run = $newCourseForm.find('.new-course-run').val();
analytics.track('Created a Course', {
'org': org,
'number': number,
'display_name': display_name,
'run': run
});
course_info = {
org: org,
number: number,
display_name: display_name,
run: run
};
$.postJSON('/course/', {
'org': org,
'number': number,
'display_name': display_name,
'run': run
},
function (data) {
if (data.url !== undefined) {
window.location = data.url;
} else if (data.ErrMsg !== undefined) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + data.ErrMsg + '</p>');
$('.new-course-save').addClass('is-disabled');
}
}
);
analytics.track('Created a Course', course_info);
CreateCourseUtils.createCourse(course_info, function (errorMessage) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + errorMessage + '</p>');
$('.new-course-save').addClass('is-disabled');
});
};
var cancelNewCourse = function (e) {
@@ -78,91 +76,20 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
$cancelButton.bind('click', cancelNewCourse);
CancelOnEscape($cancelButton);
// Check that a course (org, number, run) doesn't use any special characters
var validateCourseItemEncoding = function (item) {
var required = validateRequiredField(item);
if (required) {
return required;
}
if ($('.allow-unicode-course-id').val() === 'True'){
if (/\s/g.test(item)) {
return gettext('Please do not use any spaces in this field.');
}
}
else{
if (item !== encodeURIComponent(item)) {
return gettext('Please do not use any spaces or special characters in this field.');
}
}
return '';
};
// Ensure that org/course_num/run < 65 chars.
var validateTotalCourseItemsLength = function () {
var totalLength = _.reduce(
['.new-course-org', '.new-course-number', '.new-course-run'],
function (sum, ele) {
return sum + $(ele).val().length;
}, 0
);
if (totalLength > 65) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
$('.new-course-save').addClass('is-disabled');
}
else {
$('.wrap-error').removeClass('is-shown');
}
};
// Handle validation asynchronously
_.each(
['.new-course-org', '.new-course-number', '.new-course-run'],
function (ele) {
var $ele = $(ele);
$ele.on('keyup', function (event) {
// Don't bother showing "required field" error when
// the user tabs into a new field; this is distracting
// and unnecessary
if (event.keyCode === 9) {
return;
}
var error = validateCourseItemEncoding($ele.val());
setNewCourseFieldInErr($ele.parent('li'), error);
validateTotalCourseItemsLength();
});
}
);
var $name = $('.new-course-name');
$name.on('keyup', function () {
var error = validateRequiredField($name.val());
setNewCourseFieldInErr($name.parent('li'), error);
validateTotalCourseItemsLength();
});
CreateCourseUtils.configureHandlers();
};
var validateRequiredField = function (msg) {
return msg.length === 0 ? gettext('Required field.') : '';
};
var setNewCourseFieldInErr = function (el, msg) {
if(msg) {
el.addClass('error');
el.children('span.tip-error').addClass('is-showing').removeClass('is-hiding').text(msg);
$('.new-course-save').addClass('is-disabled');
}
else {
el.removeClass('error');
el.children('span.tip-error').addClass('is-hiding').removeClass('is-showing');
// One "error" div is always present, but hidden or shown
if($('.error').length === 1) {
$('.new-course-save').removeClass('is-disabled');
}
}
};
domReady(function () {
var onReady = function () {
$('.new-course-button').bind('click', addNewCourse);
});
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
ViewUtils.reload();
}));
$('.action-reload').bind('click', ViewUtils.reload);
};
domReady(onReady);
return {
onReady: onReady
};
});

View File

@@ -7,7 +7,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore');
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore');
createMockCourseJSON = function(options, children) {
return $.extend(true, {}, {
@@ -243,6 +244,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
});
});
describe("Rerun notification", function () {
it("can be dismissed", function () {
appendSetFixtures(mockRerunNotification);
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);
expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden');
});
});
describe("Button bar", function() {
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);

View File

@@ -0,0 +1,205 @@
define(["jquery", "js/spec_helpers/create_sinon", "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) {
describe("Create course rerun page", function () {
var selectors = {
org: '.rerun-course-org',
number: '.rerun-course-number',
run: '.rerun-course-run',
name: '.rerun-course-name',
tipError: 'span.tip-error',
save: '.rerun-course-save',
cancel: '.rerun-course-cancel',
errorWrapper: '.wrapper-error',
errorMessage: '#course_rerun_error',
error: '.error',
allowUnicode: '.allow-unicode-course-id'
},
classes = {
shown: 'is-shown',
showing: 'is-showing',
hiding: 'is-hidden',
hidden: 'is-hidden',
error: 'error',
disabled: 'is-disabled',
processing: 'is-processing'
},
mockCreateCourseRerunHTML = readFixtures('mock/mock-create-course-rerun.underscore');
var CreateCourseUtils = CreateCourseUtilsFactory(selectors, classes);
var fillInFields = function (org, number, run, name) {
$(selectors.org).val(org);
$(selectors.number).val(number);
$(selectors.run).val(run);
$(selectors.name).val(name);
};
beforeEach(function () {
view_helpers.installMockAnalytics();
window.source_course_key = 'test_course_key';
appendSetFixtures(mockCreateCourseRerunHTML);
CourseRerunUtils.onReady();
});
afterEach(function () {
view_helpers.removeMockAnalytics();
delete window.source_course_key;
});
describe("Field validation", function () {
it("returns a message for an empty string", function () {
var message = CreateCourseUtils.validateRequiredField('');
expect(message).not.toBe('');
});
it("does not return a message for a non empty string", function () {
var message = CreateCourseUtils.validateRequiredField('edX');
expect(message).toBe('');
});
});
describe("Error messages", function () {
var setErrorMessage = function(selector, message) {
var element = $(selector).parent();
CreateCourseUtils.setNewCourseFieldInErr(element, message);
return element;
};
var type = function (input, value) {
input.val(value);
input.simulate("keyup", { keyCode: $.simulate.keyCode.SPACE });
};
it("shows an error message", function () {
var element = setErrorMessage(selectors.org, 'error message');
expect(element).toHaveClass(classes.error);
expect(element.children(selectors.tipError)).not.toHaveClass(classes.hidden);
expect(element.children(selectors.tipError)).toContainText('error message');
});
it("hides an error message", function () {
var element = setErrorMessage(selectors.org, '');
expect(element).not.toHaveClass(classes.error);
expect(element.children(selectors.tipError)).toHaveClass(classes.hidden);
});
it("disables the save button", function () {
setErrorMessage(selectors.org, 'error message');
expect($(selectors.save)).toHaveClass(classes.disabled);
});
it("enables the save button when all errors are removed", function () {
setErrorMessage(selectors.org, 'error message 1');
setErrorMessage(selectors.number, 'error message 2');
expect($(selectors.save)).toHaveClass(classes.disabled);
setErrorMessage(selectors.org, '');
setErrorMessage(selectors.number, '');
expect($(selectors.save)).not.toHaveClass(classes.disabled);
});
it("does not enable the save button when errors remain", function () {
setErrorMessage(selectors.org, 'error message 1');
setErrorMessage(selectors.number, 'error message 2');
expect($(selectors.save)).toHaveClass(classes.disabled);
setErrorMessage(selectors.org, '');
expect($(selectors.save)).toHaveClass(classes.disabled);
});
it("shows an error message when non URL characters are entered", function () {
var input = $(selectors.org);
expect(input.parent()).not.toHaveClass(classes.error);
type(input, "%");
expect(input.parent()).toHaveClass(classes.error);
});
it("does not show an error message when tabbing into a field", function () {
var input = $(selectors.number);
input.val('');
expect(input.parent()).not.toHaveClass(classes.error);
input.simulate("keyup", { keyCode: $.simulate.keyCode.TAB });
expect(input.parent()).not.toHaveClass(classes.error);
});
it("shows an error message when a required field is empty", function () {
var input = $(selectors.org);
input.val('');
expect(input.parent()).not.toHaveClass(classes.error);
input.simulate("keyup", { keyCode: $.simulate.keyCode.ENTER });
expect(input.parent()).toHaveClass(classes.error);
});
it("shows an error message when spaces are entered and unicode is allowed", function () {
var input = $(selectors.org);
$(selectors.allowUnicode).val('True');
expect(input.parent()).not.toHaveClass(classes.error);
type(input, ' ');
expect(input.parent()).toHaveClass(classes.error);
});
it("shows an error message when total length exceeds 65 characters", function () {
expect($(selectors.errorWrapper)).not.toHaveClass(classes.shown);
type($(selectors.org), 'ThisIsAVeryLongNameThatWillExceedTheSixtyFiveCharacterLimit');
type($(selectors.number), 'ThisIsAVeryLongNameThatWillExceedTheSixtyFiveCharacterLimit');
type($(selectors.run), 'ThisIsAVeryLongNameThatWillExceedTheSixtyFiveCharacterLimit');
expect($(selectors.errorWrapper)).toHaveClass(classes.shown);
});
describe("Name field", function () {
it("does not show an error message when non URL characters are entered", function () {
var input = $(selectors.name);
expect(input.parent()).not.toHaveClass(classes.error);
type(input, "%");
expect(input.parent()).not.toHaveClass(classes.error);
});
});
});
it("saves course reruns", function () {
var requests = create_sinon.requests(this);
var redirectSpy = spyOn(ViewUtils, 'redirect')
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
$(selectors.save).click();
create_sinon.expectJsonRequest(requests, 'POST', '/course/', {
source_course_key: 'test_course_key',
org: 'DemoX',
number: 'DM101',
run: '2014',
display_name: 'Demo course'
});
expect($(selectors.save)).toHaveClass(classes.disabled);
expect($(selectors.save)).toHaveClass(classes.processing);
expect($(selectors.cancel)).toHaveClass(classes.hidden);
create_sinon.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);
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
$(selectors.save).click();
create_sinon.respondWithJson(requests, {
ErrMsg: 'error message'
});
expect($(selectors.errorWrapper)).not.toHaveClass(classes.hidden);
expect($(selectors.errorWrapper)).toContainText('error message');
expect($(selectors.save)).not.toHaveClass(classes.processing);
expect($(selectors.cancel)).not.toHaveClass(classes.hidden);
});
it("does not save if there are validation errors", function () {
var requests = create_sinon.requests(this);
fillInFields('DemoX', 'DM101', '', 'Demo course');
$(selectors.save).click();
expect(requests.length).toBe(0);
});
it("can be canceled", function () {
var redirectSpy = spyOn(ViewUtils, 'redirect');
$(selectors.cancel).click();
expect(redirectSpy).toHaveBeenCalledWith('/course/');
});
});
});

View File

@@ -0,0 +1,65 @@
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/index",
"js/views/utils/view_utils"],
function ($, create_sinon, view_helpers, IndexUtils, ViewUtils) {
describe("Course listing page", function () {
var mockIndexPageHTML = readFixtures('mock/mock-index-page.underscore'), fillInFields;
var fillInFields = function (org, number, run, name) {
$('.new-course-org').val(org);
$('.new-course-number').val(number);
$('.new-course-run').val(run);
$('.new-course-name').val(name);
};
beforeEach(function () {
view_helpers.installMockAnalytics();
appendSetFixtures(mockIndexPageHTML);
IndexUtils.onReady();
});
afterEach(function () {
view_helpers.removeMockAnalytics();
delete window.source_course_key;
});
it("can dismiss notifications", function () {
var requests = create_sinon.requests(this);
var reloadSpy = spyOn(ViewUtils, 'reload');
$('.dismiss-button').click();
create_sinon.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
create_sinon.respondToDelete(requests);
expect(reloadSpy).toHaveBeenCalled();
});
it("saves new courses", function () {
var requests = create_sinon.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/', {
org: 'DemoX',
number: 'DM101',
run: '2014',
display_name: 'Demo course'
});
create_sinon.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);
$('.new-course-button').click();
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
$('.new-course-save').click();
create_sinon.respondWithJson(requests, {
ErrMsg: 'error message'
});
expect($('.wrap-error')).toHaveClass('is-shown');
expect($('#course_creation_error')).toContainText('error message');
expect($('.new-course-save')).toHaveClass('is-disabled');
});
});
});

View File

@@ -0,0 +1,87 @@
define(["domReady", "jquery", "underscore", "js/views/utils/create_course_utils", "js/views/utils/view_utils"],
function (domReady, $, _, CreateCourseUtilsFactory, ViewUtils) {
var CreateCourseUtils = CreateCourseUtilsFactory({
name: '.rerun-course-name',
org: '.rerun-course-org',
number: '.rerun-course-number',
run: '.rerun-course-run',
save: '.rerun-course-save',
errorWrapper: '.wrapper-error',
errorMessage: '#course_rerun_error',
tipError: 'span.tip-error',
error: '.error',
allowUnicode: '.allow-unicode-course-id'
}, {
shown: 'is-shown',
showing: 'is-showing',
hiding: 'is-hidden',
disabled: 'is-disabled',
error: 'error'
});
var saveRerunCourse = function (e) {
e.preventDefault();
if (CreateCourseUtils.hasInvalidRequiredFields()) {
return;
}
var $newCourseForm = $(this).closest('#rerun-course-form');
var display_name = $newCourseForm.find('.rerun-course-name').val();
var org = $newCourseForm.find('.rerun-course-org').val();
var number = $newCourseForm.find('.rerun-course-number').val();
var run = $newCourseForm.find('.rerun-course-run').val();
course_info = {
source_course_key: source_course_key,
org: org,
number: number,
display_name: display_name,
run: run
};
analytics.track('Reran a Course', course_info);
CreateCourseUtils.createCourse(course_info, function (errorMessage) {
$('.wrapper-error').addClass('is-shown').removeClass('is-hidden');
$('#course_rerun_error').html('<p>' + errorMessage + '</p>');
$('.rerun-course-save').addClass('is-disabled').removeClass('is-processing').html(gettext('Create Re-run'));
$('.action-cancel').removeClass('is-hidden');
});
// Go into creating re-run state
$('.rerun-course-save').addClass('is-disabled').addClass('is-processing').html(
'<i class="icon icon-refresh icon-spin"></i>' + gettext('Processing Re-run Request')
);
$('.action-cancel').addClass('is-hidden');
};
var cancelRerunCourse = function (e) {
e.preventDefault();
// Clear out existing fields and errors
$('.rerun-course-run').val('');
$('#course_rerun_error').html('');
$('wrapper-error').removeClass('is-shown').addClass('is-hidden');
$('.rerun-course-save').off('click');
ViewUtils.redirect('/course/');
};
var onReady = function () {
var $cancelButton = $('.rerun-course-cancel');
var $courseRun = $('.rerun-course-run');
$courseRun.focus().select();
$('.rerun-course-save').on('click', saveRerunCourse);
$cancelButton.bind('click', cancelRerunCourse);
$('.cancel-button').bind('click', cancelRerunCourse);
CreateCourseUtils.configureHandlers();
};
domReady(onReady);
// Return these functions so that they can be tested
return {
saveRerunCourse: saveRerunCourse,
cancelRerunCourse: cancelRerunCourse,
onReady: onReady
};
});

View File

@@ -1,7 +1,7 @@
define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification",
"js/utils/cancel_on_escape", "js/utils/date_utils", "js/utils/module"],
"js/utils/cancel_on_escape", "js/utils/date_utils", "js/utils/module", "js/views/utils/view_utils"],
function (domReady, $, ui, _, gettext, NotificationView, CancelOnEscape,
DateUtils, ModuleUtils) {
DateUtils, ModuleUtils, ViewUtils) {
var modalSelector = '.edit-section-publish-settings';
@@ -222,6 +222,10 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$('.toggle-button-sections').bind('click', toggleSections);
$('.expand-collapse').bind('click', toggleSubmodules);
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
$('.wrapper-alert-announcement').remove();
}));
var $body = $('body');
$body.on('click', '.section-published-date .edit-release-date', editSectionPublishDate);
$body.on('click', '.edit-section-publish-settings .action-save', saveSetSectionScheduleDate);

View File

@@ -2,8 +2,8 @@
* This page is used to show the user an outline of the course.
*/
define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views/utils/xblock_utils",
"js/views/course_outline"],
function ($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView) {
"js/views/course_outline", "js/views/utils/view_utils"],
function ($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView, ViewUtils) {
var expandedLocators, CourseOutlinePage;
CourseOutlinePage = BasePage.extend({
@@ -25,6 +25,9 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
self.outlineView.handleAddEvent(event);
});
this.model.on('change', this.setCollapseExpandVisibility, this);
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
$('.wrapper-alert-announcement').removeClass('is-shown').addClass('is-hidden')
}));
},
setCollapseExpandVisibility: function() {

View File

@@ -0,0 +1,151 @@
/**
* Provides utilities for validating courses during creation, for both new courses and reruns.
*/
define(["jquery", "underscore", "gettext", "js/views/utils/view_utils"],
function ($, _, gettext, ViewUtils) {
return function (selectors, classes) {
var validateRequiredField, validateCourseItemEncoding, validateTotalCourseItemsLength, setNewCourseFieldInErr,
hasInvalidRequiredFields, createCourse, validateFilledFields, configureHandlers;
validateRequiredField = function (msg) {
return msg.length === 0 ? gettext('Required field.') : '';
};
// Check that a course (org, number, run) doesn't use any special characters
validateCourseItemEncoding = function (item) {
var required = validateRequiredField(item);
if (required) {
return required;
}
if ($(selectors.allowUnicode).val() === 'True') {
if (/\s/g.test(item)) {
return gettext('Please do not use any spaces in this field.');
}
}
else {
if (item !== encodeURIComponent(item)) {
return gettext('Please do not use any spaces or special characters in this field.');
}
}
return '';
};
// Ensure that org/course_num/run < 65 chars.
validateTotalCourseItemsLength = function () {
var totalLength = _.reduce(
[selectors.org, selectors.number, selectors.run],
function (sum, ele) {
return sum + $(ele).val().length;
}, 0
);
if (totalLength > 65) {
$(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding);
$(selectors.errorMessage).html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
$(selectors.save).addClass(classes.disabled);
}
else {
$(selectors.errorWrapper).removeClass(classes.shown).addClass(classes.hiding);
}
};
setNewCourseFieldInErr = function (el, msg) {
if (msg) {
el.addClass(classes.error);
el.children(selectors.tipError).addClass(classes.showing).removeClass(classes.hiding).text(msg);
$(selectors.save).addClass(classes.disabled);
}
else {
el.removeClass(classes.error);
el.children(selectors.tipError).addClass(classes.hiding).removeClass(classes.showing);
// One "error" div is always present, but hidden or shown
if ($(selectors.error).length === 1) {
$(selectors.save).removeClass(classes.disabled);
}
}
};
// One final check for empty values
hasInvalidRequiredFields = function () {
return _.reduce(
[selectors.name, selectors.org, selectors.number, selectors.run],
function (acc, ele) {
var $ele = $(ele);
var error = validateRequiredField($ele.val());
setNewCourseFieldInErr($ele.parent(), error);
return error ? true : acc;
},
false
);
};
createCourse = function (courseInfo, errorHandler) {
$.postJSON(
'/course/',
courseInfo,
function (data) {
if (data.url !== undefined) {
ViewUtils.redirect(data.url);
} else if (data.ErrMsg !== undefined) {
errorHandler(data.ErrMsg);
}
}
);
};
// Ensure that all fields are not empty
validateFilledFields = function () {
return _.reduce(
[selectors.org, selectors.number, selectors.run, selectors.name],
function (acc, ele) {
var $ele = $(ele);
return $ele.val().length !== 0 ? acc : false;
},
true
);
};
// Handle validation asynchronously
configureHandlers = function () {
_.each(
[selectors.org, selectors.number, selectors.run],
function (ele) {
var $ele = $(ele);
$ele.on('keyup', function (event) {
// Don't bother showing "required field" error when
// the user tabs into a new field; this is distracting
// and unnecessary
if (event.keyCode === 9) {
return;
}
var error = validateCourseItemEncoding($ele.val());
setNewCourseFieldInErr($ele.parent(), error);
validateTotalCourseItemsLength();
if (!validateFilledFields()) {
$(selectors.save).addClass(classes.disabled);
}
});
}
);
var $name = $(selectors.name);
$name.on('keyup', function () {
var error = validateRequiredField($name.val());
setNewCourseFieldInErr($name.parent(), error);
validateTotalCourseItemsLength();
if (!validateFilledFields()) {
$(selectors.save).addClass(classes.disabled);
}
});
};
return {
validateRequiredField: validateRequiredField,
validateCourseItemEncoding: validateCourseItemEncoding,
validateTotalCourseItemsLength: validateTotalCourseItemsLength,
setNewCourseFieldInErr: setNewCourseFieldInErr,
hasInvalidRequiredFields: hasInvalidRequiredFields,
createCourse: createCourse,
validateFilledFields: validateFilledFields,
configureHandlers: configureHandlers
};
};
});

View File

@@ -5,7 +5,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
function ($, _, gettext, NotificationView, PromptView) {
var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation,
runOperationShowingMessage, disableElementWhileRunning, getScrollOffset, setScrollOffset,
setScrollTop, redirect, hasChangedAttributes;
setScrollTop, redirect, reload, hasChangedAttributes, deleteNotificationHandler;
/**
* Toggles the expanded state of the current element.
@@ -94,6 +94,21 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
});
};
/**
* Returns a handler that removes a notification, both dismissing it and deleting it from the database.
* @param callback function to call when deletion succeeds
*/
deleteNotificationHandler = function(callback) {
return function (event) {
event.preventDefault();
$.ajax({
url: $(this).data('dismiss-link'),
type: 'DELETE',
success: callback
});
};
};
/**
* Performs an animated scroll so that the window has the specified scroll top.
* @param scrollTop The desired scroll top for the window.
@@ -132,6 +147,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
window.location = url;
};
/**
* Reloads the page. This is broken out as its own function for unit testing.
*/
reload = function() {
window.location.reload();
};
/**
* Returns true if a model has changes to at least one of the specified attributes.
* @param model The model in question.
@@ -158,10 +180,12 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
'confirmThenRunOperation': confirmThenRunOperation,
'runOperationShowingMessage': runOperationShowingMessage,
'disableElementWhileRunning': disableElementWhileRunning,
'deleteNotificationHandler': deleteNotificationHandler,
'setScrollTop': setScrollTop,
'getScrollOffset': getScrollOffset,
'setScrollOffset': setScrollOffset,
'redirect': redirect,
'reload': reload,
'hasChangedAttributes': hasChangedAttributes
};
});

View File

@@ -240,6 +240,282 @@ p, ul, ol, dl {
}
}
// ====================
// layout - basic
.wrapper-view {
}
// ====================
// layout - basic page header
.wrapper-mast {
margin: ($baseline*1.5) 0 0 0;
padding: 0 $baseline;
position: relative;
.mast, .metadata {
@include clearfix();
position: relative;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto $baseline auto;
color: $gray-d2;
}
.mast {
border-bottom: 1px solid $gray-l4;
padding-bottom: ($baseline/2);
// layout with actions
.page-header {
width: flex-grid(12);
}
// layout with actions
&.has-actions {
@include clearfix();
.page-header {
float: left;
width: flex-grid(6,12);
margin-right: flex-gutter();
}
.nav-actions {
position: relative;
bottom: -($baseline*0.75);
float: right;
width: flex-grid(6,12);
text-align: right;
.nav-item {
display: inline-block;
vertical-align: top;
margin-right: ($baseline/2);
&:last-child {
margin-right: 0;
}
}
// buttons
.button {
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2);
}
.new-button {
}
.view-button {
}
}
}
// layout with actions
&.has-subtitle {
.nav-actions {
bottom: -($baseline*1.5);
}
}
// layout with navigation
&.has-navigation {
.nav-actions {
bottom: -($baseline*1.5);
}
.navigation-link {
@extend %cont-truncated;
display: inline-block;
vertical-align: bottom; // correct for extra padding in FF
max-width: 250px;
&.navigation-current {
@extend %ui-disabled;
color: $gray;
max-width: 250px;
&:before {
color: $gray;
}
}
}
.navigation-link:before {
content: " / ";
margin: ($baseline/4);
color: $gray;
&:hover {
color: $gray;
}
}
.navigation .navigation-link:first-child:before {
content: "";
margin: 0;
}
}
}
// CASE: wizard-based mast
.mast-wizard {
.page-header-sub {
@extend %t-title4;
color: $gray;
font-weight: 300;
}
.page-header-super {
@extend %t-title4;
float: left;
width: flex-grid(12,12);
margin-top: ($baseline/2);
border-top: 1px solid $gray-l4;
padding-top: ($baseline/2);
font-weight: 600;
}
}
// page metadata/action bar
.metadata {
}
}
// layout - basic page content
.wrapper-content {
margin: 0;
padding: 0 $baseline;
position: relative;
}
.content {
@include clearfix();
@extend %t-copy-base;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto;
color: $gray-d2;
header {
position: relative;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l4;
padding-bottom: ($baseline/2);
.title-sub {
@extend %t-copy-sub1;
display: block;
margin: 0;
color: $gray-l2;
}
.title-1 {
@extend %t-title3;
margin: 0;
padding: 0;
font-weight: 600;
color: $gray-d3;
}
}
}
.content-primary, .content-supplementary {
@include box-sizing(border-box);
}
// layout - primary content
.content-primary {
.title-1 {
@extend %t-title3;
}
.title-2 {
@extend %t-title4;
margin: 0 0 ($baseline/2) 0;
}
.title-3 {
@extend %t-title6;
margin: 0 0 ($baseline/2) 0;
}
header {
@include clearfix();
.title-2 {
width: flex-grid(5, 12);
margin: 0 flex-gutter() 0 0;
float: left;
}
.tip {
@extend %t-copy-sub2;
width: flex-grid(7, 12);
float: right;
margin-top: ($baseline/2);
text-align: right;
color: $gray-l2;
}
}
}
// layout - supplemental content
.content-supplementary {
> section {
margin: 0 0 $baseline 0;
}
}
// ====================
// layout - grandfathered
.main-wrapper {
position: relative;
margin: 0 ($baseline*2);
}
.inner-wrapper {
@include clearfix();
position: relative;
max-width: 1280px;
margin: auto;
> article {
clear: both;
}
}
.main-column {
clear: both;
float: left;
width: 70%;
}
.sidebar {
float: right;
width: 28%;
}
.left {
float: left;
}
.right {
float: right;
}
// ====================

View File

@@ -133,6 +133,27 @@
}
}
// white secondary button
%btn-secondary-white {
@extend %ui-btn-secondary;
border-color: $white-t2;
color: $white-t3;
&:hover, &:active {
border-color: $white;
color: $white;
}
&.current, &.active {
background: $gray-d2;
color: $gray-l5;
&:hover, &:active {
background: $gray-d2;
}
}
}
// green secondary button
%btn-secondary-green {
@extend %ui-btn-secondary;
@@ -213,17 +234,6 @@
// ====================
// calls-to-action
// ====================
// specific buttons - view live
%view-live-button {
@extend %t-action4;
}
// ====================
// UI: element actions list
%actions-list {

View File

@@ -138,28 +138,10 @@ form {
}
}
// ELEM: form wrapper
.wrapper-create-element {
height: 0;
margin-bottom: $baseline;
opacity: 0.0;
pointer-events: none;
overflow: hidden;
&.animate {
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
}
&.is-shown {
height: auto; // define a specific height for the animating version of this UI to work properly
opacity: 1.0;
pointer-events: auto;
}
}
// ELEM: form
// form styling for creating a new content item (course, user, textbook)
form[class^="create-"] {
// TODO: refactor this into a placeholder to extend.
.form-create {
@extend %ui-window;
.title {
@@ -254,12 +236,19 @@ form[class^="create-"] {
.tip {
@extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
@include transition(color 0.15s ease-in-out);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
}
.tip-note {
display: block;
margin-top: ($baseline/4);
}
.tip-error {
display: none;
float: none;
@@ -366,7 +355,6 @@ form[class^="create-"] {
}
}
// form - inline xblock name edit on unit, container, outline
// TOOD: abstract this out into a Sass placeholder
@@ -403,6 +391,25 @@ form[class^="create-"] {
}
}
// ELEM: form wrapper
.wrapper-create-element {
height: 0;
margin-bottom: $baseline;
opacity: 0.0;
pointer-events: none;
overflow: hidden;
&.animate {
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
}
&.is-shown {
height: auto; // define a specific height for the animating version of this UI to work properly
opacity: 1.0;
pointer-events: auto;
}
}
// ====================
// forms - grandfathered

View File

@@ -141,6 +141,26 @@
}
}
// CASE: wizard-based mast
.mast-wizard {
.page-header-sub {
@extend %t-title4;
color: $gray;
font-weight: 300;
}
.page-header-super {
@extend %t-title4;
float: left;
width: flex-grid(12,12);
margin-top: ($baseline/2);
border-top: 1px solid $gray-l4;
padding-top: ($baseline/2);
font-weight: 600;
}
}
// page metadata/action bar
.metadata {

View File

@@ -527,7 +527,7 @@
&.wrapper-alert-warning {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange;
[class^="icon"] {
.alert-symbol {
color: $orange;
}
}
@@ -535,7 +535,7 @@
&.wrapper-alert-error {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1;
[class^="icon"] {
.alert-symbol {
color: $red-l1;
}
}
@@ -543,7 +543,7 @@
&.wrapper-alert-confirmation {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green;
[class^="icon"] {
.alert-symbol {
color: $green;
}
}
@@ -551,7 +551,7 @@
&.wrapper-alert-announcement {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
[class^="icon"] {
.alert-symbol {
color: $blue;
}
}
@@ -559,7 +559,7 @@
&.wrapper-alert-step-required {
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink;
[class^="icon"] {
.alert-symbol {
color: $pink;
}
}
@@ -579,11 +579,11 @@
@extend %t-strong;
}
[class^="icon"], .copy {
.alert-symbol, .copy {
float: left;
}
[class^="icon"] {
.alert-symbol {
@include transition (color 0.50s ease-in-out 0s);
@extend %t-icon3;
width: flex-grid(1, 12);
@@ -605,7 +605,7 @@
// with actions
&.has-actions {
[class^="icon"] {
.alert-symbol {
width: flex-grid(1, 12);
}
@@ -667,6 +667,29 @@
background: $gray-d1;
}
}
// with dismiss (to sunset action-alert-clos)
.action-dismiss {
.button {
@extend %btn-secondary-white;
padding:($baseline/4) ($baseline/2);
}
.icon,.button-copy {
display: inline-block;
vertical-align: middle;
}
.icon {
@extend %t-icon4;
margin-right: ($baseline/4);
}
.button-copy {
@extend %t-copy-sub1;
}
}
}
// ====================

View File

@@ -243,6 +243,12 @@
}
}
// learn more (aka external help button)
.external-help-button {
@extend %ui-btn-flat-outline;
@extend %sizing;
}
// actions
.list-actions {
@extend %cont-no-list;

View File

@@ -38,6 +38,7 @@
@import 'views/dashboard';
@import 'views/export';
@import 'views/index';
@import 'views/course-create';
@import 'views/import';
@import 'views/outline';
@import 'views/settings';

View File

@@ -0,0 +1,122 @@
// studio - views - course creation page
// ====================
.view-course-create {
// basic layout
// --------------------
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(9, 12);
margin-right: flex-gutter();
}
.content-supplementary {
width: flex-grid(3, 12);
}
//
// header/masthead
// --------------------
.mast .page-header-super {
.course-original-title-id, .course-original-title {
display: block;
}
.course-original-title-id {
@extend %t-title5;
}
}
// course re-run form
// --------------------
.rerun-course {
.row {
@include clearfix();
margin-bottom: ($baseline*0.75);
}
.column {
float: left;
width: 48%;
}
.column:first-child {
margin-right: 4%;
}
label {
@extend %t-title7;
display: block;
font-weight: 700;
}
.rerun-course-org,
.rerun-course-number,
.rerun-course-name,
.rerun-course-run {
width: 100%;
}
.rerun-course-name {
@extend %t-title5;
font-weight: 300;
}
.rerun-course-save {
@include blue-button;
.icon {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
.rerun-course-cancel {
@include white-button;
}
.item-details {
padding-bottom: 0;
}
.wrap-error {
@include transition(opacity $tmg-f2 ease 0s);
opacity: 0;
}
.wrap-error.is-shown {
opacity: 1;
}
.message-status {
display: block;
margin-bottom: 0;
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
font-weight: bold;
}
// NOTE: override for modern button styling until all buttons (in _forms.scss) can be converted
.actions {
.action-primary {
@include blue-button;
@extend %t-action2;
}
.action-secondary {
@include grey-button;
@extend %t-action2;
}
}
}
}

View File

@@ -236,7 +236,7 @@
@extend %t-strong;
margin: ($baseline/2);
[class^="icon-"] {
.icon {
margin-right: ($baseline/4);
}
}
@@ -294,120 +294,307 @@
// ELEM: course listings
.courses {
margin: $baseline 0;
}
.title {
@extend %t-title6;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l3;
padding-bottom: ($baseline/2);
color: $gray-l2;
}
.title {
@extend %t-title6;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l3;
padding-bottom: ($baseline/2);
color: $gray-l2;
}
}
.list-courses {
margin-top: $baseline;
border-radius: 3px;
border: 1px solid $gray;
border: 1px solid $gray-l2;
background: $white;
box-shadow: 0 1px 2px $shadow-l1;
box-shadow: 0 1px 1px $shadow-l1;
.course-item {
@include box-sizing(border-box);
width: flex-grid(9, 9);
position: relative;
border-bottom: 1px solid $gray-l1;
padding: $baseline;
li:last-child {
margin-bottom: 0;
}
}
// STATE: hover/focus
&:hover {
background: $paleYellow;
.course-actions .view-live-button {
opacity: 1.0;
pointer-events: auto;
}
// UI: course wrappers (needed for status messages)
.wrapper-course {
.course-title {
color: $orange-d1;
}
// CASE: has status
&.has-status {
.course-metadata {
opacity: 1.0;
}
}
.course-link, .course-actions {
.course-status {
@include box-sizing(border-box);
display: inline-block;
vertical-align: middle;
}
// encompassing course link
.course-link {
@extend %ui-depth2;
width: flex-grid(7, 9);
margin-right: flex-gutter();
}
// course title
.course-title {
@extend %t-title4;
@extend %t-light;
margin: 0 ($baseline*2) ($baseline/4) 0;
}
// course metadata
.course-metadata {
@extend %t-copy-sub1;
@include transition(opacity $tmg-f1 ease-in-out 0);
color: $gray;
opacity: 0.75;
.metadata-item {
display: inline-block;
&:after {
content: "/";
margin-left: ($baseline/10);
margin-right: ($baseline/10);
color: $gray-l4;
}
&:last-child {
&:after {
content: "";
margin-left: 0;
margin-right: 0;
}
}
.label {
@extend %cont-text-sr;
}
}
}
.course-actions {
@extend %ui-depth3;
position: static;
width: flex-grid(2, 9);
width: flex-grid(3, 9);
padding-right: ($baseline/2);
text-align: right;
// view live button
.view-live-button {
@extend %ui-depth3;
@include transition(opacity $tmg-f2 ease-in-out 0);
@include box-sizing(border-box);
padding: ($baseline/2);
opacity: 0.0;
pointer-events: none;
.value {
&:hover {
opacity: 1.0;
pointer-events: auto;
.copy, .icon {
display: inline-block;
vertical-align: middle;
}
.icon {
@extend %t-icon4;
margin-right: ($baseline/2);
}
.copy {
@extend %t-copy-sub1;
}
}
}
&:last-child {
border-bottom: none;
.status-message {
@extend %t-copy-sub1;
background-color: $gray-l5;
box-shadow: 0 2px 2px 0 $shadow inset;
padding: ($baseline*0.75) $baseline;
&.has-actions {
.copy, .status-actions {
display: inline-block;
vertical-align: middle;
}
.copy {
width: 65%;
margin: 0 $baseline 0 0;
}
.status-actions {
width: 30%;
text-align: right;
.button {
@extend %btn-secondary-white;
padding:($baseline/4) ($baseline/2);
}
.icon,.button-copy {
display: inline-block;
vertical-align: middle;
}
.icon {
@extend %t-icon4;
margin-right: ($baseline/4);
}
.button-copy {
@extend %t-copy-sub1;
}
}
}
}
}
}
// UI: individual course listings
.course-item {
@include box-sizing(border-box);
width: flex-grid(9, 9);
position: relative;
border-bottom: 1px solid $gray-l2;
padding: $baseline;
// STATE: hover/focus
&:hover {
background: $paleYellow;
.course-actions {
opacity: 1.0;
pointer-events: auto;
}
.course-title {
color: $orange-d1;
}
.course-metadata {
opacity: 1.0;
}
}
.course-link, .course-actions {
@include box-sizing(border-box);
display: inline-block;
vertical-align: middle;
}
// encompassing course link
.course-link {
@extend %ui-depth2;
width: flex-grid(6, 9);
margin-right: flex-gutter();
}
// course title
.course-title {
@extend %t-title4;
margin: 0 ($baseline*2) ($baseline/4) 0;
font-weight: 300;
}
// course metadata
.course-metadata {
@extend %t-copy-sub1;
@include transition(opacity $tmg-f1 ease-in-out 0);
color: $gray;
opacity: 0.75;
.metadata-item {
display: inline-block;
&:after {
content: "/";
margin-left: ($baseline/10);
margin-right: ($baseline/10);
color: $gray-l4;
}
&:last-child {
&:after {
content: "";
margin-left: 0;
margin-right: 0;
}
}
.label {
@extend %cont-text-sr;
}
}
}
.course-actions {
@include transition(opacity $tmg-f2 ease-in-out 0);
@extend %ui-depth3;
position: static;
width: flex-grid(3, 9);
text-align: right;
opacity: 0;
pointer-events: none;
.action {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/2);
&:last-child {
margin-right: 0;
}
}
.button {
@extend %t-action3;
}
// view live button
.view-button {
@include box-sizing(border-box);
padding: ($baseline/2);
}
// course re-run button
.action-rerun {
margin-right: $baseline;
}
.rerun-button {
font-weight: 600;
// TODO: sync up button styling and add secondary style here
}
}
// CASE: is processing
&.is-processing {
.course-status .value {
color: $gray-l2;
}
}
// CASE: has an error
&.has-error {
.course-status {
color: $red; // TODO: abstract this out to an error-based color variable
}
~ .status-message {
background: $red-l1; // TODO: abstract this out to an error-based color variable
color: $white;
}
}
// CASE: last course in listing
&:last-child {
border-bottom: none;
}
}
// ====================
// CASE: courses that are being processed
.courses-processing {
margin-bottom: ($baseline*2);
border-bottom: 1px solid $gray-l3;
padding-bottom: ($baseline*2);
// TODO: abstract this case out better with normal course listings
.list-courses {
border: none;
background: none;
box-shadow: none;
}
.wrapper-course {
@extend %ui-window;
position: relative;
}
.course-item {
border: none;
// STATE: hover/focus
&:hover {
background: inherit;
.course-title {
color: inherit;
}
}
}
// course details (replacement for course-link when a course cannot be linked)
.course-details {
@extend %ui-depth2;
display: inline-block;
vertical-align: middle;
width: flex-grid(6, 9);
margin-right: flex-gutter();
}
}
// ====================
// ELEM: new user form
.wrapper-create-course {
@@ -494,6 +681,5 @@
margin-bottom: 0;
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
}
}
}