CMS.Models.Textbook = Backbone.AssociatedModel.extend({ defaults: function() { return { name: "", chapters: new CMS.Collections.ChapterSet([{}]), showChapters: false, editing: false }; }, relations: [{ type: Backbone.Many, key: "chapters", relatedModel: "CMS.Models.Chapter", collectionType: "CMS.Collections.ChapterSet" }], 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(); }, url: function() { if(this.isNew()) { return CMS.URL.TEXTBOOKS + "/new"; } else { return CMS.URL.TEXTBOOKS + "/" + this.id; } }, 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: "Textbook name is required", attributes: {name: true} }; } if (attrs.chapters.length === 0) { return { message: "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: "All chapters must have a name and asset", attributes: {chapters: invalidChapters} }; } } } }); CMS.Collections.TextbookSet = Backbone.Collection.extend({ model: CMS.Models.Textbook, url: function() { return CMS.URL.TEXTBOOKS; }, 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') }; }, // 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 && !attrs.asset_path) { return { message: "Chapter name and asset_path are both required", attributes: {name: true, asset_path: true} }; } else if(!attrs.name) { return { message: "Chapter name is required", attributes: {name: true} }; } else if (!attrs.asset_path) { return { message: "asset_path is required", attributes: {asset_path: true} }; } } }); 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": "", "selectedFile": null, "uploading": false, "uploadedBytes": 0, "totalBytes": 0, "finished": false }, // 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.selectedFile && attrs.selectedFile.type !== "application/pdf") { return { message: "Only PDF files can be uploaded. Please select a file ending in .pdf to upload.", attributes: {selectedFile: true} }; } } });