diff --git a/.eslintignore b/.eslintignore index 82cdcd00dc..9044a0cc71 100644 --- a/.eslintignore +++ b/.eslintignore @@ -64,5 +64,7 @@ xmodule/js/spec/problem/edit_spec_hint.js xmodule/js/spec/problem/edit_spec.js xmodule/js/spec/tabs/edit.js +xmodule/js/public/js +xmodule/assets/*/public/js !**/.eslintrc.js diff --git a/.eslintrc.json b/.eslintrc.json index c5ca21f9b9..009e9db2fe 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,6 +18,72 @@ "spyOnEvent": true, // used by our requirejs implementation - "RequireJS": true + "RequireJS": true, + + // enable jquery + "$": true + }, + "rules": { + "func-names": "off", + "indent": ["error", 4], + "new-cap": "off", + "no-else-return": "off", + "no-shadow": "error", + "object-curly-spacing": ["error", "never"], + "one-var": "off", + "one-var-declaration-per-line": ["error", "initializations"], + "space-before-function-paren": ["error", "never"], + "strict": "off", + + // Temporary Rules (Will be removed one-by-one to minimize file changes) + "block-scoped-var": "off", + "camelcase": "off", + "comma-dangle": "off", + "consistent-return": "off", + "curly": "off", + "eqeqeq": "off", + "function-call-argument-newline": "off", + "function-paren-newline": "off", + "implicit-arrow-linebreak": "off", + "import/extensions": "off", + "import/no-amd": "off", + "import/no-dynamic-require": "off", + "import/no-unresolved": "off", + "linebreak-style": "off", + "lines-around-directive": "off", + "max-len": "off", + "newline-per-chained-call": "off", + "no-console": "off", + "no-lonely-if": "off", + "no-multi-spaces": "off", + "no-multiple-empty-lines": "off", + "no-param-reassign": "off", + "no-proto": "off", + "no-prototype-builtins": "off", + "no-redeclare": "off", + "no-restricted-globals": "off", + "no-restricted-syntax": "off", + "no-throw-literal": "off", + "no-undef": "off", + "no-underscore-dangle": "off", + "no-unused-vars": "off", + "no-use-before-define": "off", + "no-useless-escape": "off", + "no-var": "off", + "object-curly-newline": "off", + "object-shorthand": "off", + "operator-linebreak": "off", + "prefer-arrow-callback": "off", + "prefer-destructuring": "off", + "prefer-rest-params": "off", + "prefer-template": "off", + "quotes": "off", + "radix": "off", + "react/jsx-indent": "off", + "react/jsx-indent-props": "off", + "react/jsx-wrap-multilines": "off", + "react/prop-types": "off", + "semi": "off", + "vars-on-top": "off" } } diff --git a/cms/static/cms/js/spec/main_spec.js b/cms/static/cms/js/spec/main_spec.js index ed01a312b2..451f465409 100644 --- a/cms/static/cms/js/spec/main_spec.js +++ b/cms/static/cms/js/spec/main_spec.js @@ -3,80 +3,80 @@ (function(sandbox) { 'use strict'; require(['jquery', 'backbone', 'cms/js/main', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'jquery.cookie'], - function($, Backbone, main, AjaxHelpers) { - describe('CMS', function() { - it('should initialize URL', function() { - expect(window.CMS.URL).toBeDefined(); - }); - }); - describe('main helper', function() { - beforeEach(function() { - this.previousAjaxSettings = $.extend(true, {}, $.ajaxSettings); - spyOn($, 'cookie').and.callFake(function(param) { - if (param === 'csrftoken') { - return 'stubCSRFToken'; - } - }); - return main(); - }); - afterEach(function() { - $.ajaxSettings = this.previousAjaxSettings; - return $.ajaxSettings; - }); - it('turn on Backbone emulateHTTP', function() { - expect(Backbone.emulateHTTP).toBeTruthy(); - }); - it('setup AJAX CSRF token', function() { - expect($.ajaxSettings.headers['X-CSRFToken']).toEqual('stubCSRFToken'); - }); - }); - describe('AJAX Errors', function() { - var server; - server = null; - beforeEach(function() { - appendSetFixtures(sandbox({ - id: 'page-notification' - })); - }); - afterEach(function() { - return server && server.restore(); - }); - it('successful AJAX request does not pop an error notification', function() { - server = AjaxHelpers.server([ - 200, { - 'Content-Type': 'application/json' - }, '{}' - ]); - expect($('#page-notification')).toBeEmpty(); - $.ajax('/test'); - expect($('#page-notification')).toBeEmpty(); - server.respond(); - expect($('#page-notification')).toBeEmpty(); - }); - it('AJAX request with error should pop an error notification', function() { - server = AjaxHelpers.server([ - 500, { - 'Content-Type': 'application/json' - }, '{}' - ]); - $.ajax('/test'); - server.respond(); - expect($('#page-notification')).not.toBeEmpty(); - expect($('#page-notification')).toContainElement('div.wrapper-notification-error'); - }); - it('can override AJAX request with error so it does not pop an error notification', function() { - server = AjaxHelpers.server([ - 500, { - 'Content-Type': 'application/json' - }, '{}' - ]); - $.ajax({ - url: '/test', - notifyOnError: false - }); - server.respond(); - expect($('#page-notification')).toBeEmpty(); - }); + function($, Backbone, main, AjaxHelpers) { + describe('CMS', function() { + it('should initialize URL', function() { + expect(window.CMS.URL).toBeDefined(); }); }); + describe('main helper', function() { + beforeEach(function() { + this.previousAjaxSettings = $.extend(true, {}, $.ajaxSettings); + spyOn($, 'cookie').and.callFake(function(param) { + if (param === 'csrftoken') { + return 'stubCSRFToken'; + } + }); + return main(); + }); + afterEach(function() { + $.ajaxSettings = this.previousAjaxSettings; + return $.ajaxSettings; + }); + it('turn on Backbone emulateHTTP', function() { + expect(Backbone.emulateHTTP).toBeTruthy(); + }); + it('setup AJAX CSRF token', function() { + expect($.ajaxSettings.headers['X-CSRFToken']).toEqual('stubCSRFToken'); + }); + }); + describe('AJAX Errors', function() { + var server; + server = null; + beforeEach(function() { + appendSetFixtures(sandbox({ + id: 'page-notification' + })); + }); + afterEach(function() { + return server && server.restore(); + }); + it('successful AJAX request does not pop an error notification', function() { + server = AjaxHelpers.server([ + 200, { + 'Content-Type': 'application/json' + }, '{}' + ]); + expect($('#page-notification')).toBeEmpty(); + $.ajax('/test'); + expect($('#page-notification')).toBeEmpty(); + server.respond(); + expect($('#page-notification')).toBeEmpty(); + }); + it('AJAX request with error should pop an error notification', function() { + server = AjaxHelpers.server([ + 500, { + 'Content-Type': 'application/json' + }, '{}' + ]); + $.ajax('/test'); + server.respond(); + expect($('#page-notification')).not.toBeEmpty(); + expect($('#page-notification')).toContainElement('div.wrapper-notification-error'); + }); + it('can override AJAX request with error so it does not pop an error notification', function() { + server = AjaxHelpers.server([ + 500, { + 'Content-Type': 'application/json' + }, '{}' + ]); + $.ajax({ + url: '/test', + notifyOnError: false + }); + server.respond(); + expect($('#page-notification')).toBeEmpty(); + }); + }); + }); }).call(this, sandbox); diff --git a/cms/static/cms/js/xblock/cms.runtime.v1.js b/cms/static/cms/js/xblock/cms.runtime.v1.js index ce362ba584..be4ab8c6ad 100644 --- a/cms/static/cms/js/xblock/cms.runtime.v1.js +++ b/cms/static/cms/js/xblock/cms.runtime.v1.js @@ -1,182 +1,182 @@ define(['jquery', 'backbone', 'xblock/runtime.v1', 'URI', 'gettext', 'js/utils/modal', 'common/js/components/views/feedback_notification'], - function($, Backbone, XBlock, URI, gettext, ModalUtils, NotificationView) { - 'use strict'; +function($, Backbone, XBlock, URI, gettext, ModalUtils, NotificationView) { + 'use strict'; - var __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { - var key; - for (key in parent) { - if (__hasProp.call(parent, key)) { - child[key] = parent[key]; - } + var __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { + var key; + for (key in parent) { + if (__hasProp.call(parent, key)) { + child[key] = parent[key]; } - function Ctor() { - this.constructor = child; - } - Ctor.prototype = parent.prototype; - child.prototype = new Ctor(); - child.__super__ = parent.prototype; - return child; - }, - BaseRuntime = {}, - PreviewRuntime = {}, - StudioRuntime = {}; - - BaseRuntime.v1 = (function(_super) { - __extends(v1, _super); - - v1.prototype.handlerUrl = function(element, handlerName, suffix, query) { - var uri; - uri = URI(this.handlerPrefix) - .segment($(element).data('usage-id')) - .segment('handler') - .segment(handlerName); - if (suffix !== null) { - uri.segment(suffix); - } - if (query !== null) { - uri.search(query); - } - return uri.toString(); - }; - - function v1() { - v1.__super__.constructor.call(this); - this.dispatcher = _.clone(Backbone.Events); - this.listenTo('save', this._handleSave); - this.listenTo('cancel', this._handleCancel); - this.listenTo('error', this._handleError); - this.listenTo('modal-shown', function(data) { - this.modal = data; - }); - this.listenTo('modal-hidden', function() { - this.modal = null; - }); - this.listenTo('page-shown', function(data) { - this.page = data; - }); } + function Ctor() { + this.constructor = child; + } + Ctor.prototype = parent.prototype; + child.prototype = new Ctor(); + child.__super__ = parent.prototype; + return child; + }, + BaseRuntime = {}, + PreviewRuntime = {}, + StudioRuntime = {}; - /** + BaseRuntime.v1 = (function(_super) { + __extends(v1, _super); + + v1.prototype.handlerUrl = function(element, handlerName, suffix, query) { + var uri; + uri = URI(this.handlerPrefix) + .segment($(element).data('usage-id')) + .segment('handler') + .segment(handlerName); + if (suffix !== null) { + uri.segment(suffix); + } + if (query !== null) { + uri.search(query); + } + return uri.toString(); + }; + + function v1() { + v1.__super__.constructor.call(this); + this.dispatcher = _.clone(Backbone.Events); + this.listenTo('save', this._handleSave); + this.listenTo('cancel', this._handleCancel); + this.listenTo('error', this._handleError); + this.listenTo('modal-shown', function(data) { + this.modal = data; + }); + this.listenTo('modal-hidden', function() { + this.modal = null; + }); + this.listenTo('page-shown', function(data) { + this.page = data; + }); + } + + /** * Notify the Studio client-side runtime of an event so that it * can update the UI in a consistent way. * * @param {string} name The name of the event. * @param {object} data A JSON representation of the data to be included with the event. */ - v1.prototype.notify = function(name, data) { - this.dispatcher.trigger(name, data); - }; + v1.prototype.notify = function(name, data) { + this.dispatcher.trigger(name, data); + }; - /** + /** * Listen to a Studio event and invoke the specified callback when it is triggered. * * @param {string} name The name of the event. * @param {function} callback The callback to be invoked. */ - v1.prototype.listenTo = function(name, callback) { - this.dispatcher.bind(name, callback, this); - }; + v1.prototype.listenTo = function(name, callback) { + this.dispatcher.bind(name, callback, this); + }; - /** + /** * Refresh the view for the xblock represented by the specified element. * * @param {element} element The element representing the XBlock. */ - v1.prototype.refreshXBlock = function(element) { - if (this.page) { - this.page.refreshXBlock(element); - } - }; + v1.prototype.refreshXBlock = function(element) { + if (this.page) { + this.page.refreshXBlock(element); + } + }; - v1.prototype._handleError = function(data) { - var message, title; - message = data.message || data.msg; - if (message) { - // TODO: remove 'Open Assessment' specific default title - title = data.title || gettext('OpenAssessment Save Error'); - this.alert = new NotificationView.Error({ - title: title, - message: message, - closeIcon: false, - shown: false - }); - this.alert.show(); - } - }; + v1.prototype._handleError = function(data) { + var message, title; + message = data.message || data.msg; + if (message) { + // TODO: remove 'Open Assessment' specific default title + title = data.title || gettext('OpenAssessment Save Error'); + this.alert = new NotificationView.Error({ + title: title, + message: message, + closeIcon: false, + shown: false + }); + this.alert.show(); + } + }; - v1.prototype._handleSave = function(data) { - var message; - // Starting to save, so show a notification - if (data.state === 'start') { - message = data.message || gettext('Saving'); - this.notification = new NotificationView.Mini({ - title: message - }); - this.notification.show(); - } else if (data.state === 'end') { - // Finished saving, so hide the notification and refresh appropriately - this._hideAlerts(); - - if (this.modal && this.modal.onSave) { - // Notify the modal that the save has completed so that it can hide itself - // and then refresh the xblock. - this.modal.onSave(); - } else if (data.element) { - // ... else ask it to refresh the newly saved xblock - this.refreshXBlock(data.element); - } - this.notification.hide(); - } - }; - - v1.prototype._handleCancel = function() { + v1.prototype._handleSave = function(data) { + var message; + // Starting to save, so show a notification + if (data.state === 'start') { + message = data.message || gettext('Saving'); + this.notification = new NotificationView.Mini({ + title: message + }); + this.notification.show(); + } else if (data.state === 'end') { + // Finished saving, so hide the notification and refresh appropriately this._hideAlerts(); - if (this.modal) { - this.modal.cancel(); - this.notify('modal-hidden'); - } - }; - /** + if (this.modal && this.modal.onSave) { + // Notify the modal that the save has completed so that it can hide itself + // and then refresh the xblock. + this.modal.onSave(); + } else if (data.element) { + // ... else ask it to refresh the newly saved xblock + this.refreshXBlock(data.element); + } + this.notification.hide(); + } + }; + + v1.prototype._handleCancel = function() { + this._hideAlerts(); + if (this.modal) { + this.modal.cancel(); + this.notify('modal-hidden'); + } + }; + + /** * Hide any alerts that are being shown. */ - v1.prototype._hideAlerts = function() { - if (this.alert && this.alert.options.shown) { - this.alert.hide(); - } - }; - - return v1; - }(XBlock.Runtime.v1)); - - PreviewRuntime.v1 = (function(_super) { - __extends(v1, _super); - - function v1() { - return v1.__super__.constructor.apply(this, arguments); + v1.prototype._hideAlerts = function() { + if (this.alert && this.alert.options.shown) { + this.alert.hide(); } + }; - v1.prototype.handlerPrefix = '/preview/xblock'; + return v1; + }(XBlock.Runtime.v1)); - return v1; - }(BaseRuntime.v1)); + PreviewRuntime.v1 = (function(_super) { + __extends(v1, _super); - StudioRuntime.v1 = (function(_super) { - __extends(v1, _super); + function v1() { + return v1.__super__.constructor.apply(this, arguments); + } - function v1() { - return v1.__super__.constructor.apply(this, arguments); - } + v1.prototype.handlerPrefix = '/preview/xblock'; - v1.prototype.handlerPrefix = '/xblock'; + return v1; + }(BaseRuntime.v1)); - return v1; - }(BaseRuntime.v1)); + StudioRuntime.v1 = (function(_super) { + __extends(v1, _super); - // Install the runtime's into the global namespace - window.BaseRuntime = BaseRuntime; - window.PreviewRuntime = PreviewRuntime; - window.StudioRuntime = StudioRuntime; - }); + function v1() { + return v1.__super__.constructor.apply(this, arguments); + } + + v1.prototype.handlerPrefix = '/xblock'; + + return v1; + }(BaseRuntime.v1)); + + // Install the runtime's into the global namespace + window.BaseRuntime = BaseRuntime; + window.PreviewRuntime = PreviewRuntime; + window.StudioRuntime = StudioRuntime; +}); diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 7ef8b9f47c..8992ac7d12 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -14,121 +14,121 @@ define([ 'jquery.form', 'jquery.smoothScroll' ], - function( - domReady, - $, - _, - gettext, - NotificationView, - PromptView, - DateUtils, - ModuleUtils, - IframeUtils, - DropdownMenuView - ) { - 'use strict'; - var $body; +function( + domReady, + $, + _, + gettext, + NotificationView, + PromptView, + DateUtils, + ModuleUtils, + IframeUtils, + DropdownMenuView +) { + 'use strict'; + var $body; - function smoothScrollLink(e) { - (e).preventDefault(); + function smoothScrollLink(e) { + (e).preventDefault(); - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $(this).attr('href') - }); - } - - function hideNotification(e) { - (e).preventDefault(); - $(this) - .closest('.wrapper-notification') - .removeClass('is-shown') - .addClass('is-hiding') - .attr('aria-hidden', 'true'); - } - - function hideAlert(e) { - (e).preventDefault(); - $(this).closest('.wrapper-alert').removeClass('is-shown'); - } - - domReady(function() { - var dropdownMenuView; - - $body = $('body'); - - $body.on('click', '.embeddable-xml-input', function() { - $(this).select(); - }); - - $body.addClass('js'); - - // alerts/notifications - manual close - $('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert); - $('.action-notification-close').bind('click', hideNotification); - - // nav - dropdown related - $body.click(function() { - $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); - $('.nav-dd .nav-item .title').removeClass('is-selected'); - }); - - $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { - var $subnav = $(this).find('.wrapper-nav-sub'), - $title = $(this).find('.title'); - - if ($subnav.hasClass('is-shown')) { - $subnav.removeClass('is-shown'); - $title.removeClass('is-selected'); - } else { - $('.nav-dd .nav-item .title').removeClass('is-selected'); - $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); - $title.addClass('is-selected'); - $subnav.addClass('is-shown'); - // if propagation is not stopped, the event will bubble up to the - // body element, which will close the dropdown. - e.stopPropagation(); - } - }); - - // general link management - new window/tab - $('a[rel="external"]:not([title])') - .attr('title', gettext('This link will open in a new browser window/tab')); - $('a[rel="external"]').attr({ - rel: 'noopener external', - target: '_blank' - }); - - // general link management - lean modal window - $('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({ - overlay: 0.50, - closeButton: '.action-modal-close' - }); - $('.action-modal-close').click(function(e) { - (e).preventDefault(); - }); - - // general link management - smooth scrolling page links - $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); - - IframeUtils.iframeBinding(); - - // disable ajax caching in IE so that backbone fetches work - if ($.browser.msie) { - $.ajaxSetup({cache: false}); - } - - // Initiate the edx tool kit dropdown menu - if ($('.js-header-user-menu').length) { - dropdownMenuView = new DropdownMenuView({ - el: '.js-header-user-menu' - }); - dropdownMenuView.postRender(); - } - - window.studioNavMenuActive = true; + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $(this).attr('href') }); - }); // end require() + } + + function hideNotification(e) { + (e).preventDefault(); + $(this) + .closest('.wrapper-notification') + .removeClass('is-shown') + .addClass('is-hiding') + .attr('aria-hidden', 'true'); + } + + function hideAlert(e) { + (e).preventDefault(); + $(this).closest('.wrapper-alert').removeClass('is-shown'); + } + + domReady(function() { + var dropdownMenuView; + + $body = $('body'); + + $body.on('click', '.embeddable-xml-input', function() { + $(this).select(); + }); + + $body.addClass('js'); + + // alerts/notifications - manual close + $('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert); + $('.action-notification-close').bind('click', hideNotification); + + // nav - dropdown related + $body.click(function() { + $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); + $('.nav-dd .nav-item .title').removeClass('is-selected'); + }); + + $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { + var $subnav = $(this).find('.wrapper-nav-sub'), + $title = $(this).find('.title'); + + if ($subnav.hasClass('is-shown')) { + $subnav.removeClass('is-shown'); + $title.removeClass('is-selected'); + } else { + $('.nav-dd .nav-item .title').removeClass('is-selected'); + $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); + $title.addClass('is-selected'); + $subnav.addClass('is-shown'); + // if propagation is not stopped, the event will bubble up to the + // body element, which will close the dropdown. + e.stopPropagation(); + } + }); + + // general link management - new window/tab + $('a[rel="external"]:not([title])') + .attr('title', gettext('This link will open in a new browser window/tab')); + $('a[rel="external"]').attr({ + rel: 'noopener external', + target: '_blank' + }); + + // general link management - lean modal window + $('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({ + overlay: 0.50, + closeButton: '.action-modal-close' + }); + $('.action-modal-close').click(function(e) { + (e).preventDefault(); + }); + + // general link management - smooth scrolling page links + $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); + + IframeUtils.iframeBinding(); + + // disable ajax caching in IE so that backbone fetches work + if ($.browser.msie) { + $.ajaxSetup({cache: false}); + } + + // Initiate the edx tool kit dropdown menu + if ($('.js-header-user-menu').length) { + dropdownMenuView = new DropdownMenuView({ + el: '.js-header-user-menu' + }); + dropdownMenuView.postRender(); + } + + window.studioNavMenuActive = true; + }); +}); // end require() diff --git a/cms/static/js/certificates/factories/certificates_page_factory.js b/cms/static/js/certificates/factories/certificates_page_factory.js index a367f52cce..5c13f73e8e 100644 --- a/cms/static/js/certificates/factories/certificates_page_factory.js +++ b/cms/static/js/certificates/factories/certificates_page_factory.js @@ -21,7 +21,7 @@ define([ function($, CertificatesCollection, Certificate, CertificatesPage, CertificatePreview) { 'use strict'; return function(certificatesJson, certificateUrl, courseOutlineUrl, courseModes, certificateWebViewUrl, - isActive, certificateActivationHandlerUrl) { + isActive, certificateActivationHandlerUrl) { // Initialize the model collection, passing any necessary options to the constructor var certificatesCollection = new CertificatesCollection(certificatesJson, { parse: true, diff --git a/cms/static/js/certificates/models/certificate.js b/cms/static/js/certificates/models/certificate.js index d0e6ae6e9f..97a2735d71 100644 --- a/cms/static/js/certificates/models/certificate.js +++ b/cms/static/js/certificates/models/certificate.js @@ -10,89 +10,89 @@ define([ 'js/certificates/models/signatory', 'js/certificates/collections/signatories' ], - function(_, Backbone, BackboneRelational, BackboneAssociations, gettext, CoffeeSrcMain, - SignatoryModel, SignatoryCollection) { - 'use strict'; - var Certificate = Backbone.RelationalModel.extend({ - idAttribute: 'id', - defaults: { - // Metadata fields currently displayed in web forms - course_title: '', +function(_, Backbone, BackboneRelational, BackboneAssociations, gettext, CoffeeSrcMain, + SignatoryModel, SignatoryCollection) { + 'use strict'; + var Certificate = Backbone.RelationalModel.extend({ + idAttribute: 'id', + defaults: { + // Metadata fields currently displayed in web forms + course_title: '', - // Metadata fields not currently displayed in web forms - name: 'Name of the certificate', - description: 'Description of the certificate', + // Metadata fields not currently displayed in web forms + name: 'Name of the certificate', + description: 'Description of the certificate', - // Internal-use only, not displayed in web forms - version: 1, - is_active: false - }, + // Internal-use only, not displayed in web forms + version: 1, + is_active: false + }, - // Certificate child collection/model mappings (backbone-relational) - relations: [{ - type: Backbone.HasMany, - key: 'signatories', - relatedModel: SignatoryModel, - collectionType: SignatoryCollection, - reverseRelation: { - key: 'certificate', - includeInJSON: 'id' - } - }], - - initialize: function(attributes, options) { - // Set up the initial state of the attributes set for this model instance - this.canBeEmpty = options && options.canBeEmpty; - if (options.add && !attributes.signatories) { - // Ensure at least one child Signatory model is defined for any new Certificate model - attributes.signatories = new SignatoryModel({certificate: this}); - } - this.setOriginalAttributes(); - return this; - }, - - parse: function(response) { - // Parse must be defined for the model, but does not need to do anything special right now - return response; - }, - - setOriginalAttributes: function() { - // Remember the current state of this model (enables edit->cancel use cases) - this._originalAttributes = this.parse(this.toJSON()); - - this.get('signatories').each(function(modelSignatory) { - modelSignatory.setOriginalAttributes(); - }); - - // If no url is defined for the signatories child collection we'll need to create that here as well - if (!this.isNew() && !this.get('signatories').url) { - this.get('signatories').url = this.collection.url + '/' + this.get('id') + '/signatories'; - } - }, - - validate: function(attrs) { - // Ensure the provided attributes set meets our expectations for format, type, etc. - if (!attrs.name.trim()) { - return { - message: gettext('Certificate name is required.'), - attributes: {name: true} - }; - } - var allSignatoriesValid = _.every(attrs.signatories.models, function(signatory) { - return signatory.isValid(); - }); - if (!allSignatoriesValid) { - return { - message: gettext('Signatory field(s) has invalid data.'), - attributes: {signatories: attrs.signatories.models} - }; - } - }, - - reset: function() { - // Revert the attributes of this model instance back to initial state - this.set(this._originalAttributes, {parse: true, validate: true}); + // Certificate child collection/model mappings (backbone-relational) + relations: [{ + type: Backbone.HasMany, + key: 'signatories', + relatedModel: SignatoryModel, + collectionType: SignatoryCollection, + reverseRelation: { + key: 'certificate', + includeInJSON: 'id' } - }); - return Certificate; + }], + + initialize: function(attributes, options) { + // Set up the initial state of the attributes set for this model instance + this.canBeEmpty = options && options.canBeEmpty; + if (options.add && !attributes.signatories) { + // Ensure at least one child Signatory model is defined for any new Certificate model + attributes.signatories = new SignatoryModel({certificate: this}); + } + this.setOriginalAttributes(); + return this; + }, + + parse: function(response) { + // Parse must be defined for the model, but does not need to do anything special right now + return response; + }, + + setOriginalAttributes: function() { + // Remember the current state of this model (enables edit->cancel use cases) + this._originalAttributes = this.parse(this.toJSON()); + + this.get('signatories').each(function(modelSignatory) { + modelSignatory.setOriginalAttributes(); + }); + + // If no url is defined for the signatories child collection we'll need to create that here as well + if (!this.isNew() && !this.get('signatories').url) { + this.get('signatories').url = this.collection.url + '/' + this.get('id') + '/signatories'; + } + }, + + validate: function(attrs) { + // Ensure the provided attributes set meets our expectations for format, type, etc. + if (!attrs.name.trim()) { + return { + message: gettext('Certificate name is required.'), + attributes: {name: true} + }; + } + var allSignatoriesValid = _.every(attrs.signatories.models, function(signatory) { + return signatory.isValid(); + }); + if (!allSignatoriesValid) { + return { + message: gettext('Signatory field(s) has invalid data.'), + attributes: {signatories: attrs.signatories.models} + }; + } + }, + + reset: function() { + // Revert the attributes of this model instance back to initial state + this.set(this._originalAttributes, {parse: true, validate: true}); + } }); + return Certificate; +}); diff --git a/cms/static/js/certificates/spec/views/certificate_details_spec.js b/cms/static/js/certificates/spec/views/certificate_details_spec.js index 7a0332bda8..d1c810f2aa 100644 --- a/cms/static/js/certificates/spec/views/certificate_details_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_details_spec.js @@ -15,7 +15,7 @@ define([ 'js/certificates/spec/custom_matchers' ], function(_, Course, CertificatesCollection, CertificateModel, CertificateDetailsView, CertificatePreview, - Notification, AjaxHelpers, TemplateHelpers, ViewHelpers, ValidationHelpers, CustomMatchers) { + Notification, AjaxHelpers, TemplateHelpers, ViewHelpers, ValidationHelpers, CustomMatchers) { 'use strict'; var SELECTORS = { diff --git a/cms/static/js/certificates/spec/views/certificate_editor_spec.js b/cms/static/js/certificates/spec/views/certificate_editor_spec.js index 39416e56a8..64ef492c82 100644 --- a/cms/static/js/certificates/spec/views/certificate_editor_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_editor_spec.js @@ -15,7 +15,7 @@ define([ 'js/certificates/spec/custom_matchers' ], function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, CertificateEditorView, - Notification, AjaxHelpers, TemplateHelpers, ViewHelpers, ValidationHelpers, CustomMatchers) { + Notification, AjaxHelpers, TemplateHelpers, ViewHelpers, ValidationHelpers, CustomMatchers) { 'use strict'; var MAX_SIGNATORIES_LIMIT = 10; diff --git a/cms/static/js/certificates/spec/views/certificates_list_spec.js b/cms/static/js/certificates/spec/views/certificates_list_spec.js index 7925810897..fec1829611 100644 --- a/cms/static/js/certificates/spec/views/certificates_list_spec.js +++ b/cms/static/js/certificates/spec/views/certificates_list_spec.js @@ -16,8 +16,8 @@ define([ 'js/certificates/spec/custom_matchers' ], function(_, Course, CertificatesCollection, CertificateModel, CertificateDetailsView, CertificateEditorView, - CertificateItemView, CertificatesListView, CertificatePreview, Notification, AjaxHelpers, TemplateHelpers, - CustomMatchers) { + CertificateItemView, CertificatesListView, CertificatePreview, Notification, AjaxHelpers, TemplateHelpers, + CustomMatchers) { 'use strict'; var SELECTORS = { diff --git a/cms/static/js/certificates/views/certificate_details.js b/cms/static/js/certificates/views/certificate_details.js index 121be569db..4c62c5fa0c 100644 --- a/cms/static/js/certificates/views/certificate_details.js +++ b/cms/static/js/certificates/views/certificate_details.js @@ -13,7 +13,7 @@ define([ 'text!templates/certificate-details.underscore' ], function($, _, str, gettext, BaseView, SignatoryModel, SignatoryDetailsView, ViewUtils, smoothScroll, - certificateDetailsTemplate) { + certificateDetailsTemplate) { 'use strict'; var CertificateDetailsView = BaseView.extend({ tagName: 'div', diff --git a/cms/static/js/certificates/views/certificate_editor.js b/cms/static/js/certificates/views/certificate_editor.js index fa19bd2de2..6b0365ffbf 100644 --- a/cms/static/js/certificates/views/certificate_editor.js +++ b/cms/static/js/certificates/views/certificate_editor.js @@ -12,7 +12,7 @@ define([ 'edx-ui-toolkit/js/utils/html-utils' ], function($, _, Backbone, gettext, - ListItemEditorView, SignatoryModel, SignatoryEditorView, certificateEditorTemplate, HtmlUtils) { + ListItemEditorView, SignatoryModel, SignatoryEditorView, certificateEditorTemplate, HtmlUtils) { 'use strict'; // If signatories limit is required to specific value then we can change it. diff --git a/cms/static/js/certificates/views/signatory_details.js b/cms/static/js/certificates/views/signatory_details.js index 9286b28259..e3186ea7fe 100644 --- a/cms/static/js/certificates/views/signatory_details.js +++ b/cms/static/js/certificates/views/signatory_details.js @@ -15,7 +15,7 @@ define([ 'edx-ui-toolkit/js/utils/html-utils' ], function($, _, str, Backbone, gettext, TemplateUtils, ViewUtils, BaseView, SignatoryEditorView, - signatoryDetailsTemplate, signatoryActionsTemplate, HtmlUtils) { + signatoryDetailsTemplate, signatoryActionsTemplate, HtmlUtils) { 'use strict'; var SignatoryDetailsView = BaseView.extend({ tagName: 'div', diff --git a/cms/static/js/certificates/views/signatory_editor.js b/cms/static/js/certificates/views/signatory_editor.js index 9c70e68a44..1e64a058be 100644 --- a/cms/static/js/certificates/views/signatory_editor.js +++ b/cms/static/js/certificates/views/signatory_editor.js @@ -15,8 +15,8 @@ define([ 'edx-ui-toolkit/js/utils/html-utils' ], function($, _, Backbone, gettext, - TemplateUtils, ViewUtils, PromptView, NotificationView, FileUploadModel, FileUploadDialog, - signatoryEditorTemplate, HtmlUtils) { + TemplateUtils, ViewUtils, PromptView, NotificationView, FileUploadModel, FileUploadDialog, + signatoryEditorTemplate, HtmlUtils) { 'use strict'; var SignatoryEditorView = Backbone.View.extend({ tagName: 'div', diff --git a/cms/static/js/collections/textbook.js b/cms/static/js/collections/textbook.js index 9554a36393..d388ca9800 100644 --- a/cms/static/js/collections/textbook.js +++ b/cms/static/js/collections/textbook.js @@ -1,8 +1,8 @@ define(['backbone', 'js/models/textbook'], - function(Backbone, TextbookModel) { - var TextbookCollection = Backbone.Collection.extend({ - model: TextbookModel, - url: function() { return CMS.URL.TEXTBOOKS; } - }); - return TextbookCollection; + function(Backbone, TextbookModel) { + var TextbookCollection = Backbone.Collection.extend({ + model: TextbookModel, + url: function() { return CMS.URL.TEXTBOOKS; } }); + return TextbookCollection; + }); diff --git a/cms/static/js/factories/base.js b/cms/static/js/factories/base.js index 2a952f94db..ea0602cbdd 100644 --- a/cms/static/js/factories/base.js +++ b/cms/static/js/factories/base.js @@ -2,7 +2,7 @@ // of RequireJS define(['js/base', 'cms/js/main', 'js/src/logger', 'datepair', 'accessibility', 'ieshim', 'tooltip_manager', 'lang_edx', 'js/models/course', 'jquery_extend_patch'], - function() { - 'use strict'; - } +function() { + 'use strict'; +} ); diff --git a/cms/static/js/factories/group_configurations.js b/cms/static/js/factories/group_configurations.js index 424f60a50c..285e23f2d6 100644 --- a/cms/static/js/factories/group_configurations.js +++ b/cms/static/js/factories/group_configurations.js @@ -3,10 +3,10 @@ define([ ], function(GroupConfigurationCollection, GroupConfigurationModel, GroupConfigurationsPage) { 'use strict'; return function(experimentsEnabled, - experimentGroupConfigurationsJson, - allGroupConfigurationJson, - groupConfigurationUrl, - courseOutlineUrl) { + experimentGroupConfigurationsJson, + allGroupConfigurationJson, + groupConfigurationUrl, + courseOutlineUrl) { var experimentGroupConfigurations = new GroupConfigurationCollection( experimentGroupConfigurationsJson, {parse: true} ), diff --git a/cms/static/js/factories/index.js b/cms/static/js/factories/index.js index 10f395ef1d..9fa8071a13 100644 --- a/cms/static/js/factories/index.js +++ b/cms/static/js/factories/index.js @@ -6,9 +6,9 @@ define(['jquery.form', 'js/index'], function() { e.preventDefault(); $(this) .closest('.wrapper-creationrights') - .toggleClass('is-shown') + .toggleClass('is-shown') .find('.ui-toggle-control') - .toggleClass('current'); + .toggleClass('current'); }); var reloadPage = function() { @@ -19,10 +19,10 @@ define(['jquery.form', 'js/index'], function() { $('#request-coursecreator-submit') .toggleClass('has-error') .find('.label') - .text('Sorry, there was error with your request'); + .text('Sorry, there was error with your request'); $('#request-coursecreator-submit') .find('.fa-cog') - .toggleClass('fa-spin'); + .toggleClass('fa-spin'); }; $('#request-coursecreator').ajaxForm({ diff --git a/cms/static/js/factories/manage_users.js b/cms/static/js/factories/manage_users.js index 837ea6fec7..ba364ee38e 100644 --- a/cms/static/js/factories/manage_users.js +++ b/cms/static/js/factories/manage_users.js @@ -2,34 +2,34 @@ Code for editing users and assigning roles within a course team context. */ define(['underscore', 'gettext', 'js/views/manage_users_and_roles'], -function(_, gettext, ManageUsersAndRoles) { - 'use strict'; - return function(containerName, users, tplUserURL, current_user_id, allow_actions) { - function updateMessages(messages) { - var local_messages = _.extend({}, messages); - local_messages.alreadyMember.title = gettext('Already a course team member'); - local_messages.deleteUser.messageTpl = gettext( - 'Are you sure you want to delete {email} from the course team for “{container}”?' - ); - return local_messages; - } - // Roles order are important: first role is considered initial role (the role added to user when (s)he's added - // Last role is considered an admin role (unrestricted access + ability to manage other users' permissions) - // Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed - var roles = [{key: 'staff', name: gettext('Staff')}, {key: 'instructor', name: gettext('Admin')}]; + function(_, gettext, ManageUsersAndRoles) { + 'use strict'; + return function(containerName, users, tplUserURL, current_user_id, allow_actions) { + function updateMessages(messages) { + var local_messages = _.extend({}, messages); + local_messages.alreadyMember.title = gettext('Already a course team member'); + local_messages.deleteUser.messageTpl = gettext( + 'Are you sure you want to delete {email} from the course team for “{container}”?' + ); + return local_messages; + } + // Roles order are important: first role is considered initial role (the role added to user when (s)he's added + // Last role is considered an admin role (unrestricted access + ability to manage other users' permissions) + // Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed + var roles = [{key: 'staff', name: gettext('Staff')}, {key: 'instructor', name: gettext('Admin')}]; - var options = { - el: $('#content'), - containerName: containerName, - tplUserURL: tplUserURL, - roles: roles, - users: users, - messages_modifier: updateMessages, - current_user_id: current_user_id, - allow_actions: allow_actions + var options = { + el: $('#content'), + containerName: containerName, + tplUserURL: tplUserURL, + roles: roles, + users: users, + messages_modifier: updateMessages, + current_user_id: current_user_id, + allow_actions: allow_actions + }; + + var view = new ManageUsersAndRoles(options); + view.render(); }; - - var view = new ManageUsersAndRoles(options); - view.render(); - }; -}); + }); diff --git a/cms/static/js/factories/manage_users_lib.js b/cms/static/js/factories/manage_users_lib.js index 8bfe07134d..a2ffc86362 100644 --- a/cms/static/js/factories/manage_users_lib.js +++ b/cms/static/js/factories/manage_users_lib.js @@ -2,38 +2,38 @@ Code for editing users and assigning roles within a library context. */ define(['underscore', 'gettext', 'js/views/manage_users_and_roles'], -function(_, gettext, ManageUsersAndRoles) { - 'use strict'; - return function(containerName, users, tplUserURL, current_user_id, allow_actions) { - function updateMessages(messages) { - var local_messages = _.extend({}, messages); - local_messages.alreadyMember.title = gettext('Already a library team member'); - local_messages.deleteUser.messageTpl = gettext( - 'Are you sure you want to delete {email} from the library “{container}”?' - ); - return local_messages; - } - // Roles order are important: first role is considered initial role (the role added to user when (s)he's added - // Last role is considered an admin role (unrestricted access + ability to manage other users' permissions) - // Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed - var roles = [ - {key: 'library_user', name: gettext('Library User')}, - {key: 'staff', name: gettext('Staff')}, - {key: 'instructor', name: gettext('Admin')} - ]; + function(_, gettext, ManageUsersAndRoles) { + 'use strict'; + return function(containerName, users, tplUserURL, current_user_id, allow_actions) { + function updateMessages(messages) { + var local_messages = _.extend({}, messages); + local_messages.alreadyMember.title = gettext('Already a library team member'); + local_messages.deleteUser.messageTpl = gettext( + 'Are you sure you want to delete {email} from the library “{container}”?' + ); + return local_messages; + } + // Roles order are important: first role is considered initial role (the role added to user when (s)he's added + // Last role is considered an admin role (unrestricted access + ability to manage other users' permissions) + // Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed + var roles = [ + {key: 'library_user', name: gettext('Library User')}, + {key: 'staff', name: gettext('Staff')}, + {key: 'instructor', name: gettext('Admin')} + ]; - var options = { - el: $('#content'), - containerName: containerName, - tplUserURL: tplUserURL, - roles: roles, - users: users, - messages_modifier: updateMessages, - current_user_id: current_user_id, - allow_actions: allow_actions + var options = { + el: $('#content'), + containerName: containerName, + tplUserURL: tplUserURL, + roles: roles, + users: users, + messages_modifier: updateMessages, + current_user_id: current_user_id, + allow_actions: allow_actions + }; + + var view = new ManageUsersAndRoles(options); + view.render(); }; - - var view = new ManageUsersAndRoles(options); - view.render(); - }; -}); + }); diff --git a/cms/static/js/features/import/views/import.js b/cms/static/js/features/import/views/import.js index 94e31d6394..ee1af307d6 100644 --- a/cms/static/js/features/import/views/import.js +++ b/cms/static/js/features/import/views/import.js @@ -150,13 +150,13 @@ define( _.map($dom.stages, completeStage); $dom.successStage - .find('.item-progresspoint-success-date') - .text('(' + date + ' at ' + time + ' UTC)'); + .find('.item-progresspoint-success-date') + .text('(' + date + ' at ' + time + ' UTC)'); break; case STATE.ERROR: - // Make all stages up to, and including, the error stage 'complete'. + // Make all stages up to, and including, the error stage 'complete'. $prev = $dom.stages.slice(0, current.stage + 1); $curr = $dom.stages.eq(current.stage); $next = $dom.stages.slice(current.stage + 1); diff --git a/cms/static/js/features_jsx/.eslintrc.js b/cms/static/js/features_jsx/.eslintrc.js index 15de20e01b..0a86988ebb 100644 --- a/cms/static/js/features_jsx/.eslintrc.js +++ b/cms/static/js/features_jsx/.eslintrc.js @@ -1,7 +1,14 @@ module.exports = { - extends: '@edx/eslint-config', - root: true, - settings: { - 'import/resolver': 'webpack', - }, + extends: '@edx/eslint-config', + root: true, + settings: { + 'import/resolver': 'webpack', + }, + rules: { + indent: ['error', 4], + 'import/extensions': 'off', + 'import/no-unresolved': 'off', + 'react/jsx-indent': 'off', + 'react/jsx-indent-props': 'off', + }, }; diff --git a/cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx b/cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx index 7e7326ffe3..dd00984dc0 100644 --- a/cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx +++ b/cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx @@ -6,83 +6,83 @@ import React from 'react'; import ReactDOM from 'react-dom'; export function CourseOrLibraryListing(props) { - const allowReruns = props.allowReruns; - const linkClass = props.linkClass; - const idBase = props.idBase; + const allowReruns = props.allowReruns; + const linkClass = props.linkClass; + const idBase = props.idBase; - const renderCourseMetadata = (item, i) => ( -
'), errorMessage, edx.HtmlUtils.HTML('
')); - $('.create-course .wrap-error').addClass('is-shown'); - edx.HtmlUtils.setHtml($('#course_creation_error'), msg); - $('.new-course-save').addClass('is-disabled').attr('aria-disabled', true); - }); + var course_info = { + org: org, + number: number, + display_name: display_name, + run: run }; - var rtlTextDirection = function() { - var Selectors = { - new_course_run: '#new-course-run' - }; - - if ($('body').hasClass('rtl')) { - $(Selectors.new_course_run).addClass('course-run-text-direction placeholder-text-direction'); - $(Selectors.new_course_run).on('input', function() { - if (this.value === '') { - $(Selectors.new_course_run).addClass('placeholder-text-direction'); - } else { - $(Selectors.new_course_run).removeClass('placeholder-text-direction'); - } - }); - } - }; - - var makeCancelHandler = function(addType) { - return function(e) { - e.preventDefault(); - $('.new-' + addType + '-button').removeClass('is-disabled').attr('aria-disabled', false); - $('.wrapper-create-' + addType).removeClass('is-shown'); - // Clear out existing fields and errors - $('#create-' + addType + '-form input[type=text]').val(''); - $('#' + addType + '_creation_error').html(''); - $('.create-' + addType + ' .wrap-error').removeClass('is-shown'); - $('.new-' + addType + '-save').off('click'); - }; - }; - - var addNewCourse = function(e) { - var $newCourse, - $cancelButton, - $courseName; - e.preventDefault(); - $('.new-course-button').addClass('is-disabled').attr('aria-disabled', true); + analytics.track('Created a Course', course_info); + CreateCourseUtils.create(course_info, function(errorMessage) { + var msg = edx.HtmlUtils.joinHtml(edx.HtmlUtils.HTML(''), errorMessage, edx.HtmlUtils.HTML('
')); + $('.create-course .wrap-error').addClass('is-shown'); + edx.HtmlUtils.setHtml($('#course_creation_error'), msg); $('.new-course-save').addClass('is-disabled').attr('aria-disabled', true); - $newCourse = $('.wrapper-create-course').addClass('is-shown'); - $cancelButton = $newCourse.find('.new-course-cancel'); - $courseName = $('.new-course-name'); - $courseName.focus().select(); - $('.new-course-save').on('click', saveNewCourse); - $cancelButton.bind('click', makeCancelHandler('course')); - CancelOnEscape($cancelButton); - CreateCourseUtils.setupOrgAutocomplete(); - CreateCourseUtils.configureHandlers(); - rtlTextDirection(); + }); + }; + + var rtlTextDirection = function() { + var Selectors = { + new_course_run: '#new-course-run' }; - var saveNewLibrary = function(e) { - e.preventDefault(); - - if (CreateLibraryUtils.hasInvalidRequiredFields()) { - return; - } - - var $newLibraryForm = $(this).closest('#create-library-form'); - var display_name = $newLibraryForm.find('.new-library-name').val(); - var org = $newLibraryForm.find('.new-library-org').val(); - var number = $newLibraryForm.find('.new-library-number').val(); - - var lib_info = { - org: org, - number: number, - display_name: display_name - }; - - analytics.track('Created a Library', lib_info); - CreateLibraryUtils.create(lib_info, function(errorMessage) { - var msg = edx.HtmlUtils.joinHtml(edx.HtmlUtils.HTML(''), errorMessage, edx.HtmlUtils.HTML('
')); - $('.create-library .wrap-error').addClass('is-shown'); - edx.HtmlUtils.setHtml($('#library_creation_error'), msg); - $('.new-library-save').addClass('is-disabled').attr('aria-disabled', true); + if ($('body').hasClass('rtl')) { + $(Selectors.new_course_run).addClass('course-run-text-direction placeholder-text-direction'); + $(Selectors.new_course_run).on('input', function() { + if (this.value === '') { + $(Selectors.new_course_run).addClass('placeholder-text-direction'); + } else { + $(Selectors.new_course_run).removeClass('placeholder-text-direction'); + } }); - }; + } + }; - var addNewLibrary = function(e) { + var makeCancelHandler = function(addType) { + return function(e) { e.preventDefault(); - $('.new-library-button').addClass('is-disabled').attr('aria-disabled', true); - $('.new-library-save').addClass('is-disabled').attr('aria-disabled', true); - var $newLibrary = $('.wrapper-create-library').addClass('is-shown'); - var $cancelButton = $newLibrary.find('.new-library-cancel'); - var $libraryName = $('.new-library-name'); - $libraryName.focus().select(); - $('.new-library-save').on('click', saveNewLibrary); - $cancelButton.bind('click', makeCancelHandler('library')); - CancelOnEscape($cancelButton); + $('.new-' + addType + '-button').removeClass('is-disabled').attr('aria-disabled', false); + $('.wrapper-create-' + addType).removeClass('is-shown'); + // Clear out existing fields and errors + $('#create-' + addType + '-form input[type=text]').val(''); + $('#' + addType + '_creation_error').html(''); + $('.create-' + addType + ' .wrap-error').removeClass('is-shown'); + $('.new-' + addType + '-save').off('click'); + }; + }; - CreateLibraryUtils.configureHandlers(); + var addNewCourse = function(e) { + var $newCourse, + $cancelButton, + $courseName; + e.preventDefault(); + $('.new-course-button').addClass('is-disabled').attr('aria-disabled', true); + $('.new-course-save').addClass('is-disabled').attr('aria-disabled', true); + $newCourse = $('.wrapper-create-course').addClass('is-shown'); + $cancelButton = $newCourse.find('.new-course-cancel'); + $courseName = $('.new-course-name'); + $courseName.focus().select(); + $('.new-course-save').on('click', saveNewCourse); + $cancelButton.bind('click', makeCancelHandler('course')); + CancelOnEscape($cancelButton); + CreateCourseUtils.setupOrgAutocomplete(); + CreateCourseUtils.configureHandlers(); + rtlTextDirection(); + }; + + var saveNewLibrary = function(e) { + e.preventDefault(); + + if (CreateLibraryUtils.hasInvalidRequiredFields()) { + return; + } + + var $newLibraryForm = $(this).closest('#create-library-form'); + var display_name = $newLibraryForm.find('.new-library-name').val(); + var org = $newLibraryForm.find('.new-library-org').val(); + var number = $newLibraryForm.find('.new-library-number').val(); + + var lib_info = { + org: org, + number: number, + display_name: display_name }; - var showTab = function(tab) { - return function(e) { - e.preventDefault(); - window.location.hash = tab; - $('.courses-tab').toggleClass('active', tab === 'courses-tab'); - $('.archived-courses-tab').toggleClass('active', tab === 'archived-courses-tab'); - $('.libraries-tab').toggleClass('active', tab === 'libraries-tab'); + analytics.track('Created a Library', lib_info); + CreateLibraryUtils.create(lib_info, function(errorMessage) { + var msg = edx.HtmlUtils.joinHtml(edx.HtmlUtils.HTML(''), errorMessage, edx.HtmlUtils.HTML('
')); + $('.create-library .wrap-error').addClass('is-shown'); + edx.HtmlUtils.setHtml($('#library_creation_error'), msg); + $('.new-library-save').addClass('is-disabled').attr('aria-disabled', true); + }); + }; + + var addNewLibrary = function(e) { + e.preventDefault(); + $('.new-library-button').addClass('is-disabled').attr('aria-disabled', true); + $('.new-library-save').addClass('is-disabled').attr('aria-disabled', true); + var $newLibrary = $('.wrapper-create-library').addClass('is-shown'); + var $cancelButton = $newLibrary.find('.new-library-cancel'); + var $libraryName = $('.new-library-name'); + $libraryName.focus().select(); + $('.new-library-save').on('click', saveNewLibrary); + $cancelButton.bind('click', makeCancelHandler('library')); + CancelOnEscape($cancelButton); + + CreateLibraryUtils.configureHandlers(); + }; + + var showTab = function(tab) { + return function(e) { + e.preventDefault(); + window.location.hash = tab; + $('.courses-tab').toggleClass('active', tab === 'courses-tab'); + $('.archived-courses-tab').toggleClass('active', tab === 'archived-courses-tab'); + $('.libraries-tab').toggleClass('active', tab === 'libraries-tab'); // Also toggle this course-related notice shown below the course tab, if it is present: - $('.wrapper-creationrights').toggleClass('is-hidden', tab !== 'courses-tab'); - }; + $('.wrapper-creationrights').toggleClass('is-hidden', tab !== 'courses-tab'); }; + }; - var onReady = function() { - var courseTabHref = $('#course-index-tabs .courses-tab a').attr('href'); - var libraryTabHref = $('#course-index-tabs .libraries-tab a').attr('href'); - var ArchivedTabHref = $('#course-index-tabs .archived-courses-tab a').attr('href'); + var onReady = function() { + var courseTabHref = $('#course-index-tabs .courses-tab a').attr('href'); + var libraryTabHref = $('#course-index-tabs .libraries-tab a').attr('href'); + var ArchivedTabHref = $('#course-index-tabs .archived-courses-tab a').attr('href'); - $('.new-course-button').bind('click', addNewCourse); - $('.new-library-button').bind('click', addNewLibrary); + $('.new-course-button').bind('click', addNewCourse); + $('.new-library-button').bind('click', addNewLibrary); - $('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function() { - ViewUtils.reload(); - })); + $('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function() { + ViewUtils.reload(); + })); - $('.action-reload').bind('click', ViewUtils.reload); + $('.action-reload').bind('click', ViewUtils.reload); - if (courseTabHref === '#') { - $('#course-index-tabs .courses-tab').bind('click', showTab('courses-tab')); - } + if (courseTabHref === '#') { + $('#course-index-tabs .courses-tab').bind('click', showTab('courses-tab')); + } - if (libraryTabHref === '#') { - $('#course-index-tabs .libraries-tab').bind('click', showTab('libraries-tab')); - } + if (libraryTabHref === '#') { + $('#course-index-tabs .libraries-tab').bind('click', showTab('libraries-tab')); + } - if (ArchivedTabHref === '#') { - $('#course-index-tabs .archived-courses-tab').bind('click', showTab('archived-courses-tab')); - } - if (window.location.hash) { - $(window.location.hash.replace('#', '.')).first('a').trigger('click'); - } - }; + if (ArchivedTabHref === '#') { + $('#course-index-tabs .archived-courses-tab').bind('click', showTab('archived-courses-tab')); + } + if (window.location.hash) { + $(window.location.hash.replace('#', '.')).first('a').trigger('click'); + } + }; - domReady(onReady); + domReady(onReady); - return { - onReady: onReady - }; - }); + return { + onReady: onReady + }; +}); diff --git a/cms/static/js/maintenance/force_publish_course.js b/cms/static/js/maintenance/force_publish_course.js index e7eed221d6..c7b29bc8d0 100644 --- a/cms/static/js/maintenance/force_publish_course.js +++ b/cms/static/js/maintenance/force_publish_course.js @@ -54,29 +54,29 @@ function($, _, gettext, ViewUtils, StringUtils, HtmlUtils) { dataType: 'json', data: data }) - .done(function(response) { - if (response.error) { - showError('#course-id-container', response.msg); - } else { - if (response.msg) { - showError('#result-error', response.msg); + .done(function(response) { + if (response.error) { + showError('#course-id-container', response.msg); } else { - attrs = $.extend({}, response, {StringUtils: StringUtils}); - forcePublishedTemplate = HtmlUtils.template( - $('#force-published-course-response-tpl').text() - ); - HtmlUtils.setHtml($('#result-container'), forcePublishedTemplate(attrs)); + if (response.msg) { + showError('#result-error', response.msg); + } else { + attrs = $.extend({}, response, {StringUtils: StringUtils}); + forcePublishedTemplate = HtmlUtils.template( + $('#force-published-course-response-tpl').text() + ); + HtmlUtils.setHtml($('#result-container'), forcePublishedTemplate(attrs)); + } } - } - }) - .fail(function() { + }) + .fail(function() { // response.responseText here because it would show some strange output, it may output Traceback // sometimes if unexpected issue arises. Better to show just internal error when getting 500 error. - showError('#result-error', gettext('Internal Server Error.')); - }) - .always(function() { - deferred.resolve(); - }); + showError('#result-error', gettext('Internal Server Error.')); + }) + .always(function() { + deferred.resolve(); + }); }); }; }); diff --git a/cms/static/js/models/asset.js b/cms/static/js/models/asset.js index 3ad7020421..044c07d90f 100644 --- a/cms/static/js/models/asset.js +++ b/cms/static/js/models/asset.js @@ -1,5 +1,5 @@ define(['backbone'], function(Backbone) { - /** + /** * Simple model for an asset. */ var Asset = Backbone.Model.extend({ diff --git a/cms/static/js/models/license.js b/cms/static/js/models/license.js index ff90c60b8d..e60c174e2a 100644 --- a/cms/static/js/models/license.js +++ b/cms/static/js/models/license.js @@ -80,10 +80,10 @@ define(['backbone', 'underscore'], function(Backbone, _) { } var eqIndex = optionString.indexOf('='); if (eqIndex == -1) { - // this is a boolean flag + // this is a boolean flag optionsObj[optionString] = true; } else { - // this is a key-value pair + // this is a key-value pair var optionKey = optionString.substring(0, eqIndex); var optionVal = optionString.substring(eqIndex + 1); optionsObj[optionKey] = optionVal; diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 560816fabc..803b307600 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -1,167 +1,167 @@ define(['backbone', 'underscore', 'gettext', 'js/models/validation_helpers', 'js/utils/date_utils', 'edx-ui-toolkit/js/utils/string-utils' ], - function(Backbone, _, gettext, ValidationHelpers, DateUtils, StringUtils) { - 'use strict'; - var CourseDetails = Backbone.Model.extend({ - defaults: { - org: '', - course_id: '', - run: '', - language: '', - start_date: null, // maps to 'start' - end_date: null, // maps to 'end' - certificates_display_behavior: "", - certificate_available_date: null, - enrollment_start: null, - enrollment_end: null, - syllabus: null, - title: '', - subtitle: '', - duration: '', - description: '', - short_description: '', - overview: '', - intro_video: null, - effort: null, // an int or null, - license: null, - course_image_name: '', // the filename - course_image_asset_path: '', // the full URL (/c4x/org/course/num/asset/filename) - banner_image_name: '', - banner_image_asset_path: '', - video_thumbnail_image_name: '', - video_thumbnail_image_asset_path: '', - pre_requisite_courses: [], - entrance_exam_enabled: '', - entrance_exam_minimum_score_pct: '50', - learning_info: [], - instructor_info: {}, - self_paced: null - }, +function(Backbone, _, gettext, ValidationHelpers, DateUtils, StringUtils) { + 'use strict'; + var CourseDetails = Backbone.Model.extend({ + defaults: { + org: '', + course_id: '', + run: '', + language: '', + start_date: null, // maps to 'start' + end_date: null, // maps to 'end' + certificates_display_behavior: "", + certificate_available_date: null, + enrollment_start: null, + enrollment_end: null, + syllabus: null, + title: '', + subtitle: '', + duration: '', + description: '', + short_description: '', + overview: '', + intro_video: null, + effort: null, // an int or null, + license: null, + course_image_name: '', // the filename + course_image_asset_path: '', // the full URL (/c4x/org/course/num/asset/filename) + banner_image_name: '', + banner_image_asset_path: '', + video_thumbnail_image_name: '', + video_thumbnail_image_asset_path: '', + pre_requisite_courses: [], + entrance_exam_enabled: '', + entrance_exam_minimum_score_pct: '50', + learning_info: [], + instructor_info: {}, + self_paced: null + }, - validate: function(newattrs) { + validate: function(newattrs) { // Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation. - var errors = {}; - const CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS = { - END: "end", - END_WITH_DATE: "end_with_date", - EARLY_NO_INFO: "early_no_info" - }; + var errors = {}; + const CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS = { + END: "end", + END_WITH_DATE: "end_with_date", + EARLY_NO_INFO: "early_no_info" + }; - newattrs = DateUtils.convertDateStringsToObjects( - newattrs, - ['start_date', 'end_date', 'certificate_available_date', 'enrollment_start', 'enrollment_end'] - ); + newattrs = DateUtils.convertDateStringsToObjects( + newattrs, + ['start_date', 'end_date', 'certificate_available_date', 'enrollment_start', 'enrollment_end'] + ); - if (newattrs.start_date === null) { - errors.start_date = gettext('The course must have an assigned start date.'); - } + if (newattrs.start_date === null) { + errors.start_date = gettext('The course must have an assigned start date.'); + } - if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { - errors.end_date = gettext('The course end date must be later than the course start date.'); - } - if (newattrs.start_date && newattrs.enrollment_start && + if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { + errors.end_date = gettext('The course end date must be later than the course start date.'); + } + if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) { - errors.enrollment_start = gettext( - 'The course start date must be later than the enrollment start date.' - ); - } - if (newattrs.enrollment_start && newattrs.enrollment_end && + errors.enrollment_start = gettext( + 'The course start date must be later than the enrollment start date.' + ); + } + if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) { - errors.enrollment_end = gettext( - 'The enrollment start date cannot be after the enrollment end date.' - ); - } - if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) { - errors.enrollment_end = gettext('The enrollment end date cannot be after the course end date.'); - } - if (this.showCertificateAvailableDate && newattrs.end_date && newattrs.certificate_available_date && + errors.enrollment_end = gettext( + 'The enrollment start date cannot be after the enrollment end date.' + ); + } + if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) { + errors.enrollment_end = gettext('The enrollment end date cannot be after the course end date.'); + } + if (this.showCertificateAvailableDate && newattrs.end_date && newattrs.certificate_available_date && newattrs.certificate_available_date < newattrs.end_date) { - errors.certificate_available_date = gettext( - 'The certificate available date must be later than the course end date.' + errors.certificate_available_date = gettext( + 'The certificate available date must be later than the course end date.' + ); + } + + if (this.useV2CertDisplaySettings){ + if ( + newattrs.certificates_display_behavior + && !(Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).includes(newattrs.certificates_display_behavior)) + ) { + + errors.certificates_display_behavior = StringUtils.interpolate( + gettext( + "The certificate display behavior must be one of: {behavior_options}" + ), + { + behavior_options: Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).join(', ') + } ); } - if (this.useV2CertDisplaySettings){ - if ( - newattrs.certificates_display_behavior - && !(Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).includes(newattrs.certificates_display_behavior)) - ) { - - errors.certificates_display_behavior = StringUtils.interpolate( - gettext( - "The certificate display behavior must be one of: {behavior_options}" - ), - { - behavior_options: Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).join(', ') - } - ); - } - - // Throw error if there's a value for certificate_available_date - if( - (newattrs.certificate_available_date && newattrs.certificates_display_behavior != CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE) + // Throw error if there's a value for certificate_available_date + if( + (newattrs.certificate_available_date && newattrs.certificates_display_behavior != CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE) || (!newattrs.certificate_available_date && newattrs.certificates_display_behavior == CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE) - ){ - errors.certificates_display_behavior = StringUtils.interpolate( - gettext( - "The certificates display behavior must be {valid_option} if certificate available date is set." - ), - { - valid_option: CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE - } - ); - } + ){ + errors.certificates_display_behavior = StringUtils.interpolate( + gettext( + "The certificates display behavior must be {valid_option} if certificate available date is set." + ), + { + valid_option: CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE + } + ); } + } - if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) { - if (this._videokey_illegal_chars.exec(newattrs.intro_video)) { - errors.intro_video = gettext('Key should only contain letters, numbers, _, or -'); - } + if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) { + if (this._videokey_illegal_chars.exec(newattrs.intro_video)) { + errors.intro_video = gettext('Key should only contain letters, numbers, _, or -'); + } // TODO check if key points to a real video using google's youtube api + } + if (_.has(newattrs, 'entrance_exam_minimum_score_pct')) { + var range = { + min: 1, + max: 100 + }; + if (!ValidationHelpers.validateIntegerRange(newattrs.entrance_exam_minimum_score_pct, range)) { + errors.entrance_exam_minimum_score_pct = StringUtils.interpolate(gettext( + 'Please enter an integer between %(min)s and %(max)s.' + ), range, true); } - if (_.has(newattrs, 'entrance_exam_minimum_score_pct')) { - var range = { - min: 1, - max: 100 - }; - if (!ValidationHelpers.validateIntegerRange(newattrs.entrance_exam_minimum_score_pct, range)) { - errors.entrance_exam_minimum_score_pct = StringUtils.interpolate(gettext( - 'Please enter an integer between %(min)s and %(max)s.' - ), range, true); - } - } - if (!_.isEmpty(errors)) return errors; + } + if (!_.isEmpty(errors)) return errors; // NOTE don't return empty errors as that will be interpreted as an error state - }, + }, - _videokey_illegal_chars: /[^a-zA-Z0-9_-]/g, + _videokey_illegal_chars: /[^a-zA-Z0-9_-]/g, - set_videosource: function(newsource) { + set_videosource: function(newsource) { // newsource either is or just the "speed:key, *" string // returns the videosource for the preview which iss the key whose speed is closest to 1 - if (_.isEmpty(newsource) && + if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.set({intro_video: null}, {validate: true}); - // TODO remove all whitespace w/in string - else { - if (this.get('intro_video') !== newsource) this.set('intro_video', newsource, {validate: true}); - } - - return this.videosourceSample(); - }, - - videosourceSample: function() { - if (this.has('intro_video')) return '//www.youtube.com/embed/' + this.get('intro_video'); - else return ''; - }, - - // Whether or not the course pacing can be toggled. If the course - // has already started, returns false; otherwise, returns true. - canTogglePace: function() { - return new Date() <= new Date(this.get('start_date')); + // TODO remove all whitespace w/in string + else { + if (this.get('intro_video') !== newsource) this.set('intro_video', newsource, {validate: true}); } - }); - return CourseDetails; - }); // end define() + return this.videosourceSample(); + }, + + videosourceSample: function() { + if (this.has('intro_video')) return '//www.youtube.com/embed/' + this.get('intro_video'); + else return ''; + }, + + // Whether or not the course pacing can be toggled. If the course + // has already started, returns false; otherwise, returns true. + canTogglePace: function() { + return new Date() <= new Date(this.get('start_date')); + } + }); + + return CourseDetails; +}); // end define() diff --git a/cms/static/js/models/settings/course_grader.js b/cms/static/js/models/settings/course_grader.js index 6c53f0af60..094be7eae6 100644 --- a/cms/static/js/models/settings/course_grader.js +++ b/cms/static/js/models/settings/course_grader.js @@ -43,8 +43,8 @@ define(['backbone', 'underscore', 'gettext'], function(Backbone, _, gettext) { // FIXME b/c saves don't update the models if validation fails, we should // either revert the field value to the one in the model and make them make room // or figure out a holistic way to balance the vals across the whole -// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100) -// errors.weight = "The weights cannot add to more than 100."; + // if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100) + // errors.weight = "The weights cannot add to more than 100."; } } } @@ -63,8 +63,8 @@ define(['backbone', 'underscore', 'gettext'], function(Backbone, _, gettext) { } if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && !_.has(errors, 'min_count') && !_.has(errors, 'drop_count') && attrs.drop_count > attrs.min_count) { var template = _.template( - gettext('Cannot drop more <%- types %> assignments than are assigned.') - ); + gettext('Cannot drop more <%- types %> assignments than are assigned.') + ); errors.drop_count = template({types: attrs.type}); } if (!_.isEmpty(errors)) return errors; diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js index f954e5bf03..316e35a8d2 100644 --- a/cms/static/js/models/settings/course_grading_policy.js +++ b/cms/static/js/models/settings/course_grading_policy.js @@ -12,7 +12,7 @@ define(['backbone', 'js/models/location', 'js/collections/course_grader', 'edx-u parse: function(attributes) { if (attributes.graders) { var graderCollection; - // interesting race condition: if {parse:true} when newing, then parse called before .attributes created + // interesting race condition: if {parse:true} when newing, then parse called before .attributes created if (this.attributes && this.has('graders')) { graderCollection = this.get('graders'); graderCollection.reset(attributes.graders, {parse: true}); @@ -21,16 +21,16 @@ define(['backbone', 'js/models/location', 'js/collections/course_grader', 'edx-u } attributes.graders = graderCollection; } - // If grace period is unset or equal to 00:00 on the server, - // it's received as null + // If grace period is unset or equal to 00:00 on the server, + // it's received as null if (attributes.grace_period === null) { attributes.grace_period = { hours: 0, minutes: 0 }; } - // If minimum_grade_credit is unset or equal to 0 on the server, - // it's received as 0 + // If minimum_grade_credit is unset or equal to 0 on the server, + // it's received as 0 if (attributes.minimum_grade_credit === null) { attributes.minimum_grade_credit = 0; } @@ -51,7 +51,7 @@ define(['backbone', 'js/models/location', 'js/collections/course_grader', 'edx-u return newDate; }, parseGracePeriod: function(grace_period) { - // Enforce hours:minutes format + // Enforce hours:minutes format if (!/^\d{2,3}:\d{2}$/.test(grace_period)) { return null; } @@ -62,7 +62,7 @@ define(['backbone', 'js/models/location', 'js/collections/course_grader', 'edx-u }; }, parseMinimumGradeCredit: function(minimum_grade_credit) { - // get the value of minimum grade credit value in percentage + // get the value of minimum grade credit value in percentage if (isNaN(minimum_grade_credit)) { return 0; } @@ -78,16 +78,16 @@ define(['backbone', 'js/models/location', 'js/collections/course_grader', 'edx-u } } if (this.get('is_credit_course') && _.has(attrs, 'minimum_grade_credit')) { - // Getting minimum grade cutoff value + // Getting minimum grade cutoff value minimumGradeCutoff = _.min(_.values(attrs.grade_cutoffs)); if (isNaN(attrs.minimum_grade_credit) || attrs.minimum_grade_credit === null || attrs.minimum_grade_credit < minimumGradeCutoff) { return { minimum_grade_credit: StringUtils.interpolate( - gettext('Not able to set passing grade to less than %(minimum_grade_cutoff)s%.'), - {minimum_grade_cutoff: minimumGradeCutoff * 100}, - true - ) + gettext('Not able to set passing grade to less than %(minimum_grade_cutoff)s%.'), + {minimum_grade_cutoff: minimumGradeCutoff * 100}, + true + ) }; } } diff --git a/cms/static/js/models/textbook.js b/cms/static/js/models/textbook.js index 22eac9dd39..cc14824048 100644 --- a/cms/static/js/models/textbook.js +++ b/cms/static/js/models/textbook.js @@ -1,89 +1,89 @@ define(['backbone', 'underscore', 'gettext', 'js/models/chapter', 'js/collections/chapter', 'backbone.associations', 'cms/js/main'], - function(Backbone, _, gettext, ChapterModel, ChapterCollection) { - var Textbook = Backbone.AssociatedModel.extend({ - defaults: function() { - return { - name: '', - chapters: new ChapterCollection([{}]), - showChapters: false, - editing: false - }; - }, - relations: [{ - type: Backbone.Many, - key: 'chapters', - relatedModel: ChapterModel, - collectionType: ChapterCollection - }], - initialize: function() { - this.setOriginalAttributes(); - return this; - }, - setOriginalAttributes: function() { - this._originalAttributes = this.parse(this.toJSON()); - }, - reset: function() { - this.set(this._originalAttributes, {parse: true}); - }, - isDirty: function() { - return !_.isEqual(this._originalAttributes, this.parse(this.toJSON())); - }, - isEmpty: function() { - return !this.get('name') && this.get('chapters').isEmpty(); - }, - urlRoot: function() { return CMS.URL.TEXTBOOKS; }, - parse: function(response) { - var ret = $.extend(true, {}, response); - if ('tab_title' in ret && !('name' in ret)) { - ret.name = ret.tab_title; - delete ret.tab_title; - } - if ('url' in ret && !('chapters' in ret)) { - ret.chapters = {url: ret.url}; - delete ret.url; - } - _.each(ret.chapters, function(chapter, i) { - chapter.order = chapter.order || i + 1; - }); - return ret; - }, - toJSON: function() { - return { - tab_title: this.get('name'), - chapters: this.get('chapters').toJSON() - }; - }, +function(Backbone, _, gettext, ChapterModel, ChapterCollection) { + var Textbook = Backbone.AssociatedModel.extend({ + defaults: function() { + return { + name: '', + chapters: new ChapterCollection([{}]), + showChapters: false, + editing: false + }; + }, + relations: [{ + type: Backbone.Many, + key: 'chapters', + relatedModel: ChapterModel, + collectionType: ChapterCollection + }], + initialize: function() { + this.setOriginalAttributes(); + return this; + }, + setOriginalAttributes: function() { + this._originalAttributes = this.parse(this.toJSON()); + }, + reset: function() { + this.set(this._originalAttributes, {parse: true}); + }, + isDirty: function() { + return !_.isEqual(this._originalAttributes, this.parse(this.toJSON())); + }, + isEmpty: function() { + return !this.get('name') && this.get('chapters').isEmpty(); + }, + urlRoot: function() { return CMS.URL.TEXTBOOKS; }, + parse: function(response) { + var ret = $.extend(true, {}, response); + if ('tab_title' in ret && !('name' in ret)) { + ret.name = ret.tab_title; + delete ret.tab_title; + } + if ('url' in ret && !('chapters' in ret)) { + ret.chapters = {url: ret.url}; + delete ret.url; + } + _.each(ret.chapters, function(chapter, i) { + chapter.order = chapter.order || i + 1; + }); + return ret; + }, + toJSON: function() { + return { + tab_title: this.get('name'), + chapters: this.get('chapters').toJSON() + }; + }, // NOTE: validation functions should return non-internationalized error // messages. The messages will be passed through gettext in the template. - validate: function(attrs, options) { - if (!attrs.name) { - return { - message: gettext('Textbook name is required'), - attributes: {name: true} - }; - } - if (attrs.chapters.length === 0) { - return { - message: gettext('Please add at least one chapter'), - attributes: {chapters: true} - }; - } else { + validate: function(attrs, options) { + if (!attrs.name) { + return { + message: gettext('Textbook name is required'), + attributes: {name: true} + }; + } + if (attrs.chapters.length === 0) { + return { + message: gettext('Please add at least one chapter'), + attributes: {chapters: true} + }; + } else { // validate all chapters - var invalidChapters = []; - attrs.chapters.each(function(chapter) { - if (!chapter.isValid()) { - invalidChapters.push(chapter); - } - }); - if (!_.isEmpty(invalidChapters)) { - return { - message: gettext('All chapters must have a name and asset'), - attributes: {chapters: invalidChapters} - }; + var invalidChapters = []; + attrs.chapters.each(function(chapter) { + if (!chapter.isValid()) { + invalidChapters.push(chapter); } + }); + if (!_.isEmpty(invalidChapters)) { + return { + message: gettext('All chapters must have a name and asset'), + attributes: {chapters: invalidChapters} + }; } } - }); - return Textbook; + } }); + return Textbook; +}); diff --git a/cms/static/js/models/uploads.js b/cms/static/js/models/uploads.js index 8db4129d9e..58277c3eb8 100644 --- a/cms/static/js/models/uploads.js +++ b/cms/static/js/models/uploads.js @@ -15,26 +15,26 @@ define(['backbone', 'underscore', 'gettext'], function(Backbone, _, gettext) { if (attrs.selectedFile && !this.checkTypeValidity(attrs.selectedFile)) { return { message: _.template(gettext('Only <%- fileTypes %> files can be uploaded. Please select a file ending in <%- (fileExtensions) %> to upload.'))( // eslint-disable-line max-len - this.formatValidTypes() - ), + this.formatValidTypes() + ), attributes: {selectedFile: true} }; } }, - // Return a list of this uploader's valid file types + // Return a list of this uploader's valid file types fileTypes: function() { var mimeTypes = _.map( - this.attributes.mimeTypes, - function(type) { - return type.split('/')[1].toUpperCase(); - } - ), + this.attributes.mimeTypes, + function(type) { + return type.split('/')[1].toUpperCase(); + } + ), fileFormats = _.map( - this.attributes.fileFormats, - function(type) { - return type.toUpperCase(); - } - ); + this.attributes.fileFormats, + function(type) { + return type.toUpperCase(); + } + ); return mimeTypes.concat(fileFormats); }, @@ -49,8 +49,8 @@ define(['backbone', 'underscore', 'gettext'], function(Backbone, _, gettext) { _.contains(attrs.mimeTypes, file.type) || getRegExp(attrs.fileFormats).test(file.name); }, - // Return strings for the valid file types and extensions this - // uploader accepts, formatted as natural language + // Return strings for the valid file types and extensions this + // uploader accepts, formatted as natural language formatValidTypes: function() { var attrs = this.attributes; @@ -71,11 +71,11 @@ define(['backbone', 'underscore', 'gettext'], function(Backbone, _, gettext) { return { fileTypes: formatTypes(this.fileTypes()), fileExtensions: formatTypes( - _.map(this.fileTypes(), - function(type) { - return '.' + type.toLowerCase(); - }) - ) + _.map(this.fileTypes(), + function(type) { + return '.' + type.toLowerCase(); + }) + ) }; } }); diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 78eaaddea2..983edc4e56 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -1,270 +1,270 @@ define( ['backbone', 'underscore', 'underscore.string', 'js/utils/module'], -function(Backbone, _, str, ModuleUtils) { - 'use strict'; - var XBlockInfo = Backbone.Model.extend({ + function(Backbone, _, str, ModuleUtils) { + 'use strict'; + var XBlockInfo = Backbone.Model.extend({ - urlRoot: ModuleUtils.urlRoot, + urlRoot: ModuleUtils.urlRoot, - // NOTE: 'publish' is not an attribute on XBlockInfo, but it is used to signal the publish - // and discard changes actions. Therefore 'publish' cannot be introduced as an attribute. - defaults: { - id: null, - display_name: null, - category: null, - data: null, - metadata: null, - /** + // NOTE: 'publish' is not an attribute on XBlockInfo, but it is used to signal the publish + // and discard changes actions. Therefore 'publish' cannot be introduced as an attribute. + defaults: { + id: null, + display_name: null, + category: null, + data: null, + metadata: null, + /** * The Studio URL for this xblock, or null if it doesn't have one. */ - studio_url: null, - /** + studio_url: null, + /** * An optional object with information about the children as well as about * the primary xblock type that is supported as a child. */ - child_info: null, - /** + child_info: null, + /** * An optional object with information about each of the ancestors. */ - ancestor_info: null, - /** + ancestor_info: null, + /** * Date of the last edit to this xblock or any of its descendants. */ - edited_on: null, - /** + edited_on: null, + /** * User who last edited the xblock or any of its descendants. Will only be present if * publishing info was explicitly requested. */ - edited_by: null, - /** + edited_by: null, + /** * True iff a published version of the xblock exists. */ - published: null, - /** + published: null, + /** * Date of the last publish of this xblock, or null if never published. */ - published_on: null, - /** + published_on: null, + /** * User who last published the xblock, or null if never published. Will only be present if * publishing info was explicitly requested. */ - published_by: null, - /** + published_by: null, + /** * True if the xblock is a parentable xblock. */ - has_children: null, - /** + has_children: null, + /** * True if the xblock has changes. * Note: this is not always provided as a performance optimization. It is only provided for * verticals functioning as units. */ - has_changes: null, - /** + has_changes: null, + /** * Represents the possible publish states for an xblock. See the documentation * for XBlockVisibility to see a comprehensive enumeration of the states. */ - visibility_state: null, - /** + visibility_state: null, + /** * True if the release date of the xblock is in the past. */ - released_to_students: null, - /** + released_to_students: null, + /** * If the xblock is published, the date on which it will be released to students. * This can be null if the release date is unscheduled. */ - release_date: null, - /** + release_date: null, + /** * The xblock which is determining the release date. For instance, for a unit, * this will either be the parent subsection or the grandparent section. * This can be null if the release date is unscheduled. Will only be present if * publishing info was explicitly requested. */ - release_date_from: null, - /** + release_date_from: null, + /** * True if this xblock is currently visible to students. This is computed server-side * so that the logic isn't duplicated on the client. Will only be present if * publishing info was explicitly requested. */ - currently_visible_to_students: null, - /** + currently_visible_to_students: null, + /** * If xblock is graded, the date after which student assessment will be evaluated. * It has same format as release date, for example: 'Jan 02, 2015 at 00:00 UTC'. */ - due_date: null, - /** + due_date: null, + /** * Grading policy for xblock. */ - format: null, - /** + format: null, + /** * List of course graders names. */ - course_graders: null, - /** + course_graders: null, + /** * True if this xblock contributes to the final course grade. */ - graded: null, - /** + graded: null, + /** * The same as `release_date` but as an ISO-formatted date string. */ - start: null, - /** + start: null, + /** * The same as `due_date` but as an ISO-formatted date string. */ - due: null, - /** + due: null, + /** * True iff this xblock is explicitly staff locked. */ - has_explicit_staff_lock: null, - /** + has_explicit_staff_lock: null, + /** * True iff this any of this xblock's ancestors are staff locked. */ - ancestor_has_staff_lock: null, - /** + ancestor_has_staff_lock: null, + /** * The xblock which is determining the staff lock value. For instance, for a unit, * this will either be the parent subsection or the grandparent section. * This can be null if the xblock has no inherited staff lock. Will only be present if * publishing info was explicitly requested. */ - staff_lock_from: null, - /** + staff_lock_from: null, + /** * True iff this xblock should display a "Contains staff only content" message. */ - staff_only_message: null, - /** + staff_only_message: null, + /** * True iff this xblock is a unit, and it has children that are only visible to certain * user partition groups. Note that this is not a recursive property. Will only be present if * publishing info was explicitly requested. */ - has_partition_group_components: null, - /** + has_partition_group_components: null, + /** * actions defines the state of delete, drag and child add functionality for a xblock. * currently, each xblock has default value of 'True' for keys: deletable, draggable and childAddable. */ - actions: null, - /** + actions: null, + /** * Header visible to UI. */ - is_header_visible: null, - /** + is_header_visible: null, + /** * Optional explanatory message about the xblock. */ - explanatory_message: null, - /** + explanatory_message: null, + /** * The XBlock's group access rules. This is a dictionary keyed to user partition IDs, * where the values are lists of group IDs. */ - group_access: null, - /** + group_access: null, + /** * User partition dictionary. This is pre-processed by Studio, so it contains * some additional fields that are not stored in the course descriptor * (for example, which groups are selected for this particular XBlock). */ - user_partitions: null, - /** + user_partitions: null, + /** * This xBlock's Highlights to message to learners. */ - highlights: [], - highlights_enabled: false, - highlights_enabled_for_messaging: false, - highlights_preview_only: true, - highlights_doc_url: '' - }, + highlights: [], + highlights_enabled: false, + highlights_enabled_for_messaging: false, + highlights_preview_only: true, + highlights_doc_url: '' + }, - initialize: function() { + initialize: function() { // Extend our Model by helper methods. - _.extend(this, this.getCategoryHelpers()); - }, + _.extend(this, this.getCategoryHelpers()); + }, - parse: function(response) { - if (response.ancestor_info) { - response.ancestor_info.ancestors = this.parseXBlockInfoList(response.ancestor_info.ancestors); - } - if (response.child_info) { - response.child_info.children = this.parseXBlockInfoList(response.child_info.children); - } - return response; - }, + parse: function(response) { + if (response.ancestor_info) { + response.ancestor_info.ancestors = this.parseXBlockInfoList(response.ancestor_info.ancestors); + } + if (response.child_info) { + response.child_info.children = this.parseXBlockInfoList(response.child_info.children); + } + return response; + }, - parseXBlockInfoList: function(list) { - return _.map(list, function(item) { - return this.createChild(item); - }, this); - }, + parseXBlockInfoList: function(list) { + return _.map(list, function(item) { + return this.createChild(item); + }, this); + }, - createChild: function(response) { - return new XBlockInfo(response, {parse: true}); - }, + createChild: function(response) { + return new XBlockInfo(response, {parse: true}); + }, - hasChildren: function() { - var childInfo = this.get('child_info'); - return childInfo && childInfo.children.length > 0; - }, + hasChildren: function() { + var childInfo = this.get('child_info'); + return childInfo && childInfo.children.length > 0; + }, - isPublishable: function() { - return !this.get('published') || this.get('has_changes'); - }, + isPublishable: function() { + return !this.get('published') || this.get('has_changes'); + }, - isDeletable: function() { - return this.isActionRequired('deletable'); - }, + isDeletable: function() { + return this.isActionRequired('deletable'); + }, - isDuplicable: function() { - return this.isActionRequired('duplicable'); - }, + isDuplicable: function() { + return this.isActionRequired('duplicable'); + }, - isDraggable: function() { - return this.isActionRequired('draggable'); - }, + isDraggable: function() { + return this.isActionRequired('draggable'); + }, - isChildAddable: function() { - return this.isActionRequired('childAddable'); - }, + isChildAddable: function() { + return this.isActionRequired('childAddable'); + }, - isHeaderVisible: function() { - if (this.get('is_header_visible') !== null) { - return this.get('is_header_visible'); - } - return true; - }, + isHeaderVisible: function() { + if (this.get('is_header_visible') !== null) { + return this.get('is_header_visible'); + } + return true; + }, - /** + /** * Return true if action is required e.g. delete, drag, add new child etc or if given key is not present. * @return {boolean} */ - isActionRequired: function(actionName) { - var actions = this.get('actions'); - if (actions !== null) { - if (_.has(actions, actionName) && !actions[actionName]) { - return false; + isActionRequired: function(actionName) { + var actions = this.get('actions'); + if (actions !== null) { + if (_.has(actions, actionName) && !actions[actionName]) { + return false; + } } - } - return true; - }, + return true; + }, - /** + /** * Return a list of convenience methods to check affiliation to the category. * @return {Array} */ - getCategoryHelpers: function() { - var categories = ['course', 'chapter', 'sequential', 'vertical'], - helpers = {}; + getCategoryHelpers: function() { + var categories = ['course', 'chapter', 'sequential', 'vertical'], + helpers = {}; - _.each(categories, function(item) { - helpers['is' + str.titleize(item)] = function() { - return this.get('category') === item; - }; - }, this); + _.each(categories, function(item) { + helpers['is' + str.titleize(item)] = function() { + return this.get('category') === item; + }; + }, this); - return helpers; - }, + return helpers; + }, - /** + /** * Check if we can edit current XBlock or not on Course Outline page. * @return {Boolean} */ - isEditableOnCourseOutline: function() { - return this.isSequential() || this.isChapter() || this.isVertical(); - } + isEditableOnCourseOutline: function() { + return this.isSequential() || this.isChapter() || this.isVertical(); + } + }); + return XBlockInfo; }); - return XBlockInfo; -}); diff --git a/cms/static/js/spec/utils/drag_and_drop_spec.js b/cms/static/js/spec/utils/drag_and_drop_spec.js index 1c56f9a479..c68db7ed13 100644 --- a/cms/static/js/spec/utils/drag_and_drop_spec.js +++ b/cms/static/js/spec/utils/drag_and_drop_spec.js @@ -1,375 +1,375 @@ define(['sinon', 'js/utils/drag_and_drop', 'common/js/components/views/feedback_notification', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'jquery', 'underscore'], - function(sinon, ContentDragger, Notification, AjaxHelpers, $, _) { - 'use strict'; - describe('Overview drag and drop functionality', function() { +function(sinon, ContentDragger, Notification, AjaxHelpers, $, _) { + 'use strict'; + describe('Overview drag and drop functionality', function() { + beforeEach(function() { + setFixtures(readFixtures('mock/mock-outline.underscore')); + _.each( + $('.unit'), + function(element) { + ContentDragger.makeDraggable(element, { + type: '.unit', + handleClass: '.unit-drag-handle', + droppableClass: 'ol.sortable-unit-list', + parentLocationSelector: 'li.courseware-subsection', + refresh: jasmine.createSpy('Spy on Unit'), + ensureChildrenRendered: jasmine.createSpy('Spy on Unit') + }); + } + ); + _.each( + $('.courseware-subsection'), + function(element) { + ContentDragger.makeDraggable(element, { + type: '.courseware-subsection', + handleClass: '.subsection-drag-handle', + droppableClass: '.sortable-subsection-list', + parentLocationSelector: 'section', + refresh: jasmine.createSpy('Spy on Subsection'), + ensureChildrenRendered: jasmine.createSpy('Spy on Subsection') + }); + } + ); + }); + + describe('findDestination', function() { + it('correctly finds the drop target of a drag', function() { + var $ele, destination; + $ele = $('#unit-1'); + $ele.offset({ + top: $ele.offset().top + 10, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 1); + expect(destination.ele).toEqual($('#unit-2')); + expect(destination.attachMethod).toBe('before'); + }); + it('can drag and drop across section boundaries, with special handling for single sibling', function() { + var $ele, $unit0, $unit4, destination; + $ele = $('#unit-1'); + $unit4 = $('#unit-4'); + $ele.offset({ + top: $unit4.offset().top + 8, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 1); + expect(destination.ele).toEqual($unit4); + expect(destination.attachMethod).toBe('after'); + destination = ContentDragger.findDestination($ele, -1); + expect(destination.ele).toEqual($unit4); + expect(destination.attachMethod).toBe('before'); + $ele.offset({ + top: $unit4.offset().top + $unit4.height() + 1, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 0); + expect(destination.ele).toEqual($unit4); + expect(destination.attachMethod).toBe('after'); + $unit0 = $('#unit-0'); + $ele.offset({ + top: $unit0.offset().top - 16, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 0); + expect(destination.ele).toEqual($unit0); + expect(destination.attachMethod).toBe('before'); + }); + it('can drop before the first element, even if element being dragged is\nslightly before the first element', function() { + var $ele, destination; + $ele = $('#subsection-2'); + $ele.offset({ + top: $('#subsection-0').offset().top - 5, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, -1); + expect(destination.ele).toEqual($('#subsection-0')); + expect(destination.attachMethod).toBe('before'); + }); + it('can drag and drop across section boundaries, with special handling for last element', function() { + var $ele, destination; + $ele = $('#unit-4'); + $ele.offset({ + top: $('#unit-3').offset().bottom + 4, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, -1); + expect(destination.ele).toEqual($('#unit-3')); + expect(destination.attachMethod).toBe('after'); + $ele.offset({ + top: $('#unit-3').offset().top + 4, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, -1); + expect(destination.ele).toEqual($('#unit-3')); + expect(destination.attachMethod).toBe('before'); + }); + it('can drop past the last element, even if element being dragged is\nslightly before/taller then the last element', function() { + var $ele, destination; + $ele = $('#subsection-2'); + $ele.offset({ + top: $('#subsection-4').offset().top - 1, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 1); + expect(destination.ele).toEqual($('#subsection-4')); + expect(destination.attachMethod).toBe('after'); + }); + it('can drag into an empty list', function() { + var $ele, destination; + $ele = $('#unit-1'); + $ele.offset({ + top: $('#subsection-3').offset().top + 10, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 1); + expect(destination.ele).toEqual($('#subsection-list-3')); + expect(destination.attachMethod).toBe('prepend'); + }); + it('reports a null destination on a failed drag', function() { + var $ele, destination; + $ele = $('#unit-1'); + $ele.offset({ + top: $ele.offset().top + 200, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 1); + expect(destination).toEqual({ + ele: null, + attachMethod: '' + }); + }); + it('can drag into a collapsed list', function() { + var $ele, destination; + $('#subsection-2').addClass('is-collapsed'); + $ele = $('#unit-2'); + $ele.offset({ + top: $('#subsection-2').offset().top + 3, + left: $ele.offset().left + }); + destination = ContentDragger.findDestination($ele, 1); + expect(destination.ele).toEqual($('#subsection-list-2')); + expect(destination.parentList).toEqual($('#subsection-2')); + expect(destination.attachMethod).toBe('prepend'); + }); + }); + describe('onDragStart', function() { + it('sets the dragState to its default values', function() { + expect(ContentDragger.dragState).toEqual({}); + ContentDragger.onDragStart({ + element: $('#unit-1') + }, null, null); + expect(ContentDragger.dragState).toEqual({ + dropDestination: null, + attachMethod: '', + parentList: null, + lastY: 0, + dragDirection: 0 + }); + }); + it('collapses expanded elements', function() { + expect($('#subsection-1')).not.toHaveClass('is-collapsed'); + ContentDragger.onDragStart({ + element: $('#subsection-1') + }, null, null); + expect($('#subsection-1')).toHaveClass('is-collapsed'); + expect($('#subsection-1')).toHaveClass('expand-on-drop'); + }); + }); + describe('onDragMove', function() { beforeEach(function() { - setFixtures(readFixtures('mock/mock-outline.underscore')); - _.each( - $('.unit'), - function(element) { - ContentDragger.makeDraggable(element, { - type: '.unit', - handleClass: '.unit-drag-handle', - droppableClass: 'ol.sortable-unit-list', - parentLocationSelector: 'li.courseware-subsection', - refresh: jasmine.createSpy('Spy on Unit'), - ensureChildrenRendered: jasmine.createSpy('Spy on Unit') - }); + this.redirectSpy = spyOn(window, 'scrollBy').and.callThrough(); + }); + it('adds the correct CSS class to the drop destination', function() { + var $ele, dragX, dragY; + $ele = $('#unit-1'); + dragY = $ele.offset().top + 10; + dragX = $ele.offset().left; + $ele.offset({ + top: dragY, + left: dragX + }); + ContentDragger.onDragMove({ + element: $ele, + dragPoint: { + y: dragY } - ); - _.each( - $('.courseware-subsection'), - function(element) { - ContentDragger.makeDraggable(element, { - type: '.courseware-subsection', - handleClass: '.subsection-drag-handle', - droppableClass: '.sortable-subsection-list', - parentLocationSelector: 'section', - refresh: jasmine.createSpy('Spy on Subsection'), - ensureChildrenRendered: jasmine.createSpy('Spy on Subsection') - }); + }, '', { + clientX: dragX + }); + expect($('#unit-2')).toHaveClass('drop-target drop-target-before'); + expect($ele).toHaveClass('valid-drop'); + }); + it('does not add CSS class to the drop destination if out of bounds', function() { + var $ele, dragY; + $ele = $('#unit-1'); + dragY = $ele.offset().top + 10; + $ele.offset({ + top: dragY, + left: $ele.offset().left + }); + ContentDragger.onDragMove({ + element: $ele, + dragPoint: { + y: dragY } + }, '', { + clientX: $ele.offset().left - 3 + }); + expect($('#unit-2')).not.toHaveClass('drop-target drop-target-before'); + expect($ele).not.toHaveClass('valid-drop'); + }); + it('scrolls up if necessary', function() { + ContentDragger.onDragMove({ + element: $('#unit-1') + }, '', { + clientY: 2 + }); + expect(this.redirectSpy).toHaveBeenCalledWith(0, -10); + }); + it('scrolls down if necessary', function() { + ContentDragger.onDragMove({ + element: $('#unit-1') + }, '', { + clientY: window.innerHeight - 5 + }); + expect(this.redirectSpy).toHaveBeenCalledWith(0, 10); + }); + }); + describe('onDragEnd', function() { + beforeEach(function() { + this.reorderSpy = spyOn(ContentDragger, 'handleReorder'); + }); + afterEach(function() { + this.reorderSpy.calls.reset(); + }); + it('calls handleReorder on a successful drag', function() { + ContentDragger.dragState.dropDestination = $('#unit-2'); + ContentDragger.dragState.attachMethod = 'after'; + ContentDragger.dragState.parentList = $('#subsection-1'); + $('#unit-1').offset({ + top: $('#unit-1').offset().top + 10, + left: $('#unit-1').offset().left + }); + ContentDragger.onDragEnd({ + element: $('#unit-1') + }, null, { + clientX: $('#unit-1').offset().left + }); + expect(this.reorderSpy).toHaveBeenCalled(); + }); + it('clears out the drag state', function() { + ContentDragger.onDragEnd({ + element: $('#unit-1') + }, null, null); + expect(ContentDragger.dragState).toEqual({}); + }); + it('sets the element to the correct position', function() { + ContentDragger.onDragEnd({ + element: $('#unit-1') + }, null, null); + expect(['0px', 'auto']).toContain($('#unit-1').css('top')); + expect(['0px', 'auto']).toContain($('#unit-1').css('left')); + }); + it('expands an element if it was collapsed on drag start', function() { + $('#subsection-1').addClass('is-collapsed'); + $('#subsection-1').addClass('expand-on-drop'); + ContentDragger.onDragEnd({ + element: $('#subsection-1') + }, null, null); + expect($('#subsection-1')).not.toHaveClass('is-collapsed'); + expect($('#subsection-1')).not.toHaveClass('expand-on-drop'); + }); + it('expands a collapsed element when something is dropped in it', function() { + var expandElementSpy = spyOn(ContentDragger, 'expandElement').and.callThrough(); + expect(expandElementSpy).not.toHaveBeenCalled(); + expect($('#subsection-2').data('ensureChildrenRendered')).not.toHaveBeenCalled(); + + $('#subsection-2').addClass('is-collapsed'); + ContentDragger.dragState.dropDestination = $('#list-2'); + ContentDragger.dragState.attachMethod = 'prepend'; + ContentDragger.dragState.parentList = $('#subsection-2'); + ContentDragger.onDragEnd({ + element: $('#unit-1') + }, null, { + clientX: $('#unit-1').offset().left + }); + + // verify collapsed element expands while ensuring its children are properly rendered + expect(expandElementSpy).toHaveBeenCalled(); + expect($('#subsection-2').data('ensureChildrenRendered')).toHaveBeenCalled(); + expect($('#subsection-2')).not.toHaveClass('is-collapsed'); + }); + }); + describe('AJAX', function() { + beforeEach(function() { + this.savingSpies = jasmine.stealth.spyOnConstructor(Notification, 'Mini', ['show', 'hide']); + this.savingSpies.show.and.returnValue(this.savingSpies); + this.clock = sinon.useFakeTimers(); + }); + afterEach(function() { + this.clock.restore(); + jasmine.stealth.clearSpies(); + }); + it('should send an update on reorder from one parent to another', function() { + var requests, request, savingOptions; + requests = AjaxHelpers.requests(this); + ContentDragger.dragState.dropDestination = $('#unit-4'); + ContentDragger.dragState.attachMethod = 'after'; + ContentDragger.dragState.parentList = $('#subsection-2'); + $('#unit-1').offset({ + top: $('#unit-4').offset().top + 10, + left: $('#unit-4').offset().left + }); + ContentDragger.onDragEnd({ + element: $('#unit-1') + }, null, { + clientX: $('#unit-1').offset().left + }); + request = AjaxHelpers.currentRequest(requests); + expect(this.savingSpies.constructor).toHaveBeenCalled(); + expect(this.savingSpies.show).toHaveBeenCalled(); + expect(this.savingSpies.hide).not.toHaveBeenCalled(); + savingOptions = this.savingSpies.constructor.calls.mostRecent().args[0]; + expect(savingOptions.title).toMatch(/Saving/); + expect($('#unit-1')).toHaveClass('was-dropped'); + expect(request.requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}'); + request.respond(204); + expect(this.savingSpies.hide).toHaveBeenCalled(); + this.clock.tick(1001); + expect($('#unit-1')).not.toHaveClass('was-dropped'); + // source + expect($('#subsection-1').data('refresh')).toHaveBeenCalled(); + // target + expect($('#subsection-2').data('refresh')).toHaveBeenCalled(); + }); + it('should send an update on reorder within the same parent', function() { + var requests = AjaxHelpers.requests(this), + request; + ContentDragger.dragState.dropDestination = $('#unit-2'); + ContentDragger.dragState.attachMethod = 'after'; + ContentDragger.dragState.parentList = $('#subsection-1'); + $('#unit-1').offset({ + top: $('#unit-1').offset().top + 10, + left: $('#unit-1').offset().left + }); + ContentDragger.onDragEnd({ + element: $('#unit-1') + }, null, { + clientX: $('#unit-1').offset().left + }); + request = AjaxHelpers.currentRequest(requests); + expect($('#unit-1')).toHaveClass('was-dropped'); + expect(request.requestBody).toEqual( + '{"children":["second-unit-id","first-unit-id","third-unit-id"]}' ); - }); - - describe('findDestination', function() { - it('correctly finds the drop target of a drag', function() { - var $ele, destination; - $ele = $('#unit-1'); - $ele.offset({ - top: $ele.offset().top + 10, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 1); - expect(destination.ele).toEqual($('#unit-2')); - expect(destination.attachMethod).toBe('before'); - }); - it('can drag and drop across section boundaries, with special handling for single sibling', function() { - var $ele, $unit0, $unit4, destination; - $ele = $('#unit-1'); - $unit4 = $('#unit-4'); - $ele.offset({ - top: $unit4.offset().top + 8, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 1); - expect(destination.ele).toEqual($unit4); - expect(destination.attachMethod).toBe('after'); - destination = ContentDragger.findDestination($ele, -1); - expect(destination.ele).toEqual($unit4); - expect(destination.attachMethod).toBe('before'); - $ele.offset({ - top: $unit4.offset().top + $unit4.height() + 1, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 0); - expect(destination.ele).toEqual($unit4); - expect(destination.attachMethod).toBe('after'); - $unit0 = $('#unit-0'); - $ele.offset({ - top: $unit0.offset().top - 16, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 0); - expect(destination.ele).toEqual($unit0); - expect(destination.attachMethod).toBe('before'); - }); - it('can drop before the first element, even if element being dragged is\nslightly before the first element', function() { - var $ele, destination; - $ele = $('#subsection-2'); - $ele.offset({ - top: $('#subsection-0').offset().top - 5, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, -1); - expect(destination.ele).toEqual($('#subsection-0')); - expect(destination.attachMethod).toBe('before'); - }); - it('can drag and drop across section boundaries, with special handling for last element', function() { - var $ele, destination; - $ele = $('#unit-4'); - $ele.offset({ - top: $('#unit-3').offset().bottom + 4, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, -1); - expect(destination.ele).toEqual($('#unit-3')); - expect(destination.attachMethod).toBe('after'); - $ele.offset({ - top: $('#unit-3').offset().top + 4, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, -1); - expect(destination.ele).toEqual($('#unit-3')); - expect(destination.attachMethod).toBe('before'); - }); - it('can drop past the last element, even if element being dragged is\nslightly before/taller then the last element', function() { - var $ele, destination; - $ele = $('#subsection-2'); - $ele.offset({ - top: $('#subsection-4').offset().top - 1, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 1); - expect(destination.ele).toEqual($('#subsection-4')); - expect(destination.attachMethod).toBe('after'); - }); - it('can drag into an empty list', function() { - var $ele, destination; - $ele = $('#unit-1'); - $ele.offset({ - top: $('#subsection-3').offset().top + 10, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 1); - expect(destination.ele).toEqual($('#subsection-list-3')); - expect(destination.attachMethod).toBe('prepend'); - }); - it('reports a null destination on a failed drag', function() { - var $ele, destination; - $ele = $('#unit-1'); - $ele.offset({ - top: $ele.offset().top + 200, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 1); - expect(destination).toEqual({ - ele: null, - attachMethod: '' - }); - }); - it('can drag into a collapsed list', function() { - var $ele, destination; - $('#subsection-2').addClass('is-collapsed'); - $ele = $('#unit-2'); - $ele.offset({ - top: $('#subsection-2').offset().top + 3, - left: $ele.offset().left - }); - destination = ContentDragger.findDestination($ele, 1); - expect(destination.ele).toEqual($('#subsection-list-2')); - expect(destination.parentList).toEqual($('#subsection-2')); - expect(destination.attachMethod).toBe('prepend'); - }); - }); - describe('onDragStart', function() { - it('sets the dragState to its default values', function() { - expect(ContentDragger.dragState).toEqual({}); - ContentDragger.onDragStart({ - element: $('#unit-1') - }, null, null); - expect(ContentDragger.dragState).toEqual({ - dropDestination: null, - attachMethod: '', - parentList: null, - lastY: 0, - dragDirection: 0 - }); - }); - it('collapses expanded elements', function() { - expect($('#subsection-1')).not.toHaveClass('is-collapsed'); - ContentDragger.onDragStart({ - element: $('#subsection-1') - }, null, null); - expect($('#subsection-1')).toHaveClass('is-collapsed'); - expect($('#subsection-1')).toHaveClass('expand-on-drop'); - }); - }); - describe('onDragMove', function() { - beforeEach(function() { - this.redirectSpy = spyOn(window, 'scrollBy').and.callThrough(); - }); - it('adds the correct CSS class to the drop destination', function() { - var $ele, dragX, dragY; - $ele = $('#unit-1'); - dragY = $ele.offset().top + 10; - dragX = $ele.offset().left; - $ele.offset({ - top: dragY, - left: dragX - }); - ContentDragger.onDragMove({ - element: $ele, - dragPoint: { - y: dragY - } - }, '', { - clientX: dragX - }); - expect($('#unit-2')).toHaveClass('drop-target drop-target-before'); - expect($ele).toHaveClass('valid-drop'); - }); - it('does not add CSS class to the drop destination if out of bounds', function() { - var $ele, dragY; - $ele = $('#unit-1'); - dragY = $ele.offset().top + 10; - $ele.offset({ - top: dragY, - left: $ele.offset().left - }); - ContentDragger.onDragMove({ - element: $ele, - dragPoint: { - y: dragY - } - }, '', { - clientX: $ele.offset().left - 3 - }); - expect($('#unit-2')).not.toHaveClass('drop-target drop-target-before'); - expect($ele).not.toHaveClass('valid-drop'); - }); - it('scrolls up if necessary', function() { - ContentDragger.onDragMove({ - element: $('#unit-1') - }, '', { - clientY: 2 - }); - expect(this.redirectSpy).toHaveBeenCalledWith(0, -10); - }); - it('scrolls down if necessary', function() { - ContentDragger.onDragMove({ - element: $('#unit-1') - }, '', { - clientY: window.innerHeight - 5 - }); - expect(this.redirectSpy).toHaveBeenCalledWith(0, 10); - }); - }); - describe('onDragEnd', function() { - beforeEach(function() { - this.reorderSpy = spyOn(ContentDragger, 'handleReorder'); - }); - afterEach(function() { - this.reorderSpy.calls.reset(); - }); - it('calls handleReorder on a successful drag', function() { - ContentDragger.dragState.dropDestination = $('#unit-2'); - ContentDragger.dragState.attachMethod = 'after'; - ContentDragger.dragState.parentList = $('#subsection-1'); - $('#unit-1').offset({ - top: $('#unit-1').offset().top + 10, - left: $('#unit-1').offset().left - }); - ContentDragger.onDragEnd({ - element: $('#unit-1') - }, null, { - clientX: $('#unit-1').offset().left - }); - expect(this.reorderSpy).toHaveBeenCalled(); - }); - it('clears out the drag state', function() { - ContentDragger.onDragEnd({ - element: $('#unit-1') - }, null, null); - expect(ContentDragger.dragState).toEqual({}); - }); - it('sets the element to the correct position', function() { - ContentDragger.onDragEnd({ - element: $('#unit-1') - }, null, null); - expect(['0px', 'auto']).toContain($('#unit-1').css('top')); - expect(['0px', 'auto']).toContain($('#unit-1').css('left')); - }); - it('expands an element if it was collapsed on drag start', function() { - $('#subsection-1').addClass('is-collapsed'); - $('#subsection-1').addClass('expand-on-drop'); - ContentDragger.onDragEnd({ - element: $('#subsection-1') - }, null, null); - expect($('#subsection-1')).not.toHaveClass('is-collapsed'); - expect($('#subsection-1')).not.toHaveClass('expand-on-drop'); - }); - it('expands a collapsed element when something is dropped in it', function() { - var expandElementSpy = spyOn(ContentDragger, 'expandElement').and.callThrough(); - expect(expandElementSpy).not.toHaveBeenCalled(); - expect($('#subsection-2').data('ensureChildrenRendered')).not.toHaveBeenCalled(); - - $('#subsection-2').addClass('is-collapsed'); - ContentDragger.dragState.dropDestination = $('#list-2'); - ContentDragger.dragState.attachMethod = 'prepend'; - ContentDragger.dragState.parentList = $('#subsection-2'); - ContentDragger.onDragEnd({ - element: $('#unit-1') - }, null, { - clientX: $('#unit-1').offset().left - }); - - // verify collapsed element expands while ensuring its children are properly rendered - expect(expandElementSpy).toHaveBeenCalled(); - expect($('#subsection-2').data('ensureChildrenRendered')).toHaveBeenCalled(); - expect($('#subsection-2')).not.toHaveClass('is-collapsed'); - }); - }); - describe('AJAX', function() { - beforeEach(function() { - this.savingSpies = jasmine.stealth.spyOnConstructor(Notification, 'Mini', ['show', 'hide']); - this.savingSpies.show.and.returnValue(this.savingSpies); - this.clock = sinon.useFakeTimers(); - }); - afterEach(function() { - this.clock.restore(); - jasmine.stealth.clearSpies(); - }); - it('should send an update on reorder from one parent to another', function() { - var requests, request, savingOptions; - requests = AjaxHelpers.requests(this); - ContentDragger.dragState.dropDestination = $('#unit-4'); - ContentDragger.dragState.attachMethod = 'after'; - ContentDragger.dragState.parentList = $('#subsection-2'); - $('#unit-1').offset({ - top: $('#unit-4').offset().top + 10, - left: $('#unit-4').offset().left - }); - ContentDragger.onDragEnd({ - element: $('#unit-1') - }, null, { - clientX: $('#unit-1').offset().left - }); - request = AjaxHelpers.currentRequest(requests); - expect(this.savingSpies.constructor).toHaveBeenCalled(); - expect(this.savingSpies.show).toHaveBeenCalled(); - expect(this.savingSpies.hide).not.toHaveBeenCalled(); - savingOptions = this.savingSpies.constructor.calls.mostRecent().args[0]; - expect(savingOptions.title).toMatch(/Saving/); - expect($('#unit-1')).toHaveClass('was-dropped'); - expect(request.requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}'); - request.respond(204); - expect(this.savingSpies.hide).toHaveBeenCalled(); - this.clock.tick(1001); - expect($('#unit-1')).not.toHaveClass('was-dropped'); - // source - expect($('#subsection-1').data('refresh')).toHaveBeenCalled(); - // target - expect($('#subsection-2').data('refresh')).toHaveBeenCalled(); - }); - it('should send an update on reorder within the same parent', function() { - var requests = AjaxHelpers.requests(this), - request; - ContentDragger.dragState.dropDestination = $('#unit-2'); - ContentDragger.dragState.attachMethod = 'after'; - ContentDragger.dragState.parentList = $('#subsection-1'); - $('#unit-1').offset({ - top: $('#unit-1').offset().top + 10, - left: $('#unit-1').offset().left - }); - ContentDragger.onDragEnd({ - element: $('#unit-1') - }, null, { - clientX: $('#unit-1').offset().left - }); - request = AjaxHelpers.currentRequest(requests); - expect($('#unit-1')).toHaveClass('was-dropped'); - expect(request.requestBody).toEqual( - '{"children":["second-unit-id","first-unit-id","third-unit-id"]}' - ); - request.respond(204); - this.clock.tick(1001); - expect($('#unit-1')).not.toHaveClass('was-dropped'); - // parent - expect($('#subsection-1').data('refresh')).toHaveBeenCalled(); - }); + request.respond(204); + this.clock.tick(1001); + expect($('#unit-1')).not.toHaveClass('was-dropped'); + // parent + expect($('#subsection-1').data('refresh')).toHaveBeenCalled(); }); }); }); +}); diff --git a/cms/static/js/spec/utils/handle_iframe_binding_spec.js b/cms/static/js/spec/utils/handle_iframe_binding_spec.js index e3f75fff20..c373544e35 100644 --- a/cms/static/js/spec/utils/handle_iframe_binding_spec.js +++ b/cms/static/js/spec/utils/handle_iframe_binding_spec.js @@ -3,52 +3,52 @@ define( 'jquery', 'underscore', 'js/utils/handle_iframe_binding' ], -function($, _, IframeBinding) { - describe('IframeBinding', function() { - var doc = document.implementation.createHTMLDocument('New Document'); - var iframe_html = ''; - iframe_html += ''; - iframe_html += '