diff --git a/cms/static/js/models/textbooks.js b/cms/static/js/models/textbooks.js
new file mode 100644
index 0000000000..baca1a5b3d
--- /dev/null
+++ b/cms/static/js/models/textbooks.js
@@ -0,0 +1,101 @@
+CMS.Models.Textbook = Backbone.AssociatedModel.extend({
+ defaults: function() {
+ return {
+ name: "",
+ chapters: new CMS.Collections.ChapterSet([{}]),
+ showChapters: false
+ };
+ },
+ relations: [{
+ type: Backbone.Many,
+ key: "chapters",
+ relatedModel: "CMS.Models.Chapter",
+ collectionType: "CMS.Collections.ChapterSet"
+ }],
+ isEmpty: function() {
+ return !this.get('name') && this.get('chapters').isEmpty();
+ },
+ parse: function(response) {
+ if("tab_title" in response && !("name" in response)) {
+ response.name = response.tab_title;
+ delete response.tab_title;
+ }
+ if("url" in response && !("chapters" in response)) {
+ response.chapters = {"url": response.url};
+ delete response.url;
+ }
+ _.each(response.chapters, function(chapter, i) {
+ chapter.order = chapter.order || i+1;
+ });
+ return response;
+ },
+ toJSON: function() {
+ return {
+ tab_title: this.get('name'),
+ chapters: this.get('chapters').toJSON()
+ };
+ }
+});
+CMS.Collections.TextbookSet = Backbone.Collection.extend({
+ model: CMS.Models.Textbook,
+ url: function() { return window.TEXTBOOK_URL; },
+ initialize: function() {
+ this.listenTo(this, "editOne", this.editOne);
+ },
+ editOne: function(textbook) {
+ this.editing = textbook;
+ },
+ save: function(options) {
+ return this.sync('update', this, options);
+ }
+});
+CMS.Models.Chapter = Backbone.AssociatedModel.extend({
+ defaults: function() {
+ return {
+ name: "",
+ asset_path: "",
+ order: this.collection ? this.collection.nextOrder() : 1
+ };
+ },
+ isEmpty: function() {
+ return !this.get('name') && !this.get('asset_path');
+ },
+ parse: function(response) {
+ if("title" in response && !("name" in response)) {
+ response.name = response.title;
+ delete response.title;
+ }
+ if("url" in response && !("asset_path" in response)) {
+ response.asset_path = response.url;
+ delete response.url;
+ }
+ return response;
+ },
+ toJSON: function() {
+ return {
+ title: this.get('name'),
+ url: this.get('asset_path')
+ };
+ }
+});
+CMS.Collections.ChapterSet = Backbone.Collection.extend({
+ model: CMS.Models.Chapter,
+ comparator: "order",
+ nextOrder: function() {
+ if(!this.length) return 1;
+ return this.last().get('order') + 1;
+ },
+ isEmpty: function() {
+ return this.length === 0 || this.every(function(m) { return m.isEmpty(); });
+ }
+});
+CMS.Models.FileUpload = Backbone.Model.extend({
+ defaults: {
+ "title": "",
+ "message": "",
+ "selectFile": null,
+ "uploading": false,
+ "uploadedBytes": 0,
+ "totalBytes": 0
+ }
+});
diff --git a/cms/static/js/views/textbooks.js b/cms/static/js/views/textbooks.js
new file mode 100644
index 0000000000..f9fad1a6a5
--- /dev/null
+++ b/cms/static/js/views/textbooks.js
@@ -0,0 +1,333 @@
+CMS.Views.TextbookShow = Backbone.View.extend({
+ initialize: function() {
+ this.template = _.template($("#show-textbook-tpl").text());
+ this.listenTo(this.model, "change", this.render);
+ },
+ tagName: "li",
+ events: {
+ "click .edit": "editTextbook",
+ "click .delete": "confirmDelete",
+ "click .show-chapters": "showChapters",
+ "click .hide-chapters": "hideChapters"
+ },
+ render: function() {
+ this.$el.html(this.template(this.model.attributes));
+ return this;
+ },
+ editTextbook: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.model.collection.trigger("editOne", this.model);
+ },
+ confirmDelete: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ var textbook = this.model, collection = this.model.collection;
+ var msg = new CMS.Models.WarningMessage({
+ title: _.str.sprintf(gettext("Delete ā%sā?"),
+ textbook.escape('name')),
+ message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."),
+ actions: {
+ primary: {
+ text: gettext("Delete"),
+ click: function(view) {
+ view.hide();
+ collection.remove(textbook);
+ var delmsg = new CMS.Models.SystemFeedback({
+ intent: "saving",
+ title: gettext("Deleting…")
+ });
+ var notif = new CMS.Views.Notification({
+ model: delmsg,
+ closeIcon: false,
+ minShown: 1250
+ });
+ collection.save({
+ complete: function() {
+ notif.hide();
+ }
+ });
+ }
+ },
+ secondary: [{
+ text: gettext("Cancel"),
+ click: function(view) {
+ view.hide();
+ }
+ }]
+ }
+ });
+ var prompt = new CMS.Views.Prompt({model: msg});
+ },
+ showChapters: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.model.set('showChapters', true);
+ },
+ hideChapters: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.model.set('showChapters', false);
+ }
+});
+CMS.Views.TextbookEdit = Backbone.View.extend({
+ initialize: function() {
+ this.template = _.template($("#new-textbook-tpl").text());
+ var chapters = this.model.get('chapters');
+ this.listenTo(chapters, "add", this.addOne);
+ this.listenTo(chapters, "reset", this.addAll);
+ this.listenTo(chapters, "all", this.render);
+ this.listenTo(this.model.collection, "editOne", this.remove);
+ },
+ tagName: "li",
+ render: function() {
+ this.$el.html(this.template({
+ name: this.model.escape('name'),
+ errors: null
+ }));
+ this.addAll();
+ return this;
+ },
+ events: {
+ "change input[name=textbook-name]": "setName",
+ "submit": "setAndClose",
+ "click .action-cancel": "cancel",
+ "click .action-add-chapter": "createChapter"
+ },
+ addOne: function(chapter) {
+ var view = new CMS.Views.ChapterEdit({model: chapter});
+ this.$("ol.chapters").append(view.render().el)
+ return this;
+ },
+ addAll: function() {
+ this.model.get('chapters').each(this.addOne, this);
+ },
+ createChapter: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.setValues();
+ this.model.get('chapters').add([{}]);
+ },
+ setName: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.model.set("name", this.$("#textbook-name-input").val(), {silent: true});
+ },
+ setValues: function() {
+ this.setName();
+ var that = this;
+ _.each(this.$("li"), function(li, i) {
+ var chapter = that.model.get('chapters').at(i);
+ chapter.set({
+ "name": $(".chapter-name", li).val(),
+ "asset_path": $(".chapter-asset-path", li).val()
+ })
+ });
+ return this;
+ },
+ setAndClose: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.setValues();
+ msg = new CMS.Models.SystemFeedback({
+ intent: "saving",
+ title: gettext("Saving…")
+ });
+ notif = new CMS.Views.Notification({
+ model: msg,
+ closeIcon: false,
+ minShown: 1250
+ });
+ var that = this;
+ this.model.collection.save({
+ success: function() {
+ that.close();
+ },
+ complete: function() {
+ notif.hide();
+ }
+ });
+ },
+ cancel: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ return this.close();
+ },
+ close: function() {
+ var textbooks = this.model.collection;
+ delete textbooks.editing;
+ this.remove();
+ // if the textbook has no content, remove it from the collection
+ if(this.model.isEmpty()) {
+ textbooks.remove(this.model);
+ }
+ textbooks.trigger('render');
+ return this;
+ }
+});
+CMS.Views.ListTextbooks = Backbone.View.extend({
+ initialize: function() {
+ this.emptyTemplate = _.template($("#no-textbooks-tpl").text());
+ this.listenTo(this.collection, 'all', this.render);
+ },
+ tagName: "ul",
+ className: "textbooks",
+ render: function() {
+ var textbooks = this.collection;
+ if(textbooks.length === 0) {
+ this.$el.html(this.emptyTemplate());
+ } else {
+ var $el = this.$el;
+ $el.empty();
+ textbooks.each(function(textbook) {
+ var view;
+ if (textbook === textbooks.editing) {
+ view = new CMS.Views.TextbookEdit({model: textbook});
+ } else {
+ view = new CMS.Views.TextbookShow({model: textbook});
+ }
+ $el.append(view.render().el);
+ });
+ }
+ return this;
+ },
+ events: {
+ "click .new-button": "addOne"
+ },
+ addOne: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ // if the existing edited textbook is empty, don't do anything
+ if(this.collection.editing && this.collection.editing.isEmpty()) { return; }
+ var m = new this.collection.model();
+ this.collection.add(m);
+ this.collection.trigger("editOne", m);
+ }
+});
+CMS.Views.ChapterEdit = Backbone.View.extend({
+ initialize: function() {
+ this.template = _.template($("#new-chapter-tpl").text());
+ this.listenTo(this.model, "change", this.render);
+ },
+ tagName: "li",
+ className: function() {
+ return "field-group chapter chapter" + this.model.get('order');
+ },
+ render: function() {
+ this.$el.html(this.template({
+ name: this.model.escape('name'),
+ asset_path: this.model.escape('asset_path'),
+ order: this.model.get('order')
+ }));
+ return this;
+ },
+ events: {
+ "click .action-close": "removeChapter",
+ "click .action-upload": "openUploadDialog",
+ "submit": "uploadAsset"
+ },
+ removeChapter: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.model.collection.remove(this.model);
+ return this.remove();
+ },
+ openUploadDialog: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ var msg = new CMS.Models.FileUpload({
+ title: _.str.sprintf(gettext("Upload a new asset to %s"),
+ section.escape('name')),
+ message: "This is sample text about asset upload requirements, like no bigger than 2MB, must be in PDF format or whatever."
+ });
+ var view = new CMS.Views.UploadDialog({model: msg, chapter: this.model});
+ $(".wrapper-view").after(view.show().el);
+ }
+});
+
+CMS.Views.UploadDialog = Backbone.View.extend({
+ options: {
+ shown: true
+ },
+ initialize: function() {
+ this.template = _.template($("#upload-dialog-tpl").text());
+ this.listenTo(this.model, "change", this.render);
+ },
+ render: function() {
+ // some browsers (like Chrome) allow you to assign to the .files attribute
+ // of an DOM element -- for those browsers, we can
+ // create a new DOM element and assign the old content to it. Other browsers
+ // (like Firefox) make this attribute read-only, and we have to save the
+ // old DOM element in order to save it's content. For compatibility purposes,
+ // we'll just save the old element every time.
+ var oldInput = this.$("input[type=file]").get(0), selectedFile;
+ if (oldInput && oldInput.files.length) {
+ selectedFile = oldInput.files[0];
+ }
+ this.$el.html(this.template({
+ shown: this.options.shown,
+ url: UPLOAD_ASSET_CALLBACK_URL,
+ title: this.model.escape('title'),
+ message: this.model.escape('message'),
+ selectedFile: selectedFile,
+ uploading: this.model.get('uploading'),
+ uploadedBytes: this.model.get('uploadedBytes'),
+ totalBytes: this.model.get('totalBytes')
+ }));
+ if (oldInput) {
+ this.$('input[type=file]').replaceWith(oldInput);
+ }
+
+ return this;
+ },
+ events: {
+ "change input[type=file]": "selectFile",
+ "click .action-cancel": "hideAndRemove",
+ "click .action-upload": "upload"
+ },
+ selectFile: function(e) {
+ this.model.set('fileList', e.target.files);
+ },
+ show: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.options.shown = true;
+ $body.addClass('dialog-is-shown');
+ return this.render();
+ },
+ hide: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ this.options.shown = false;
+ $body.removeClass('dialog-is-shown');
+ return this.render();
+ },
+ hideAndRemove: function(e) {
+ if(e && e.preventDefault) { e.preventDefault(); }
+ return this.hide().remove();
+ },
+ upload: function(e) {
+ this.model.set('uploading', true);
+ this.$("form").ajaxSubmit({
+ success: _.bind(this.success, this),
+ error: _.bind(this.error, this),
+ uploadProgress: _.bind(this.progress, this),
+ data: {
+ notifyOnError: false
+ }
+ });
+ },
+ progress: function(event, position, total, percentComplete) {
+ this.model.set({
+ "uploadedBytes": position,
+ "totalBytes": total
+ });
+ },
+ success: function(response, statusText, xhr, form) {
+ this.model.set('uploading', false);
+ var chapter = this.options.chapter;
+ if(chapter) {
+ var options = {};
+ if(!chapter.get("name")) {
+ options.name = response.displayname;
+ }
+ options.asset_path = response.url;
+ chapter.set(options);
+ }
+ this.remove();
+ },
+ error: function() {
+ this.model.set({
+ "uploading": false,
+ "uploadedBytes": 0,
+ "title": gettext("We're sorry, there was an error")
+ });
+ }
+});
diff --git a/cms/templates/textbooks.html b/cms/templates/textbooks.html
index 02b5f42a00..781ea21554 100644
--- a/cms/templates/textbooks.html
+++ b/cms/templates/textbooks.html
@@ -25,444 +25,12 @@
<%block name="jsextra">
+
+