Skip to content
Snippets Groups Projects
Commit 1d768cde authored by Simon Chen's avatar Simon Chen Committed by GitHub
Browse files

ECOM-4904 Move the program editor backbone app to Studio (#12962)

parent 7745e7cd
No related branches found
Tags release-2020-01-27-15.46
No related merge requests found
Showing
with 555 additions and 8 deletions
......@@ -28,12 +28,11 @@ class ProgramAuthoringView(View):
if programs_config.is_studio_tab_enabled and request.user.is_staff:
return render_to_response('program_authoring.html', {
'show_programs_header': programs_config.is_studio_tab_enabled,
'authoring_app_config': programs_config.authoring_app_config,
'lms_base_url': '//{}/'.format(settings.LMS_BASE),
'programs_api_url': programs_config.public_api_url,
'programs_token_url': reverse('programs_id_token'),
'studio_home_url': reverse('home'),
'uses_pattern_library': True
})
else:
raise Http404
......
......@@ -56,6 +56,7 @@
'underscore.string': 'common/js/vendor/underscore.string',
'backbone': 'common/js/vendor/backbone',
'backbone-relational': 'js/vendor/backbone-relational.min',
'backbone.validation': 'common/js/vendor/backbone-validation-min',
'backbone.associations': 'js/vendor/backbone-associations-min',
'backbone.paginator': 'common/js/vendor/backbone.paginator',
'tinymce': 'js/vendor/tinymce/js/tinymce/tinymce.full.min',
......
......@@ -38,6 +38,7 @@
'backbone': 'common/js/vendor/backbone',
'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
'backbone.paginator': 'common/js/vendor/backbone.paginator',
'backbone.validation': 'common/js/vendor/backbone-validation-min',
'backbone-relational': 'xmodule_js/common_static/js/vendor/backbone-relational.min',
'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min',
'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce',
......@@ -267,7 +268,10 @@
'js/certificates/spec/views/certificate_details_spec',
'js/certificates/spec/views/certificate_editor_spec',
'js/certificates/spec/views/certificates_list_spec',
'js/certificates/spec/views/certificate_preview_spec'
'js/certificates/spec/views/certificate_preview_spec',
'js/spec/models/auto_auth_model_spec',
'js/spec/views/programs/program_creator_spec',
'js/spec/views/programs/program_details_spec'
];
i = 0;
......
......@@ -34,6 +34,7 @@
'backbone': 'common/js/vendor/backbone',
'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
'backbone.paginator': 'common/js/vendor/backbone.paginator',
'backbone.validation': 'common/js/vendor/backbone-validation',
'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min',
'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce',
'xmodule': 'xmodule_js/src/xmodule',
......
require(["domReady", "jquery", "underscore", "gettext", "common/js/components/views/feedback_notification",
"common/js/components/views/feedback_prompt", "js/utils/date_utils",
"js/utils/module", "js/utils/handle_iframe_binding", "jquery.ui", "jquery.leanModal",
"jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils, IframeUtils)
require([
"domReady",
"jquery",
"underscore",
"gettext",
"common/js/components/views/feedback_notification",
"common/js/components/views/feedback_prompt",
"js/utils/date_utils",
"js/utils/module",
"js/utils/handle_iframe_binding",
"edx-ui-toolkit/js/dropdown-menu/dropdown-menu-view",
"jquery.ui",
"jquery.leanModal",
"jquery.form",
"jquery.smoothScroll"
],
function(
domReady,
$,
_,
gettext,
NotificationView,
PromptView,
DateUtils,
ModuleUtils,
IframeUtils,
DropdownMenuView
)
{
var $body;
domReady(function() {
var dropdownMenuView;
$body = $('body');
$body.on('click', '.embeddable-xml-input', function() {
......@@ -67,6 +92,14 @@ domReady(function() {
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();
}
});
function smoothScrollLink(e) {
......
define([
'backbone',
'js/programs/utils/auth_utils'
],
function( Backbone, auth ) {
'use strict';
return Backbone.Collection.extend(auth.autoSync);
}
);
define([
'backbone',
'jquery',
'js/programs/utils/api_config',
'js/programs/collections/auto_auth_collection',
'jquery.cookie'
],
function( Backbone, $, apiConfig, AutoAuthCollection ) {
'use strict';
return AutoAuthCollection.extend({
allRuns: [],
initialize: function(models, options) {
// Ignore pagination and give me everything
var orgStr = options.organization.key,
queries = '?org=' + orgStr + '&username=' + apiConfig.get('username') + '&page_size=1000';
this.url = apiConfig.get('lmsBaseUrl') + 'api/courses/v1/courses/' + queries;
},
/*
* Abridged version of Backbone.Collection.Create that does not
* save the updated Collection back to the server
* (code based on original function - http://backbonejs.org/docs/backbone.html#section-134)
*/
create: function(model, options) {
options = options ? _.clone(options) : {};
model = this._prepareModel(model, options);
if (!!model) {
this.add(model, options);
return model;
}
},
parse: function(data) {
this.allRuns = data.results;
// Because pagination is ignored just set results
return data.results;
},
// Adds a run back into the model for selection
addRun: function(id) {
var courseRun = _.findWhere( this.allRuns, { id: id });
this.create(courseRun);
},
// Removes a run from the model for selection
removeRun: function(id) {
var courseRun = this.where({id: id});
this.remove(courseRun);
}
});
}
);
define([
'backbone',
'jquery',
'js/programs/models/program_model'
],
function( Backbone, $, ProgramModel ) {
'use strict';
return Backbone.Collection.extend({
model: ProgramModel
});
}
);
define([
'backbone'
],
function( Backbone ) {
'use strict';
return Backbone.Model.extend({
defaults: {
username: '',
lmsBaseUrl: '',
programsApiUrl: '',
authUrl: '/programs/id_token/',
idToken: ''
}
});
}
);
define([
'backbone',
'js/programs/utils/auth_utils'
],
function( Backbone, auth ) {
'use strict';
return Backbone.Model.extend(auth.autoSync);
}
);
define([
'backbone',
'jquery',
'js/programs/utils/api_config',
'js/programs/models/auto_auth_model',
'jquery.cookie',
'gettext'
],
function( Backbone, $, apiConfig, AutoAuthModel ) {
'use strict';
return AutoAuthModel.extend({
validation: {
key: {
required: true,
maxLength: 64
},
display_name: {
required: true,
maxLength: 128
}
},
labels: {
key: gettext('Course Code'),
display_name: gettext('Course Title')
},
defaults: {
display_name: false,
key: false,
organization: [],
run_modes: []
}
});
}
);
define([
'backbone'
],
function( Backbone ) {
'use strict';
return Backbone.Model.extend({
defaults: {
course_key: '',
mode_slug: 'verified',
sku: '',
start_date: '',
run_key: ''
}
});
}
);
define([
'js/programs/utils/api_config',
'js/programs/models/auto_auth_model'
],
function( apiConfig, AutoAuthModel ) {
'use strict';
return AutoAuthModel.extend({
url: function() {
return apiConfig.get('programsApiUrl') + 'organizations/?page_size=1000';
}
});
}
);
define([
'backbone',
'jquery',
'js/programs/utils/api_config',
'js/programs/models/auto_auth_model',
'jquery.cookie'
],
function( Backbone, $, apiConfig, AutoAuthModel ) {
'use strict';
return AutoAuthModel.extend({
// Backbone.Validation rules.
// See: http://thedersen.com/projects/backbone-validation/#configure-validation-rules-on-the-model.
validation: {
name: {
required: true,
maxLength: 255
},
subtitle: {
// The underlying Django model does not require a subtitle.
maxLength: 255
},
category: {
required: true,
// XSeries is currently the only valid Program type.
oneOf: ['xseries']
},
organizations: 'validateOrganizations',
marketing_slug: {
maxLength: 255
}
},
initialize: function() {
this.url = apiConfig.get('programsApiUrl') + 'programs/' + this.id + '/';
},
validateOrganizations: function( orgArray ) {
/**
* The array passed to this method contains a single object representing
* the selected organization; the object contains the organization's key.
* In the future, multiple organizations might be associated with a program.
*/
var i,
len = orgArray ? orgArray.length : 0;
for ( i = 0; i < len; i++ ) {
if ( orgArray[i].key === 'false' ) {
return gettext('Please select a valid organization.');
}
}
},
getConfig: function( options ) {
var patch = options && options.patch,
params = patch ? this.get('id') + '/' : '',
config = _.extend({ validate: true, parse: true }, {
type: patch ? 'PATCH' : 'POST',
url: apiConfig.get('programsApiUrl') + 'programs/' + params,
contentType: patch ? 'application/merge-patch+json' : 'application/json',
context: this,
// NB: setting context fails in tests
success: _.bind( this.saveSuccess, this ),
error: _.bind( this.saveError, this )
});
if ( patch ) {
config.data = JSON.stringify( options.update ) || this.attributes;
}
return config;
},
patch: function( data ) {
this.save({
patch: true,
update: data
});
},
save: function( options ) {
var method,
patch = options && options.patch ? true : false,
config = this.getConfig( options );
/**
* Simplified version of code from the default Backbone save function
* http://backbonejs.org/docs/backbone.html#section-87
*/
method = this.isNew() ? 'create' : ( patch ? 'patch' : 'update' );
this.sync( method, this, config );
},
saveError: function( jqXHR ) {
this.trigger( 'error', jqXHR );
},
saveSuccess: function( data ) {
this.set({ id: data.id });
this.trigger( 'sync', this );
}
});
}
);
(function() {
'use strict';
require([
'js/programs/views/program_admin_app_view'
],
function( ProgramAdminAppView ) {
return new ProgramAdminAppView();
}
);
})();
define([
'backbone',
'js/programs/views/program_creator_view',
'js/programs/views/program_details_view',
'js/programs/models/program_model'
],
function( Backbone, ProgramCreatorView, ProgramDetailsView, ProgramModel ) {
'use strict';
return Backbone.Router.extend({
root: '/program/',
routes: {
'new': 'programCreator',
':id': 'programDetails'
},
initialize: function( options ) {
this.homeUrl = options.homeUrl;
},
goHome: function() {
window.location.href = this.homeUrl;
},
loadProgramDetails: function() {
this.programDetailsView = new ProgramDetailsView({
model: this.programModel
});
},
programCreator: function() {
if ( this.programCreatorView ) {
this.programCreatorView.destroy();
}
this.programCreatorView = new ProgramCreatorView({
router: this
});
},
programDetails: function( id ) {
this.programModel = new ProgramModel({
id: id
});
this.programModel.on( 'sync', this.loadProgramDetails, this );
this.programModel.fetch();
},
/**
* Starts the router.
*/
start: function () {
if ( !Backbone.history.started ) {
Backbone.history.start({
pushState: true,
root: this.root
});
}
return this;
}
});
}
);
/**
* the Programs application loads gettext identity library via django, thus
* components reference gettext globally so a shim is added here to reflect
* the text so tests can be run if modules reference gettext
*/
(function() {
'use strict';
if ( !window.gettext ) {
window.gettext = function (text) {
return text;
};
}
if ( !window.interpolate ) {
window.interpolate = function (text) {
return text;
};
}
return window;
})();
define([
'js/programs/models/api_config_model'
],
function( ApiConfigModel ) {
'use strict';
/**
* This js module implements the Singleton pattern for an instance
* of the ApiConfigModel Backbone model. It returns the same shared
* instance of that model anywhere it is required.
*/
var instance;
if (instance === undefined) {
instance = new ApiConfigModel();
}
return instance;
}
);
define([
'jquery',
'underscore',
'js/programs/utils/api_config'
],
function( $, _, apiConfig ) {
'use strict';
var auth = {
autoSync: {
/**
* Override Backbone.sync to seamlessly attempt (re-)authentication when necessary.
*
* If a 401 error response is encountered while making a request to the Programs,
* API, this wrapper will attempt to request an id token from a custom endpoint
* via AJAX. Then the original request will be retried once more.
*
* Any other response than 401 on the original API request, or any error occurring
* on the retried API request (including 401), will be handled by the base sync
* implementation.
*
*/
sync: function( method, model, options ) {
var oldError = options.error;
this._setHeaders( options );
options.notifyOnError = false; // suppress Studio error pop-up that will happen if we get a 401
options.error = function(xhr, textStatus, errorThrown) {
if (xhr && xhr.status === 401) {
// attempt auth and retry
this._updateToken(function() {
// restore the original error handler
options.error = oldError;
options.notifyOnError = true; // if it fails again, let Studio notify.
delete options.xhr; // remove the failed (401) xhr from the last try.
// update authorization header
this._setHeaders( options );
Backbone.sync.call(this, method, model, options);
}.bind(this));
} else if (oldError) {
// fall back to the original error handler
oldError.call(this, xhr, textStatus, errorThrown);
}
}.bind(this);
return Backbone.sync.call(this, method, model, options);
},
/**
* Fix up headers on an imminent AJAX sync, ensuring that the JWT token is enclosed
* and that credentials are included when the request is being made cross-domain.
*/
_setHeaders: function( ajaxOptions ) {
ajaxOptions.headers = _.extend ( ajaxOptions.headers || {}, {
Authorization: 'JWT ' + apiConfig.get( 'idToken' )
});
ajaxOptions.xhrFields = _.extend( ajaxOptions.xhrFields || {}, {
withCredentials: true
});
},
/**
* Fetch a new id token from the configured endpoint, update the api config,
* and invoke the specified callback.
*/
_updateToken: function( success ) {
$.ajax({
url: apiConfig.get('authUrl'),
xhrFields: {
// See: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
withCredentials: true
},
crossDomain: true
}).done(function ( data ) {
// save the newly-retrieved id token
apiConfig.set( 'idToken', data.id_token );
}).done( success );
}
}
};
return auth;
}
);
/**
* Reusable constants
*/
define([], function() {
'use strict';
return {
keyCodes: {
tab: 9,
enter: 13,
esc: 27,
up: 38,
down: 40
}
};
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment